Skip to content

fix(server): type registerTool structuredContent from outputSchema - #2449

Open
mattzcarey wants to merge 1 commit into
mainfrom
fix/sep-2106-output-schema-safety
Open

mattzcarey wants to merge 1 commit into
mainfrom
fix/sep-2106-output-schema-safety

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Jul 7, 2026 •

Copy link
Copy Markdown
Member

McpServer.registerTool now type-checks a callback's successful result against the tool's outputSchema. structuredContent is required and must match the schema, for Standard Schema and the deprecated raw-shape overload. isError: true results, input-required results and tools without an outputSchema keep their current types. Types only: runtime behaviour is unchanged. Closes #2754.

server.registerTool('ping', { outputSchema: z.object({ data: z.string() }) }, async () => ({
    content: [{ type: 'text', text: 'pong' }],
    structuredContent: 'pong' // error: Type 'string' is not assignable to type '{ data: string; }'
}));

Why it is shaped this way:

  • The server validates the handler's raw value and sends it unparsed, while tools/list advertises the output-side JSON Schema that clients validate against. So the required type is the schema's output when that is also a valid input, else input & output. A .default() field must be supplied, a coerced field must already have the output type, and an incoherent .transform() resolves to never.
  • undefined is rejected (the runtime treats it as absent), including for optional roots like z.string().optional(). null is allowed when the schema allows it. An unknown schema output (an untyped fromJsonSchema(...), z.unknown()) accepts any value, so forwarded unknown results still compile; structuredContent is still required.
  • The deprecated raw-shape overload now comes before the Standard Schema overload, and a gate on config lets it match only configs that really use a raw shape. TypeScript reports the last overload's error, so a mismatch now points at structuredContent rather than at ZodRawShape, and zero-argument raw-shape callbacks returning isError: true no longer hit a false positive.
  • New exported types: ToolResultFor, ToolStructuredContent, ToolResultWithStructuredContent, ToolErrorResult. ToolCallback<In, Out?> takes an optional output schema.

This is a compile-time break for tools with an outputSchema, so the changeset is a server minor and lists each case. Besides mismatched or missing structuredContent, it breaks callbacks whose result is typed as plain CallToolResult, and registries forwarding an outputSchema typed S | undefined, which TypeScript checks as if present.

Runtime validation is unchanged and still authoritative for refinements, fromJsonSchema schemas, casts and JavaScript callers.

Implements SEP-2106's guidance that SDKs provide generics over a tool's outputSchema. Supersedes #2438 and #2800, which type the same callback; this version also catches missing structuredContent, keeps content typed and handles transforms. Compile-time tests cover the #2754 snippet, Zod, ArkType, Valibot and raw shapes under tsgo and tsc.

@mattzcarey
mattzcarey requested a review from a team as a code owner July 7, 2026 11:11
@changeset-bot

changeset-bot Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b1b9efb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/server Minor
@modelcontextprotocol/client Minor
@modelcontextprotocol/codemod Minor
@modelcontextprotocol/core Minor
@modelcontextprotocol/server-legacy Minor
@modelcontextprotocol/core-internal Patch

Not sure what this means? Click here to learn what changesets are.

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2449

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2449

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2449

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2449

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2449

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2449

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2449

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2449

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2449

commit: b1b9efb

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Aug 18, 2026
@mattzcarey
mattzcarey force-pushed the fix/sep-2106-output-schema-safety branch from ea5c666 to d92a8bf Compare October 27, 2026 11:51
@mattzcarey mattzcarey changed the title fix: complete SEP-2106 output typing and schema safety fix(server): type registerTool structuredContent from outputSchema Oct 27, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 optional notes from this repository's REVIEW.md or CLAUDE.md checks were not posted as comments, over this review's limit for such notes; they are on this commit's check card.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 skills/mcp-typescript-sdk-upgrade-to-v2/references/server-registration.md — nit: The upgrade skill describes the registerTool overload error and the content rule in ways this change makes false. Lines 113-117 say TS2769 lists "both overloads" with the cause in "the first overload's elaboration". registerTool now has three overloads and the Standard Schema one is last (packages/server/src/server/mcp.ts:1088). Fix: reword each skill statement the new typing contradicts, which covers the 2 sites listed. Same instruction at 2 sites (server-registration.md:117, behavioral-changes.md:200). The second says the TypeScript surface requires content on handler results. [also at: packages/server/src/server/mcp.ts:1033 - nit: The upgrade guide's zod-version diagnosis no longer matches the registerTool error users see. skills/mcp-typescript-sdk-upgrade-to-v2/references/server-registration.md:113-119 describes a "two-overload error" with the cause in "the first overload's elaboration".; skills/mcp-typescript-sdk-upgrade-to-v2/references/server-registration.md:46 - nit: The upgrade skill's troubleshooting text no longer matches the registerTool error it describes. Lines 113-117 of this file say TS2769 lists "both overloads" with the cause in "the first overload's elaboration".] (Finding ID: f-fd569433)

    Why this was flagged

    No runtime trigger; this is a words-are-true check on the upgrade skill that users and agents follow to diagnose migration compile errors. The diff reorders the registerTool overloads at packages/server/src/server/mcp.ts:1032-1105: two deprecated raw-shape overloads come first and the Standard Schema overload is third. With zod typings that lack ~standard.jsonSchema, tsc now reports three overloads and the Property 'jsonSchema' is missing elaboration sits under the last one, not the first, so the "two-overload error" cue at server-registration.md:117 no longer matches. The diff also makes content optional on typed successes (mcp.ts:1403), while skills/mcp-typescript-sdk-upgrade-to-v2/references/behavioral-changes.md:199-201 still says "the TypeScript surface requires content on handler results". On the base both statements matched registerTool. No safeguard applies: the skill text is not checked against the types.

    Verification: On the base, registerTool had two overloads with the Standard Schema one first, so the text was accurate. At HEAD there are three: raw-shape at packages/server/src/server/mcp.ts:1033, raw-shape output-only at :1048, Standard Schema last at :1088. The diff adds ToolResultWithStructuredContent with content?: CallToolResult['content'] (mcp.ts:1403), so content is optional on typed successes.

Comment thread packages/server/src/server/mcp.ts Resolved
Comment thread packages/server/src/server/mcp.ts Resolved
Comment thread packages/server/src/server/mcp.ts Resolved
Comment thread packages/server/src/server/mcp.ts Resolved
Comment thread packages/server/src/server/mcp.ts Resolved
@mattzcarey
mattzcarey force-pushed the fix/sep-2106-output-schema-safety branch from d92a8bf to 518c67c Compare October 27, 2026 12:34

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

3 further findings on code this push did not change were outside the scope of this re-review and not posted.

Still open from earlier reviews (7):

  • 🔴 .changeset/register-tool-output-typing.md:5 — Users upgrading get compile errors on correct tool callbacks that the changeset does not list as breaking. Line 5 names…
  • Also unresolved: 6 minor or pre-existing.

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

@mattzcarey
mattzcarey force-pushed the fix/sep-2106-output-schema-safety branch from 518c67c to 83b661f Compare October 27, 2026 12:52

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

2 other optional suggestions (nits or notes on pre-existing code) were found and not posted.

Comment thread packages/server/src/server/mcp.ts Resolved
Comment thread packages/server/src/server/mcp.ts Resolved
@mattzcarey
mattzcarey force-pushed the fix/sep-2106-output-schema-safety branch from 83b661f to ae97ba9 Compare November 5, 2026 14:20

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

3 other optional suggestions (nits or notes on pre-existing code) were found and not posted.

Comment thread test/e2e/requirements.ts Resolved
A tool's outputSchema now types its callback's successful result:
structuredContent is required and must match the schema, for the
Standard Schema overload and the deprecated raw-shape overload.
isError results, input-required results and schema-less tools keep
their existing types. Types only; runtime behaviour is unchanged.

The required value satisfies both sides of the schema, because the
server validates and sends the handler's raw value while tools/list
advertises the output-side JSON Schema. undefined is excluded because
the runtime treats it as absent; an unknown schema output accepts any
value.

The raw-shape overload moves ahead of the Standard Schema overload so
overload errors point at structuredContent. A gate on config lets it
match only configs that use a raw shape, so optional or any-typed
schemas keep resolving as before.

Closes #2754.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment on lines +5 to +6
* other registration must compile. `pnpm typecheck` enforces both. The runtime assertions only
* pin the authoring affordances the types rely on (content auto-fill).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): The new test file's header describes runtime assertions for "content auto-fill" that the file does not contain, so maintainers are pointed at coverage and behaviour that do not exist. Lines 5-6 say "The runtime assertions only pin the authoring affordances the types rely on (content auto-fill)". The file has no expect( call, and line 12 still imports expect. The same file now asserts that content is required (lines 128-140). Fix: drop that sentence and the unused expect import so the header says the file is type-level only.

Why this was flagged

No runtime trigger; this is a wording check on a comment the diff adds. packages/server/test/server/registerToolOutputTyping.test.ts:5-6 claims runtime assertions that pin "content auto-fill". The file contains only expectTypeOf checks and @ ts-expect-error lines; expect is imported at line 12 and never called. The typed result declares content: CallToolResult['content'] as required (packages/server/src/server/mcp.ts ToolResultWithStructuredContent), and the test at lines 128-140 is titled 'requires content on every result, typed or not', so there is no auto-fill behaviour for the comment to refer to. The base branch has no such file. No safeguard catches it: lint runs eslint src/ only (packages/server/package.json:155), and the shared tsconfig sets no noUnusedLocals, so the unused import and the stale sentence pass CI.

Verification: nit — no runtime trigger; a stale header comment in a file this diff adds. packages/server/test/server/registerToolOutputTyping.test.ts:5-6 says "The runtime assertions only pin the authoring affordances the types rely on (content auto-fill)." expect is imported at line 12 and never used. So there are no runtime assertions at all, and nothing pins "content auto-fill".

Finding ID: f-1a117278

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v2] Should raise error at build time if zod outputSchema is not consistent with the structuredContent type

1 participant