Skip to content

fix(server): handle ZodObject in RegisteredTool.update (#1960)#1971

Open
MukundaKatta wants to merge 1 commit intomodelcontextprotocol:v1.xfrom
MukundaKatta:fix/registered-tool-update-zod-object
Open

fix(server): handle ZodObject in RegisteredTool.update (#1960)#1971
MukundaKatta wants to merge 1 commit intomodelcontextprotocol:v1.xfrom
MukundaKatta:fix/registered-tool-update-zod-object

Conversation

@MukundaKatta
Copy link
Copy Markdown

Fixes #1960.

RegisteredTool.update({ paramsSchema }) crashes with TypeError: Cannot read properties of null (reading '_zod') when paramsSchema is a ZodObject (e.g. z.object({...}).passthrough()) instead of a raw shape.

Why

The create path in _createRegisteredTool uses getZodSchemaObject(), which handles both raw shapes and ZodObject instances:

function getZodSchemaObject(schema) {
    if (!schema) return undefined;
    if (isZodRawShapeCompat(schema)) return objectFromShape(schema);
    return schema;
}

The update path was calling objectFromShape() directly. objectFromShape does Object.values(shape), which on a ZodObject iterates internal fields like _cached (which is null), and downstream isZ4Schema(null) accesses null._zod and throws.

Fix

  • In RegisteredTool.update, route paramsSchema and outputSchema through getZodSchemaObject() (the same helper the create path uses) instead of objectFromShape() directly.
  • Widen the update<InputArgs, OutputArgs> generic constraints from ZodRawShapeCompat to ZodRawShapeCompat | AnySchema, matching registerTool. This resolves the type-level inconsistency mentioned in the issue.

Net diff is ~3 lines of code; no new public API.

Test

Added should update tool with ZodObject paramsSchema in test/server/mcp.test.ts. It registers a tool with z.object({...}).passthrough(), updates it with a fresh ZodObject, and asserts tool.update(...) does not throw and the new schema is reflected in tools/list and the call result. The test runs under the existing zodTestMatrix, so it covers both Zod v3 and v4.

Reproduction (before this PR)

const schema = z.object({ id: z.string() }).passthrough();
const tool = server.registerTool('my_tool', { description: 'test', inputSchema: schema }, async () => ({ content: [{ type: 'text', text: 'ok' }] }));
tool.update({ paramsSchema: schema });
// TypeError: Cannot read properties of null (reading '_zod')

After the patch this is a no-op update with no crash.

@MukundaKatta MukundaKatta requested a review from a team as a code owner April 28, 2026 03:41
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

⚠️ No Changeset found

Latest commit: d7d87a3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 28, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@1971

commit: d7d87a3

…rotocol#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 modelcontextprotocol#1960.

Fixes modelcontextprotocol#1960
@MukundaKatta MukundaKatta force-pushed the fix/registered-tool-update-zod-object branch from 0435482 to d7d87a3 Compare April 28, 2026 05:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant