From d7d87a3a4f31d89252195c5907927ac23c9e9f5c Mon Sep 17 00:00:00 2001 From: Mukunda Rao Katta Date: Mon, 27 Apr 2026 20:40:51 -0700 Subject: [PATCH 1/3] fix(server): handle ZodObject in RegisteredTool.update (#1960) The create path uses `getZodSchemaObject()` which accepts both raw shapes and ZodObject instances. The update path was calling `objectFromShape()` directly, which iterates `Object.values(shape)` and blows up on internal Zod fields when given a ZodObject (e.g. `z.object({...}).passthrough()`): TypeError: Cannot read properties of null (reading '_zod') Mirror the create path so updating with either form works. Also widen the `paramsSchema` / `outputSchema` generics on `RegisteredTool.update` to match `registerTool` (`ZodRawShapeCompat | AnySchema`), fixing the type-level inconsistency called out in #1960. Fixes #1960 --- src/server/mcp.ts | 6 ++-- test/server/mcp.test.ts | 72 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 6 deletions(-) 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' } ] From 707108d0b93b9cef21695f41da495a5582692ec2 Mon Sep 17 00:00:00 2001 From: Mukunda Rao Katta Date: Tue, 28 Apr 2026 08:21:56 -0700 Subject: [PATCH 2/3] chore: add changeset for #1960 fix --- .changeset/fix-registered-tool-update-zod-object.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-registered-tool-update-zod-object.md 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..302abd02c --- /dev/null +++ b/.changeset/fix-registered-tool-update-zod-object.md @@ -0,0 +1,5 @@ +--- +'@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. From 8c7414579c267b34e269740e241ded27aa5d1555 Mon Sep 17 00:00:00 2001 From: Mukunda Rao Katta Date: Tue, 28 Apr 2026 08:29:28 -0700 Subject: [PATCH 3/3] chore: wrap changeset prose to satisfy prettier --- .changeset/fix-registered-tool-update-zod-object.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/fix-registered-tool-update-zod-object.md b/.changeset/fix-registered-tool-update-zod-object.md index 302abd02c..38dc0cf22 100644 --- a/.changeset/fix-registered-tool-update-zod-object.md +++ b/.changeset/fix-registered-tool-update-zod-object.md @@ -2,4 +2,6 @@ '@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. +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.