diff --git a/.changeset/fix-registered-tool-update-zod-object.md b/.changeset/fix-registered-tool-update-zod-object.md new file mode 100644 index 000000000..38dc0cf22 --- /dev/null +++ b/.changeset/fix-registered-tool-update-zod-object.md @@ -0,0 +1,7 @@ +--- +'@modelcontextprotocol/sdk': patch +--- + +Fix `RegisteredTool.update` crash when given a Zod object schema. The update +path now uses the same `getZodSchemaObject` helper as `_createRegisteredTool` +so both paths handle `ZodObject` inputs cleanly. Resolves #1960. diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 9fe0ed549..a94bbb387 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -905,8 +905,8 @@ export class McpServer { } if (typeof updates.title !== 'undefined') registeredTool.title = updates.title; if (typeof updates.description !== 'undefined') registeredTool.description = updates.description; - if (typeof updates.paramsSchema !== 'undefined') registeredTool.inputSchema = objectFromShape(updates.paramsSchema); - if (typeof updates.outputSchema !== 'undefined') registeredTool.outputSchema = objectFromShape(updates.outputSchema); + if (typeof updates.paramsSchema !== 'undefined') registeredTool.inputSchema = getZodSchemaObject(updates.paramsSchema); + if (typeof updates.outputSchema !== 'undefined') registeredTool.outputSchema = getZodSchemaObject(updates.outputSchema); if (typeof updates.callback !== 'undefined') registeredTool.handler = updates.callback; if (typeof updates.annotations !== 'undefined') registeredTool.annotations = updates.annotations; if (typeof updates._meta !== 'undefined') registeredTool._meta = updates._meta; @@ -1312,7 +1312,7 @@ export type RegisteredTool = { enabled: boolean; enable(): void; disable(): void; - update(updates: { + update(updates: { name?: string | null; title?: string; description?: string; diff --git a/test/server/mcp.test.ts b/test/server/mcp.test.ts index 575d6a300..03cb81dd4 100644 --- a/test/server/mcp.test.ts +++ b/test/server/mcp.test.ts @@ -407,7 +407,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { callback: async () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'Updated response' } ] @@ -534,6 +534,72 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(notifications).toHaveLength(0); }); + /*** + * Test: Updating Tool with a ZodObject paramsSchema (regression for #1960). + * The create path accepts both raw shapes and ZodObject instances, but the + * update path used to call `objectFromShape` directly which crashed on + * ZodObject inputs (e.g. `z.object({...}).passthrough()`). + */ + test('should update tool with ZodObject paramsSchema', async () => { + const mcpServer = new McpServer({ + name: 'test server', + version: '1.0' + }); + const client = new Client({ + name: 'test client', + version: '1.0' + }); + + // Register the tool using a ZodObject (not a raw shape); this path + // already worked before the fix. + const initialSchema = z.object({ id: z.string() }).passthrough(); + const tool = mcpServer.registerTool( + 'test', + { + description: 'test', + inputSchema: initialSchema + }, + async ({ id }) => ({ + content: [{ type: 'text', text: `Initial: ${id}` }] + }) + ); + + // Update the tool with a fresh ZodObject. Before #1960's fix this + // threw `TypeError: Cannot read properties of null (reading '_zod')`. + const updatedSchema = z.object({ id: z.string(), value: z.number() }).passthrough(); + expect(() => + tool.update({ + paramsSchema: updatedSchema, + callback: async ({ id, value }) => ({ + content: [{ type: 'text', text: `Updated: ${id}, ${value}` }] + }) + }) + ).not.toThrow(); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]); + + const listResult = await client.request({ method: 'tools/list' }, ListToolsResultSchema); + expect(listResult.tools[0].inputSchema).toMatchObject({ + properties: { + id: { type: 'string' }, + value: { type: 'number' } + } + }); + + const callResult = await client.request( + { + method: 'tools/call', + params: { + name: 'test', + arguments: { id: 'abc', value: 42 } + } + }, + CallToolResultSchema + ); + expect(callResult.content).toEqual([{ type: 'text', text: 'Updated: abc, 42' }]); + }); + /*** * Test: Updating Tool with outputSchema */ @@ -574,7 +640,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { sum: z.number() }, callback: async () => ({ - content: [{ type: 'text', text: '' }], + content: [{ type: 'text' as const, text: '' }], structuredContent: { result: 42, sum: 100 @@ -661,7 +727,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { callback: async () => ({ content: [ { - type: 'text', + type: 'text' as const, text: 'Updated response' } ]