Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/fix-registered-tool-update-zod-object.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1312,7 +1312,7 @@ export type RegisteredTool = {
enabled: boolean;
enable(): void;
disable(): void;
update<InputArgs extends ZodRawShapeCompat, OutputArgs extends ZodRawShapeCompat>(updates: {
update<InputArgs extends ZodRawShapeCompat | AnySchema, OutputArgs extends ZodRawShapeCompat | AnySchema>(updates: {
name?: string | null;
title?: string;
description?: string;
Expand Down
72 changes: 69 additions & 3 deletions test/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
callback: async () => ({
content: [
{
type: 'text',
type: 'text' as const,
text: 'Updated response'
}
]
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -661,7 +727,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
callback: async () => ({
content: [
{
type: 'text',
type: 'text' as const,
text: 'Updated response'
}
]
Expand Down
Loading