8000 Support cantrip selection in character management by Mnehmos · Pull Request #96 · Mnehmos/mnehmos.rpg.mcp · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/server/consolidated/character-manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const CreateSchema = z.object({
characterType: CharacterTypeSchema.optional().default('pc'),
factionId: z.string().optional(),
behavior: z.string().optional(),
cantripsKnown: z.array(z.string()).optional().default([]),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add cantrips to the registered input schema

When character_manage is invoked through the MCP server, withSession first parses arguments with CharacterManageTool.inputSchema, which does not declare cantripsKnown; Zod therefore strips the field before the router reaches this action-specific schema. Consequently, both create and update selections are silently ignored in production even though the direct-handler test passes. Add cantripsKnown to the shared registered input schema as well.

Useful? React with 👍 / 👎.

knownSpells: z.array(z.string()).optional().default([]),
preparedSpells: z.array(z.string()).optional().default([]),
resistances: z.array(z.string()).optional().default([]),
Expand Down Expand Up @@ -142,6 +143,7 @@ const UpdateSchema = z.object({
level: z.number().int().min(1).optional(),
characterType: CharacterTypeSchema.optional(),
stats: StatsSchema.partial().optional(),
cantripsKnown: z.array(z.string()).optional(),
knownSpells: z.array(z.string()).optional(),
preparedSpells: z.array(z.string()).optional(),
skillProficiencies: z.array(SkillProficiencySchema).optional(),
Expand Down Expand Up @@ -325,7 +327,7 @@ export async function handleCreate(args: z.infer<typeof CreateSchema>): Promise<
factionId: args.factionId,
behavior: arg 8000 s.behavior,
knownSpells: args.knownSpells || [],
cantripsKnown: [],
cantripsKnown: args.cantripsKnown || [],
// Known spells are immediately usable after creation unless the caller
// explicitly supplies a prepared list.
preparedSpells: args.preparedSpells?.length
Expand Down Expand Up @@ -379,7 +381,9 @@ export async function handleCreate(args: z.infer<typeof CreateSchema>): Promise<
character.preparedSpells = args.preparedSpells?.length
? [...new Set(args.preparedSpells)]
: [...new Set(character.knownSpells as string[])];
character.cantripsKnown = provisioningResult.cantripsGranted || [];
character.cantripsKnown = args.cantripsKnown?.length
? [...new Set(args.cantripsKnown)]
: provisioningResult.cantripsGranted || [];
Comment on lines +384 to +386
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass selected cantrips into provisioning

When a caster is created with provisioning enabled and explicit cantrips, the provisioning call receives customSpells but not customCantrips, so provisioningResult.cantripsGranted still contains the class defaults. These lines persist the explicit selection, but the same response reports the defaults under _provisioning.cantripsGranted, giving downstream consumers a contradictory provisioning receipt. Pass the selection as customCantrips so the result and persisted character agree.

Useful? React with 👍 / 👎.

character.spellSlots = convertSpellSlotsToObject(provisioningResult.spellSlots ?? null);
character.pactMagicSlots = provisioningResult.pactMagicSlots || undefined;

Expand Down Expand Up @@ -480,6 +484,7 @@ async function handleUpdate(args: z.infer<typeof UpdateSchema>): Promise<object>
if (args.alignment !== undefined) updateData.alignment = args.alignment;
if (args.origin !== undefined) updateData.origin = args.origin;
if (args.stats !== undefined) updateData.stats = args.stats;
if (args.cantripsKnown !== undefined) updateData.cantripsKnown = args.cantripsKnown;
if (args.knownSpells !== undefined) updateData.knownSpells = args.knownSpells;
if (args.preparedSpells !== undefined) updateData.preparedSpells = args.preparedSpells;
if (args.skillProficiencies !== undefined) updateData.skillProficiencies = args.skillProficiencies;
Expand Down
23 changes: 23 additions & 0 deletions tests/server/consolidated/character-manage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,29 @@ describe('character_manage consolidated tool', () => {
expect(parsed.toolProficiencies).toEqual(['vehicles (land)']);
});

it('persists explicit cantrips on create and update', async () => {
const createdResult = await handleCharacterManage({
action: 'create',
name: 'Spellbook Test',
class: 'Wizard',
cantripsKnown: ['Fire Bolt'],
knownSpells: ['Magic Missile'],
preparedSpells: ['Magic Missile'],
}, ctx);
const created = extractJson(createdResult.content[0].text);

expect(created.cantripsKnown).toEqual(['Fire Bolt']);

const updatedResult = await handleCharacterManage({
action: 'update',
characterId: created.id,
cantripsKnown: ['Ray of Frost'],
}, ctx);
const updated = extractJson(updatedResult.content[0].text);

expect(updated.cantripsKnown).toEqual(['Ray of Frost']);
});

it('round-trips all character proficiency fields through get and update', async () => {
const createdResult = await handleCharacterManage({
action: 'create',
Expand Down
Loading
0