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 overloads. isError: true results, input-required results and tools without an outputSchema keep their current types. 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; }'
}));

server.registerTool('ping', { outputSchema: z.object({ data: z.string() }) }, async () => ({
    structuredContent: { data: 'pong' } // ok: content omitted, sent as a JSON text block
}));

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 even for z.unknown(), because the runtime treats it as absent. null is allowed when the schema allows it.
  • content is optional on typed successes. When it is omitted, McpServer sends the JSON of structuredContent as a text block, so clients that only read content still see the result. This is the one runtime change: such results used to go out with content: [].
  • 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.
  • An unknown schema output (an untyped fromJsonSchema(...)) accepts any value, so forwarded unknown results still compile.
  • 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.

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: 83b661f

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: 83b661f

@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 .changeset/register-tool-output-typing.md Outdated
Comment thread packages/server/src/server/mcp.ts
Comment thread packages/server/src/server/mcp.ts Outdated
Comment thread packages/server/src/server/mcp.ts Outdated
Comment thread packages/server/src/server/mcp.ts
Comment thread skills/mcp-typescript-sdk-upgrade-to-v2/references/server-registration.md Outdated
Comment thread packages/server/src/server/mcp.ts Outdated
@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.

A tool's outputSchema now types its callback's successful result:
structuredContent is required and must match the schema, for both the
Standard Schema overload and the deprecated raw-shape overloads.
isError results, input-required results and schema-less tools keep
their existing types. Typed successes may omit content, which the
server already fills in.

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.

The raw-shape overloads move ahead of the Standard Schema overload and
now require a raw shape, so overload errors for the common case point
at the callback rather than at ZodRawShape.

Closes #2754.
@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
Comment thread packages/server/src/server/mcp.ts

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