Skip to content

Commit d7d87a3

Browse files
committed
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
1 parent bf1e022 commit d7d87a3

2 files changed

Lines changed: 72 additions & 6 deletions

File tree

src/server/mcp.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -905,8 +905,8 @@ export class McpServer {
905905
}
906906
if (typeof updates.title !== 'undefined') registeredTool.title = updates.title;
907907
if (typeof updates.description !== 'undefined') registeredTool.description = updates.description;
908-
if (typeof updates.paramsSchema !== 'undefined') registeredTool.inputSchema = objectFromShape(updates.paramsSchema);
909-
if (typeof updates.outputSchema !== 'undefined') registeredTool.outputSchema = objectFromShape(updates.outputSchema);
908+
if (typeof updates.paramsSchema !== 'undefined') registeredTool.inputSchema = getZodSchemaObject(updates.paramsSchema);
909+
if (typeof updates.outputSchema !== 'undefined') registeredTool.outputSchema = getZodSchemaObject(updates.outputSchema);
910910
if (typeof updates.callback !== 'undefined') registeredTool.handler = updates.callback;
911911
if (typeof updates.annotations !== 'undefined') registeredTool.annotations = updates.annotations;
912912
if (typeof updates._meta !== 'undefined') registeredTool._meta = updates._meta;
@@ -1312,7 +1312,7 @@ export type RegisteredTool = {
13121312
enabled: boolean;
13131313
enable(): void;
13141314
disable(): void;
1315-
update<InputArgs extends ZodRawShapeCompat, OutputArgs extends ZodRawShapeCompat>(updates: {
1315+
update<InputArgs extends ZodRawShapeCompat | AnySchema, OutputArgs extends ZodRawShapeCompat | AnySchema>(updates: {
13161316
name?: string | null;
13171317
title?: string;
13181318
description?: string;

test/server/mcp.test.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
407407
callback: async () => ({
408408
content: [
409409
{
410-
type: 'text',
410+
type: 'text' as const,
411411
text: 'Updated response'
412412
}
413413
]
@@ -534,6 +534,72 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
534534
expect(notifications).toHaveLength(0);
535535
});
536536

537+
/***
538+
* Test: Updating Tool with a ZodObject paramsSchema (regression for #1960).
539+
* The create path accepts both raw shapes and ZodObject instances, but the
540+
* update path used to call `objectFromShape` directly which crashed on
541+
* ZodObject inputs (e.g. `z.object({...}).passthrough()`).
542+
*/
543+
test('should update tool with ZodObject paramsSchema', async () => {
544+
const mcpServer = new McpServer({
545+
name: 'test server',
546+
version: '1.0'
547+
});
548+
const client = new Client({
549+
name: 'test client',
550+
version: '1.0'
551+
});
552+
553+
// Register the tool using a ZodObject (not a raw shape); this path
554+
// already worked before the fix.
555+
const initialSchema = z.object({ id: z.string() }).passthrough();
556+
const tool = mcpServer.registerTool(
557+
'test',
558+
{
559+
description: 'test',
560+
inputSchema: initialSchema
561+
},
562+
async ({ id }) => ({
563+
content: [{ type: 'text', text: `Initial: ${id}` }]
564+
})
565+
);
566+
567+
// Update the tool with a fresh ZodObject. Before #1960's fix this
568+
// threw `TypeError: Cannot read properties of null (reading '_zod')`.
569+
const updatedSchema = z.object({ id: z.string(), value: z.number() }).passthrough();
570+
expect(() =>
571+
tool.update({
572+
paramsSchema: updatedSchema,
573+
callback: async ({ id, value }) => ({
574+
content: [{ type: 'text', text: `Updated: ${id}, ${value}` }]
575+
})
576+
})
577+
).not.toThrow();
578+
579+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
580+
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
581+
582+
const listResult = await client.request({ method: 'tools/list' }, ListToolsResultSchema);
583+
expect(listResult.tools[0].inputSchema).toMatchObject({
584+
properties: {
585+
id: { type: 'string' },
586+
value: { type: 'number' }
587+
}
588+
});
589+
590+
const callResult = await client.request(
591+
{
592+
method: 'tools/call',
593+
params: {
594+
name: 'test',
595+
arguments: { id: 'abc', value: 42 }
596+
}
597+
},
598+
CallToolResultSchema
599+
);
600+
expect(callResult.content).toEqual([{ type: 'text', text: 'Updated: abc, 42' }]);
601+
});
602+
537603
/***
538604
* Test: Updating Tool with outputSchema
539605
*/
@@ -574,7 +640,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
574640
sum: z.number()
575641
},
576642
callback: async () => ({
577-
content: [{ type: 'text', text: '' }],
643+
content: [{ type: 'text' as const, text: '' }],
578644
structuredContent: {
579645
result: 42,
580646
sum: 100
@@ -661,7 +727,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
661727
callback: async () => ({
662728
content: [
663729
{
664-
type: 'text',
730+
type: 'text' as const,
665731
text: 'Updated response'
666732
}
667733
]

0 commit comments

Comments
 (0)