Skip to content

Version Packages (alpha)#1845

Open
github-actions[bot] wants to merge 1 commit intomainfrom
changeset-release/main
Open

Version Packages (alpha)#1845
github-actions[bot] wants to merge 1 commit intomainfrom
changeset-release/main

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot commented Apr 2, 2026

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

⚠️⚠️⚠️⚠️⚠️⚠️

main is currently in pre mode so this branch has prereleases rather than normal releases. If you want to exit prereleases, run changeset pre exit on main.

⚠️⚠️⚠️⚠️⚠️⚠️

Releases

@modelcontextprotocol/client@2.0.0-alpha.3

Minor Changes

  • #1974 db83829 Thanks @felixweinberger! - Add custom (non-spec)
    method support: a 3-arg setRequestHandler(method, schemas, handler) / setNotificationHandler(method, schemas, handler) form for vendor-prefixed methods, and a request(req, resultSchema) overload (also on ctx.mcpReq.send) for typed custom-method results. Spec-method
    calls are unchanged.

    Response result-schema validation failure now rejects with SdkError(InvalidResult) instead of a raw ZodError. Adds SdkErrorCode.InvalidResult.

  • #1653 6bec24a Thanks @rechedev9! - Add validateClientMetadataUrl()
    utility for early validation of clientMetadataUrl

    Exports a validateClientMetadataUrl() function that OAuthClientProvider implementations can call in their constructors to fail fast on invalid URL-based client IDs, instead of discovering the error deep in the auth flow.

  • #1887 96db044 Thanks @felixweinberger! - Export isSpecType and
    specTypeSchemas records for runtime validation of any MCP spec type by name. isSpecType.ContentBlock(value) is a type predicate; specTypeSchemas.ContentBlock is a StandardSchemaV1<ContentBlock> validator. Guards are standalone functions, so
    arr.filter(isSpecType.ContentBlock) works. Also export the SpecTypeName and SpecTypes types.

  • #1871 9fc9070 Thanks @felixweinberger! - Move stdio transports
    to a ./stdio subpath export. Import StdioClientTransport, getDefaultEnvironment, DEFAULT_INHERITED_ENV_VARS, and StdioServerParameters from @modelcontextprotocol/client/stdio, and StdioServerTransport from @modelcontextprotocol/server/stdio. The
    @modelcontextprotocol/client root entry no longer pulls in node:child_process, node:stream, or cross-spawn, fixing bundling for browser and Cloudflare Workers targets; the @modelcontextprotocol/server root entry drops its node:stream reference. Node.js, Bun, and
    Deno consumers update the import path; runtime behavior is unchanged.

Patch Changes

  • #1897 434b2f1 Thanks @felixweinberger! - Stop bundling
    @cfworker/json-schema into the main package barrel. Previously CfWorkerJsonSchemaValidator was re-exported from the core internal barrel, so tsdown inlined the @cfworker/json-schema dev dependency into every consumer's bundle even when it was never used. The validator is
    now reachable only via the _shims conditional (workerd/browser) and the explicit @modelcontextprotocol/{server,client}/validators/cf-worker subpath, so consumers that don't opt into it no longer ship that code. No public API change.

  • #1834 42cb6b2 Thanks @felixweinberger! - Export
    InMemoryTransport for in-process testing.

  • #1898 2a7611d Thanks @felixweinberger! - Add top-level types
    field (and typesVersions on client/server for their subpath exports) so consumers on legacy moduleResolution: "node" can resolve type declarations. The exports map remains the source of truth for nodenext/bundler resolution. The typesVersions map includes entries
    for subpaths added by sibling PRs in this series (zod-schemas, stdio); those entries are no-ops until the corresponding dist/*.d.mts files exist.

  • #1655 1eb3123 Thanks @nielskaspers! - fix(client): append custom
    Accept headers to spec-required defaults in StreamableHTTPClientTransport

    Custom Accept headers provided via requestInit.headers are now appended to the spec-mandated Accept types instead of being overwritten. This ensures the required media types (application/json, text/event-stream for POST; text/event-stream for GET SSE) are always present
    while allowing users to include additional types for proxy/gateway routing.

  • #1976 55b1f06 Thanks @felixweinberger! - refactor: subclasses
    override _wrapHandler hook instead of redeclaring setRequestHandler.

  • #1895 b256546 Thanks @felixweinberger! - Fix runtime crash on
    tools/list when a tool's inputSchema comes from zod 4.0–4.1. The SDK requires ~standard.jsonSchema (StandardJSONSchemaV1, added in zod 4.2.0); previously a missing jsonSchema crashed at undefined[io]. standardSchemaToJsonSchema now detects zod 4 schemas lacking
    jsonSchema and falls back to the SDK-bundled z.toJSONSchema(), emitting a one-time console warning. zod 3 schemas (which the bundled zod 4 converter cannot introspect) and non-zod schema libraries without jsonSchema get a clear error pointing to fromJsonSchema(). The
    workspace zod catalog is also bumped to ^4.2.0.

@modelcontextprotocol/express@2.0.0-alpha.3

Minor Changes

  • #1907 7cccc2a Thanks @felixweinberger! - Add OAuth
    Resource-Server glue to the Express adapter: requireBearerAuth middleware (token verification + RFC 6750 WWW-Authenticate challenges), mcpAuthMetadataRouter (serves RFC 9728 Protected Resource Metadata and mirrors RFC 8414 AS metadata at the resource origin), the
    getOAuthProtectedResourceMetadataUrl helper, and the OAuthTokenVerifier interface. These restore the v1 src/server/auth Resource-Server pieces as first-class v2 API so MCP servers can plug into an external Authorization Server with a few lines of Express wiring.

Patch Changes

  • #1898 2a7611d Thanks @felixweinberger! - Add top-level types
    field (and typesVersions on client/server for their subpath exports) so consumers on legacy moduleResolution: "node" can resolve type declarations. The exports map remains the source of truth for nodenext/bundler resolution. The typesVersions map includes entries
    for subpaths added by sibling PRs in this series (zod-schemas, stdio); those entries are no-ops until the corresponding dist/*.d.mts files exist.

  • Updated dependencies [434b2f1, db83829,
    42cb6b2, df4b6cc,
    2a7611d, e15a8ef,
    96db044, 9fc9070,
    55b1f06, b256546]:

    • @modelcontextprotocol/server@2.0.0-alpha.3

@modelcontextprotocol/server@2.0.0-alpha.3

Minor Changes

  • #1974 db83829 Thanks @felixweinberger! - Add custom (non-spec)
    method support: a 3-arg setRequestHandler(method, schemas, handler) / setNotificationHandler(method, schemas, handler) form for vendor-prefixed methods, and a request(req, resultSchema) overload (also on ctx.mcpReq.send) for typed custom-method results. Spec-method
    calls are unchanged.

    Response result-schema validation failure now rejects with SdkError(InvalidResult) instead of a raw ZodError. Adds SdkErrorCode.InvalidResult.

  • #1887 96db044 Thanks @felixweinberger! - Export isSpecType and
    specTypeSchemas records for runtime validation of any MCP spec type by name. isSpecType.ContentBlock(value) is a type predicate; specTypeSchemas.ContentBlock is a StandardSchemaV1<ContentBlock> validator. Guards are standalone functions, so
    arr.filter(isSpecType.ContentBlock) works. Also export the SpecTypeName and SpecTypes types.

  • #1871 9fc9070 Thanks @felixweinberger! - Move stdio transports
    to a ./stdio subpath export. Import StdioClientTransport, getDefaultEnvironment, DEFAULT_INHERITED_ENV_VARS, and StdioServerParameters from @modelcontextprotocol/client/stdio, and StdioServerTransport from @modelcontextprotocol/server/stdio. The
    @modelcontextprotocol/client root entry no longer pulls in node:child_process, node:stream, or cross-spawn, fixing bundling for browser and Cloudflare Workers targets; the @modelcontextprotocol/server root entry drops its node:stream reference. Node.js, Bun, and
    Deno consumers update the import path; runtime behavior is unchanged.

Patch Changes

  • #1897 434b2f1 Thanks @felixweinberger! - Stop bundling
    @cfworker/json-schema into the main package barrel. Previously CfWorkerJsonSchemaValidator was re-exported from the core internal barrel, so tsdown inlined the @cfworker/json-schema dev dependency into every consumer's bundle even when it was never used. The validator is
    now reachable only via the _shims conditional (workerd/browser) and the explicit @modelcontextprotocol/{server,client}/validators/cf-worker subpath, so consumers that don't opt into it no longer ship that code. No public API change.

  • #1834 42cb6b2 Thanks @felixweinberger! - Export
    InMemoryTransport for in-process testing.

  • #1788 df4b6cc Thanks @claygeo! - Prevent stack overflow in
    StreamableHTTPServerTransport.close() with re-entrant guard

  • #1898 2a7611d Thanks @felixweinberger! - Add top-level types
    field (and typesVersions on client/server for their subpath exports) so consumers on legacy moduleResolution: "node" can resolve type declarations. The exports map remains the source of truth for nodenext/bundler resolution. The typesVersions map includes entries
    for subpaths added by sibling PRs in this series (zod-schemas, stdio); those entries are no-ops until the corresponding dist/*.d.mts files exist.

  • #1901 e15a8ef Thanks @felixweinberger! -
    registerTool/registerPrompt accept a raw Zod shape ({ field: z.string() }) for inputSchema/outputSchema/argsSchema in addition to a wrapped Standard Schema. Raw shapes are auto-wrapped with z.object(). The raw-shape overloads are @deprecated; prefer wrapping
    with z.object().

    Also widens the completable() constraint from StandardSchemaWithJSON to StandardSchemaV1 so v1's completable(z.string(), fn) continues to work.

  • #1976 55b1f06 Thanks @felixweinberger! - refactor: subclasses
    override _wrapHandler hook instead of redeclaring setRequestHandler.

  • #1895 b256546 Thanks @felixweinberger! - Fix runtime crash on
    tools/list when a tool's inputSchema comes from zod 4.0–4.1. The SDK requires ~standard.jsonSchema (StandardJSONSchemaV1, added in zod 4.2.0); previously a missing jsonSchema crashed at undefined[io]. standardSchemaToJsonSchema now detects zod 4 schemas lacking
    jsonSchema and falls back to the SDK-bundled z.toJSONSchema(), emitting a one-time console warning. zod 3 schemas (which the bundled zod 4 converter cannot introspect) and non-zod schema libraries without jsonSchema get a clear error pointing to fromJsonSchema(). The
    workspace zod catalog is also bumped to ^4.2.0.

@modelcontextprotocol/fastify@2.0.0-alpha.3

Patch Changes

  • #1898 2a7611d Thanks @felixweinberger! - Add top-level types
    field (and typesVersions on client/server for their subpath exports) so consumers on legacy moduleResolution: "node" can resolve type declarations. The exports map remains the source of truth for nodenext/bundler resolution. The typesVersions map includes entries
    for subpaths added by sibling PRs in this series (zod-schemas, stdio); those entries are no-ops until the corresponding dist/*.d.mts files exist.

  • Updated dependencies [434b2f1, db83829,
    42cb6b2, df4b6cc,
    2a7611d, e15a8ef,
    96db044, 9fc9070,
    55b1f06, b256546]:

    • @modelcontextprotocol/server@2.0.0-alpha.3

@modelcontextprotocol/hono@2.0.0-alpha.3

Patch Changes

  • #1898 2a7611d Thanks @felixweinberger! - Add top-level types
    field (and typesVersions on client/server for their subpath exports) so consumers on legacy moduleResolution: "node" can resolve type declarations. The exports map remains the source of truth for nodenext/bundler resolution. The typesVersions map includes entries
    for subpaths added by sibling PRs in this series (zod-schemas, stdio); those entries are no-ops until the corresponding dist/*.d.mts files exist.

  • Updated dependencies [434b2f1, db83829,
    42cb6b2, df4b6cc,
    2a7611d, e15a8ef,
    96db044, 9fc9070,
    55b1f06, b256546]:

    • @modelcontextprotocol/server@2.0.0-alpha.3

@modelcontextprotocol/node@2.0.0-alpha.3

Patch Changes

  • #1896 5433f40 Thanks @felixweinberger! - Mark hono peer
    dependency as optional. @modelcontextprotocol/node only uses getRequestListener from @hono/node-server (Node HTTP ↔ Web Standard conversion), which does not require the hono framework at runtime. Consumers no longer need to install hono to use
    NodeStreamableHTTPServerTransport. Note: @hono/node-server itself still declares hono as a hard peer, so package managers may emit a warning; this is upstream and harmless for getRequestListener-only usage.

  • #1898 2a7611d Thanks @felixweinberger! - Add top-level types
    field (and typesVersions on client/server for their subpath exports) so consumers on legacy moduleResolution: "node" can resolve type declarations. The exports map remains the source of truth for nodenext/bundler resolution. The typesVersions map includes entries
    for subpaths added by sibling PRs in this series (zod-schemas, stdio); those entries are no-ops until the corresponding dist/*.d.mts files exist.

  • Updated dependencies [434b2f1, db83829,
    42cb6b2, df4b6cc,
    2a7611d, e15a8ef,
    96db044, 9fc9070,
    55b1f06, b256546]:

    • @modelcontextprotocol/server@2.0.0-alpha.3

@modelcontextprotocol/core@2.0.0-alpha.2

Minor Changes

  • #1974 db83829 Thanks @felixweinberger! - Add custom (non-spec)
    method support: a 3-arg setRequestHandler(method, schemas, handler) / setNotificationHandler(method, schemas, handler) form for vendor-prefixed methods, and a request(req, resultSchema) overload (also on ctx.mcpReq.send) for typed custom-method results. Spec-method
    calls are unchanged.

    Response result-schema validation failure now rejects with SdkError(InvalidResult) instead of a raw ZodError. Adds SdkErrorCode.InvalidResult.

Patch Changes

  • #1930 bdfd7f0 Thanks @Christian-Sidak! - Fix requestStream to
    call tasks/result for failed tasks instead of yielding a hardcoded ProtocolError. When a task reaches the failed terminal status, the stream now retrieves and yields the actual stored result (matching the behavior for completed tasks), as required by the spec.

  • #1901 e15a8ef Thanks @felixweinberger! -
    registerTool/registerPrompt accept a raw Zod shape ({ field: z.string() }) for inputSchema/outputSchema/argsSchema in addition to a wrapped Standard Schema. Raw shapes are auto-wrapped with z.object(). The raw-shape overloads are @deprecated; prefer wrapping
    with z.object().

    Also widens the completable() constraint from StandardSchemaWithJSON to StandardSchemaV1 so v1's completable(z.string(), fn) continues to work.

  • #1976 55b1f06 Thanks @felixweinberger! - refactor: subclasses
    override _wrapHandler hook instead of redeclaring setRequestHandler.

  • #1768 866c08d Thanks @felixweinberger! - Allow additional JSON
    Schema properties in elicitInput's requestedSchema type by adding .catchall(z.unknown()), matching the pattern used by inputSchema. This fixes type incompatibility when using Zod v4's .toJSONSchema() output which includes extra properties like $schema and additionalProperties.

  • #1895 b256546 Thanks @felixweinberger! - Fix runtime crash on
    tools/list when a tool's inputSchema comes from zod 4.0–4.1. The SDK requires ~standard.jsonSchema (StandardJSONSchemaV1, added in zod 4.2.0); previously a missing jsonSchema crashed at undefined[io]. standardSchemaToJsonSchema now detects zod 4 schemas lacking
    jsonSchema and falls back to the SDK-bundled z.toJSONSchema(), emitting a one-time console warning. zod 3 schemas (which the bundled zod 4 converter cannot introspect) and non-zod schema libraries without jsonSchema get a clear error pointing to fromJsonSchema(). The
    workspace zod catalog is also bumped to ^4.2.0.

@github-actions github-actions Bot requested a review from a team as a code owner April 2, 2026 11:51
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — routine automated version bump from the Changesets action.

Extended reasoning...

Overview

This PR touches three files: .changeset/pre.json (adds the zod-json-schema-compat changeset entry), packages/core/CHANGELOG.md (adds the 2.0.0-alpha.2 release section), and packages/core/package.json (bumps the version from 2.0.0-alpha.1 to 2.0.0-alpha.2). All changes are auto-generated by the Changesets release action.

Security risks

None. These are purely metadata/versioning files with no executable logic.

Level of scrutiny

Minimal scrutiny required. Auto-generated version bump PRs from the Changesets bot are among the simplest, most mechanical changes in a repo — the content is derived deterministically from merged changesets.

Other factors

No bugs were found by the bug hunting system, no prior review comments are outstanding, and the diff is a textbook Changesets alpha pre-release bump. Safe to approve.

@github-actions github-actions Bot force-pushed the changeset-release/main branch 3 times, most recently from 35992ec to 1aaa39d Compare April 2, 2026 13:01
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — standard auto-generated Changesets release PR with only version bumps and CHANGELOG additions.

Extended reasoning...

Overview

This is an automated "Version Packages (alpha)" PR generated by the Changesets release action. It touches five files: .changeset/pre.json (adds two new changeset IDs), packages/client/CHANGELOG.md and packages/core/CHANGELOG.md (CHANGELOG entries), and packages/client/package.json / packages/core/package.json (version bumps only — 2.0.0-alpha.2 → 2.0.0-alpha.3 for client, 2.0.0-alpha.1 → 2.0.0-alpha.2 for core). No production logic is changed.

Security risks

None. The diff contains no code — only version strings, CHANGELOG prose, and changeset bookkeeping. No auth, crypto, or permissions paths are touched.

Level of scrutiny

Minimal scrutiny warranted. Automated release PRs of this kind are fully deterministic outputs of changeset version; the actual code changes they describe landed in earlier commits and were reviewed there.

Other factors

Two inline bug comments are attached. The first is a nit about CHANGELOG wording inverting the Accept header ordering described in PR #1655 — doc-only, no functional impact. The second is a pre-existing inconsistency in packages/core/package.json (zod listed in both dependencies and peerDependencies) that was not introduced by this PR and has no npm consumer impact since core is private: true. Neither issue warrants blocking this release PR.

Comment on lines +8 to +12
Accept headers to spec-required defaults in StreamableHTTPClientTransport

Custom Accept headers provided via `requestInit.headers` are now appended to the spec-mandated Accept types instead of being overwritten. This ensures the required media types (`application/json, text/event-stream` for POST; `text/event-stream` for GET SSE) are always present
while allowing users to include additional types for proxy/gateway routing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The CHANGELOG entry says custom Accept headers are "appended to the spec-mandated Accept types", implying spec-required types come first, but the actual implementation does the reverse: user-provided types are placed first and spec-required types are appended last. The description should be corrected to accurately reflect the ordering, e.g., "spec-mandated Accept types are appended to any custom Accept headers".

Extended reasoning...

CHANGELOG Wording Inverts Actual Accept Header Ordering

What the bug is and how it manifests

The new CHANGELOG entry (line 10) states: "Custom Accept headers provided via requestInit.headers are now appended to the spec-mandated Accept types instead of being overwritten." In standard English, "X appended to Y" means Y is the base and X is added after — so this sentence implies spec-required types come first, with user custom types added afterward.

However, the actual implementation in streamableHttp.ts does the opposite. The GET SSE path uses: [...(userAccept?.split(',').map(s => s.trim().toLowerCase()) ?? []), 'text/event-stream'] and the POST path uses [...(userAccept?.split(',') ?? []), 'application/json', 'text/event-stream']. User-provided types come first, and spec-required types are appended last.

The specific code path and why it matters

Per RFC 7231, when multiple Accept types share equal q-values, earlier entries carry higher implicit preference. So the actual behavior gives user-provided types higher preference than spec-required types. The CHANGELOG implies the opposite preference ordering. While this ordering may be intentional (user routing hints for proxies need higher preference), the documentation still misrepresents which type comes first.

Addressing the refutation

The refutation argues that user-types-first is intentional for proxy/gateway routing, that CHANGELOG entries are not formal specs, and that this is a version-bump PR. These points are valid context, but they do not resolve the wording inaccuracy. The key claim in the CHANGELOG — that spec-required types are always present — is correct, but the described ordering is backwards. The fix is a one-line wording update, not a code change. Since this CHANGELOG entry appears in this PR's diff, this is the appropriate place to catch it.

Step-by-step proof of the discrepancy

  1. User sets requestInit.headers = { Accept: 'application/vnd.proxy-hint' }
  2. Code computes types = ['application/vnd.proxy-hint', 'text/event-stream']
  3. Final header: Accept: application/vnd.proxy-hint, text/event-stream
  4. application/vnd.proxy-hint has higher implicit preference (appears first)
  5. CHANGELOG says custom types are "appended to" spec types, which would produce text/event-stream, application/vnd.proxy-hint (spec first, custom second)
  6. The actual and described orderings are reversed.

Impact and fix

This is a documentation-only inaccuracy — the functional behavior is correct (spec-required types are always present). The fix is to update the CHANGELOG wording to something like: "spec-mandated Accept types are appended to any custom Accept headers provided via requestInit.headers", which accurately reflects user types appearing first with spec types appended after. The secondary q-value deduplication issue (e.g., text/event-stream; q=0.9 not deduped against text/event-stream) is a pre-existing edge case from PR #1655 and not introduced by this PR.

"name": "@modelcontextprotocol/core",
"private": true,
"version": "2.0.0-alpha.1",
"version": "2.0.0-alpha.2",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟣 Pre-existing: packages/core/package.json lists zod in both dependencies (auto-installs) and peerDependencies with optional: false (signals manual install required) — these are contradictory. The drop-zod-peer-dep changeset cleaned this up for the public client and server packages but missed core. Since core is private: true, there is no npm consumer impact, but the redundant peer dependency entry should be removed for consistency.

Extended reasoning...

Analysis

packages/core/package.json currently has zod in two conflicting locations:

  1. dependencies (line 51): "zod": "catalog:runtimeShared" — this means zod is a direct dependency that auto-installs for any consumer of core.
  2. peerDependencies (line 55): "zod": "catalog:runtimeShared" — combined with peerDependenciesMeta.zod.optional: false (line 62), this signals to package managers that users must install zod themselves as a peer.

These two declarations are semantically contradictory. A direct dependency auto-installs and requires nothing from the consumer; a non-optional peer dependency generates package-manager warnings telling consumers they must install the dependency themselves.

Why existing code does not prevent this

The drop-zod-peer-dep changeset (documented in packages/client/CHANGELOG.md under the 2.0.0-alpha.1 entry) explicitly dropped zod from peerDependencies in @modelcontextprotocol/client and @modelcontextprotocol/server. The changeset file targets those two packages but the same cleanup was not applied to packages/core/package.json. The version bump in this PR touches packages/core/package.json (line 4, version field) but does not address this inconsistency.

Step-by-step proof

  1. Open packages/core/package.json.
  2. Observe "zod": "catalog:runtimeShared" under dependencies — zod will auto-install in any workspace consuming core.
  3. Observe "zod": "catalog:runtimeShared" under peerDependencies — a package manager resolving this package would also emit a peer dependency warning.
  4. Observe peerDependenciesMeta.zod.optional: false — this marks the peer as required, not optional, making the warning an error in strict tools.
  5. Compare with packages/client/package.json: zod is only in dependencies, no peerDependencies entry — the cleanup was done there.

Impact

Practical impact is minimal: core is "private": true and is never published to npm, so no external consumers will encounter the contradictory peer dependency warning. Within the pnpm workspace the direct dependency satisfies the peer requirement automatically. The issue is purely a documentation/consistency problem.

Fix

Remove the zod entry from peerDependencies and its corresponding peerDependenciesMeta.zod block in packages/core/package.json, matching what was done for the client and server packages in the drop-zod-peer-dep changeset.

@github-actions github-actions Bot force-pushed the changeset-release/main branch from 1aaa39d to 0ac069f Compare April 4, 2026 05:42
Comment thread packages/server/package.json
Comment on lines +7 to +8
- [#1788](https://github.com/modelcontextprotocol/typescript-sdk/pull/1788) [`df4b6cc`](https://github.com/modelcontextprotocol/typescript-sdk/commit/df4b6cc88d6f24fc857519cf506a7a039f532637) Thanks [@claygeo](https://github.com/claygeo)! - Prevent stack overflow in
StreamableHTTPServerTransport.close() with re-entrant guard
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The CHANGELOG entry for 2.0.0-alpha.3 references a non-existent class name StreamableHTTPServerTransport.close(); the actual class is WebStandardStreamableHTTPServerTransport (there is also a separate NodeStreamableHTTPServerTransport). Update the entry to use the full, unambiguous class name to match both the codebase and the other entry in the same file.

Extended reasoning...

What the bug is

The new CHANGELOG entry in packages/server/CHANGELOG.md (lines 7–8) reads:

Prevent stack overflow in StreamableHTTPServerTransport.close() with re-entrant guard

No class named StreamableHTTPServerTransport exists anywhere in the codebase. The abbreviated name was informally used in the changeset file (fix-streamable-close-reentrant.md) and propagated into the CHANGELOG by the Changesets tooling.

The specific code that was changed

The re-entrancy fix (a _closed flag check) was applied to WebStandardStreamableHTTPServerTransport.close() in packages/server/src/server/streamableHttp.ts around line 224. A separate NodeStreamableHTTPServerTransport exists in packages/middleware/node. The abbreviated name is ambiguous between both implementations.

Why existing validation doesn't catch it

Changesets generates CHANGELOG prose from the markdown body of changeset files. The changeset author used the shorthand StreamableHTTPServerTransport, and no lint rule or automated check validates class name accuracy inside CHANGELOG prose. The error therefore passed through undetected.

Impact

Developers searching the CHANGELOG or codebase for StreamableHTTPServerTransport will find zero results and may conclude the entry refers to a different or removed class. The ambiguity between WebStandardStreamableHTTPServerTransport and NodeStreamableHTTPServerTransport could also cause confusion about which implementation received the fix. The other 2.0.0-alpha.2 entry in the same file (fix-onerror-callbacks) correctly names WebStandardStreamableHTTPServerTransport, making the inconsistency more jarring.

Step-by-step proof

  1. Open packages/server/CHANGELOG.md, lines 7–8: entry says StreamableHTTPServerTransport.close().
  2. Search the codebase for class StreamableHTTPServerTransport — zero matches.
  3. Search for class WebStandardStreamableHTTPServerTransport — found in packages/server/src/server/streamableHttp.ts:224.
  4. Search for class NodeStreamableHTTPServerTransport — found in packages/middleware/node.
  5. The same CHANGELOG file, line 99 (alpha.2 entry), correctly uses the full name WebStandardStreamableHTTPServerTransport.

How to fix

Change the CHANGELOG entry to: "Prevent stack overflow in WebStandardStreamableHTTPServerTransport.close() with re-entrant guard".

@github-actions github-actions Bot force-pushed the changeset-release/main branch from 0ac069f to fe53cda Compare April 11, 2026 13:30
Comment thread packages/core/CHANGELOG.md
Comment thread packages/client/CHANGELOG.md
@github-actions github-actions Bot force-pushed the changeset-release/main branch from fe53cda to a1b6ce1 Compare April 13, 2026 12:37
Comment on lines +3 to 14
## 2.0.0-alpha.3

### Patch Changes

- [#1655](https://github.com/modelcontextprotocol/typescript-sdk/pull/1655) [`1eb3123`](https://github.com/modelcontextprotocol/typescript-sdk/commit/1eb31236e707c4f4ab9234d87db21ab3f34bf0bc) Thanks [@nielskaspers](https://github.com/nielskaspers)! - fix(client): append custom
Accept headers to spec-required defaults in StreamableHTTPClientTransport

Custom Accept headers provided via `requestInit.headers` are now appended to the spec-mandated Accept types instead of being overwritten. This ensures the required media types (`application/json, text/event-stream` for POST; `text/event-stream` for GET SSE) are always present
while allowing users to include additional types for proxy/gateway routing.

## 2.0.0-alpha.2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 PR #1842 (commit 1eb80c4, 'v2: add guard methods') introduced new public API type guards — at minimum isCallToolResult and isJSONRPCResponse — to @modelcontextprotocol/client and @modelcontextprotocol/server without a changeset file, so these exports are absent from all alpha.3 CHANGELOGs. Before merging, manually add CHANGELOG entries for the new guard functions in packages/client/CHANGELOG.md and packages/server/CHANGELOG.md under 2.0.0-alpha.3.

Extended reasoning...

Guard methods from PR #1842 absent from alpha.3 CHANGELOGs

What the bug is and how it manifests

PR #1842 (commit 1eb80c4, 'v2: add guard methods') added new type guard functions to packages/core/src/types/guards.ts and exported them via packages/core/src/exports/public/index.ts. Because both packages/client/src/index.ts and packages/server/src/index.ts do export * from '@modelcontextprotocol/core/public', these guards became part of the published public API of both @modelcontextprotocol/client@2.0.0-alpha.3 and @modelcontextprotocol/server@2.0.0-alpha.3. However, PR #1842 did not create a changeset file, so the Changesets action generated no CHANGELOG entries for these additions.

The specific code path

The guards flow: packages/core/src/types/guards.ts → re-exported from packages/core/src/exports/public/index.ts (lines 107–115) → consumed via export * from '@modelcontextprotocol/core/public' in both packages/client/src/index.ts and packages/server/src/index.ts. Verifiers confirmed that at minimum isCallToolResult and isJSONRPCResponse are newly introduced in this commit; most other JSONRPC guards in the list may have pre-existed. No .changeset/ file appears in the commit, confirmed by git show 1eb80c4 -- .changeset/ returning empty.

Why existing code does not prevent it

There is no CI check that validates CHANGELOG completeness against the set of merged commits. The Changesets action only generates entries from changeset files it finds; it cannot detect commits that should have had a changeset but did not. This is the same root cause as the CF Workers CHANGELOG omission (PR #1843).

Impact

Users upgrading from alpha.2 to alpha.3 who read the release notes have no way to discover these new type guard exports. Alpha users actively track release notes precisely to find new APIs, so undocumented public additions reduce the practical value of the alpha program. The omission is a documentation gap with no functional impact.

Step-by-step proof

  1. git show 1eb80c4 -- .changeset/ returns nothing — PR v2: add guard methods #1842 created no changeset file.
  2. Open packages/client/CHANGELOG.md lines 3–14: the only alpha.3 entry is for PR fix(client): preserve custom Accept headers in StreamableHTTPClientTransport #1655 (Accept headers). No mention of any guard function.
  3. Open packages/server/CHANGELOG.md lines 3–11: the only alpha.3 entry is for PR fix: prevent stack overflow in transport close with re-entrancy guard #1788 (stack overflow fix). Same omission.
  4. Open packages/core/src/exports/public/index.ts lines 107–115: isCallToolResult, isJSONRPCResponse, and related guards are exported.
  5. A user reading the alpha.3 release notes finds zero indication that type guards were added to either published package.

How to fix

Manually add a patch-change entry to both packages/client/CHANGELOG.md and packages/server/CHANGELOG.md under 2.0.0-alpha.3, crediting PR #1842. The CHANGELOG files are plain text and can be amended before this PR is merged, as was done for other missed changeset entries.

@github-actions github-actions Bot force-pushed the changeset-release/main branch 3 times, most recently from 0cefcf3 to 2bc21b7 Compare April 13, 2026 14:18
Comment thread packages/server/package.json
Comment thread packages/server/package.json
Comment thread packages/client/package.json
@github-actions github-actions Bot force-pushed the changeset-release/main branch from 2bc21b7 to 324b11e Compare April 14, 2026 10:18
Comment thread packages/client/package.json
Comment on lines +3 to +8
## 2.0.0-alpha.2

### Patch Changes

- [#1768](https://github.com/modelcontextprotocol/typescript-sdk/pull/1768) [`866c08d`](https://github.com/modelcontextprotocol/typescript-sdk/commit/866c08d3640c5213f80c3b4220e24c42acfc2db8) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Allow additional JSON
Schema properties in elicitInput's requestedSchema type by adding .catchall(z.unknown()), matching the pattern used by inputSchema. This fixes type incompatibility when using Zod v4's .toJSONSchema() output which includes extra properties like $schema and additionalProperties.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The zod-json-schema-compat changeset declares only '@modelcontextprotocol/core': patch, but core is private: true and never published — so the elicitInput requestedSchema fix lands in core's CHANGELOG (invisible on npm) and is absent from the @modelcontextprotocol/client and @modelcontextprotocol/server alpha.3 CHANGELOGs even though that's where the fix actually ships (via tsdown noExternal inlining). Also note that because core is only a devDependency of client/server, changesets won't propagate a core-only bump — had this been the only changeset since alpha.2, client/server would not have been republished at all. Consider manually adding the #1768 entry to the client/server alpha.3 CHANGELOGs, and going forward target core-bundled fixes at the published packages.

Extended reasoning...

What the bug is

.changeset/zod-json-schema-compat.md declares only '@modelcontextprotocol/core': patch. The Changesets action consequently writes the PR #1768 entry ("Allow additional JSON Schema properties in elicitInput's requestedSchema...") into packages/core/CHANGELOG.md under 2.0.0-alpha.2 and nowhere else. But packages/core/package.json has "private": true — core is never published to npm, so its CHANGELOG is invisible to package consumers. The fix actually reaches users inside @modelcontextprotocol/client@2.0.0-alpha.3 and @modelcontextprotocol/server@2.0.0-alpha.3, because both packages set noExternal: ['@modelcontextprotocol/core'] in tsdown.config.ts and inline core's source into their bundles. Neither published package's CHANGELOG mentions the elicitInput fix.

Why changesets doesn't propagate this

There is also a structural issue beyond the documentation gap: @modelcontextprotocol/core is listed only in devDependencies of both packages/client/package.json and packages/server/package.json (not dependencies or peerDependencies). Changesets' updateInternalDependencies only follows runtime/peer dependency edges, so a core-only changeset does not cause client or server to be version-bumped. In this PR, client and server happen to be bumped to alpha.3 by unrelated changesets (fix-validate-client-metadata-url, odd-forks-enjoy, fix-streamable-close-reentrant), so the elicitInput fix ships by coincidence. If zod-json-schema-compat had been the only changeset since alpha.2, client/server would have stayed at alpha.2 and the fix would not have been published at all.

Relationship to prior comments

This is distinct from the previously-filed comments about PR #1842 and PR #1843 (which lacked changeset files entirely). Here a changeset does exist but targets an unpublished package. It is the same class of outcome (missing entry in published CHANGELOGs) with a different root cause (mis-targeted vs. missing changeset).

This pattern is not new — the alpha.1 core CHANGELOG already contains several core-only entries (#1735, #1790, #1766, #1762) that similarly never appeared in client/server CHANGELOGs, so maintainers may consider this established practice. That precedent is why this is filed as a nit rather than a blocking issue, but the devDependency-propagation observation is worth surfacing because it means core-only changesets are effectively no-ops for publishing.

Step-by-step proof

  1. .changeset/zod-json-schema-compat.md front-matter: only '@modelcontextprotocol/core': patch.
  2. packages/core/package.json line 3: "private": true — never published.
  3. packages/client/package.json and packages/server/package.json: @modelcontextprotocol/core appears only under devDependencies → changesets does not treat it as an internal dependency to bump.
  4. packages/client/CHANGELOG.md lines 3–19 (alpha.3 section): only fix: validate clientMetadataUrl at construction time (fail-fast) #1653 and fix(client): preserve custom Accept headers in StreamableHTTPClientTransport #1655 listed; no mention of elicitInput / requestedSchema / fix(core): allow additional JSON Schema properties in elicitInput requestedSchema #1768.
  5. packages/server/CHANGELOG.md lines 3–8 (alpha.3 section): only fix: prevent stack overflow in transport close with re-entrancy guard #1788 listed; no mention of elicitInput / fix(core): allow additional JSON Schema properties in elicitInput requestedSchema #1768.
  6. A user reading the npm release notes for client@2.0.0-alpha.3 or server@2.0.0-alpha.3 has no way to discover the elicitInput.requestedSchema Zod-v4 compatibility fix that those packages now contain.

Impact and fix

Impact is documentation-only in this release (the fix does ship), but the changeset is structurally mis-targeted. Short-term: manually add the #1768 entry under ### Patch Changes in both packages/client/CHANGELOG.md and packages/server/CHANGELOG.md for 2.0.0-alpha.3. Longer-term: when a fix lives in core but ships via the inlined client/server bundles, the changeset should declare '@modelcontextprotocol/client': patch and '@modelcontextprotocol/server': patch (optionally in addition to core) so the bump and CHANGELOG entry land where users can see them.

@github-actions github-actions Bot force-pushed the changeset-release/main branch from 324b11e to 91dceb0 Compare April 14, 2026 10:35
{
"name": "@modelcontextprotocol/client",
"version": "2.0.0-alpha.2",
"version": "2.0.0-alpha.3",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 PR #1875 (commit 9ed62fe, shipping in this alpha.3 release) added a claims option to PrivateKeyJwtProvider whose JSDoc at packages/client/src/client/authExtensions.ts:239-240 promises "custom claims taking precedence for any overlapping keys" — but the implementation at lines 82–89 chains .setIssuer/.setSubject/.setAudience/.setIssuedAt/.setExpirationTime/.setJti after constructing SignJWT(claims), and each jose setter unconditionally overwrites the corresponding payload field. So a user passing e.g. claims: { aud: 'https://custom-audience' } per the docs silently gets aud reset to the SDK-computed value. Either drop the redundant setter chain so the merged claims object is authoritative, or fix the JSDoc to say standard claims cannot be overridden.

Extended reasoning...

What the bug is

PR #1875 (commit 9ed62fe — direct parent of this version-bump commit, so it ships in @modelcontextprotocol/client@2.0.0-alpha.3) added a claims option to PrivateKeyJwtProviderOptions. The JSDoc at packages/client/src/client/authExtensions.ts:238-240 reads:

Optional custom claims to include in the JWT assertion. These are merged with the standard claims (iss, sub, aud, exp, iat, jti), with custom claims taking precedence for any overlapping keys.

The implementation does spread { ...baseClaims, ...options.claims } at line 60, so the merged claims object initially has the user's values winning. But lines 82–89 then build the JWT with:

new jose.SignJWT(claims)
    .setProtectedHeader({ alg, typ: 'JWT' })
    .setIssuer(options.issuer)
    .setSubject(options.subject)
    .setAudience(audience)
    .setIssuedAt(now)
    .setExpirationTime(now + lifetimeSeconds)
    .setJti(jti)
    .sign(...)

Each of those setters writes back into the payload after the constructor copies claims, so the SDK-computed values overwrite whatever the user passed for all six listed standard claims. The actual behavior is the exact opposite of the JSDoc promise.

Why existing code doesn't prevent it

Verified against jose@6.2.2 source (node_modules/.pnpm/jose@6.2.2/.../jwt/sign.js and lib/jwt_claims_set.js): the SignJWT constructor passes the payload to JWTClaimsBuilder which does this.#payload = structuredClone(payload), and each setter (set iss, set sub, set aud, set jti, set exp, set iat) does an unconditional this.#payload.<claim> = value assignment — no "only if absent" guard. So .setIssuer(options.issuer) always replaces claims.iss, etc.

The tests added in 9ed62fe only exercise non-standard claims (tenant_id, role), which do flow through correctly because no setter touches them. There is no test that passes a standard claim via options.claims and asserts it appears in the signed JWT, so the contradiction is not caught.

Step-by-step proof

  1. User constructs the provider with claims: { aud: 'https://custom-token-endpoint', jti: 'fixed-id' }, relying on the JSDoc precedence guarantee.
  2. Line 60 produces claims = { iss, sub, aud: 'https://custom-token-endpoint', exp, iat, jti: 'fixed-id' } — user values win in the merged object.
  3. Line 82 passes that object to new jose.SignJWT(claims); jose clones it into #payload.
  4. Line 86 .setAudience(audience) runs this.#payload.aud = audience (the SDK-computed metadata?.issuer ?? url), overwriting 'https://custom-token-endpoint'.
  5. Line 89 .setJti(jti) runs this.#payload.jti = jti (the SDK-generated random string), overwriting 'fixed-id'.
  6. The signed assertion contains the SDK values for aud and jti; the user's values are silently discarded with no warning. Same applies to iss, sub, iat, exp.

Impact

This is a docs-vs-implementation contradiction in a public API surface that ships to npm in this release. A user who follows the JSDoc to override aud (a real use case — some authorization servers require a token-endpoint-specific audience that differs from the issuer URL) will silently get the wrong audience in the assertion and a 401 from the AS, with nothing pointing them at the cause. Per REVIEW.md → Documentation & Changesets, prose that promises behavior the code doesn't ship misleads consumers and should be flagged at the publish gate.

The spread at line 60 is effectively dead code for the six standard claims — it only matters for additional, non-standard claims.

How to fix

Two options, pick one:

  • Honor the JSDoc (recommended, since the spread at line 60 already shows this was the intent): drop the redundant .setIssuer/.setSubject/.setAudience/.setIssuedAt/.setExpirationTime/.setJti chain at lines 84–89, leaving just new jose.SignJWT(claims).setProtectedHeader({ alg, typ: 'JWT' }).sign(key). The merged claims object already contains all six standard claims with the correct precedence.
  • Honor the implementation: change the JSDoc at lines 239–240 to state that the standard claims (iss, sub, aud, exp, iat, jti) cannot be overridden via claims and that only additional claims are merged. In that case the baseClaims spread at line 60 can also be dropped since it's a no-op for those keys.

Comment on lines +3 to +10
## 2.0.0-alpha.3

### Minor Changes

- [#1653](https://github.com/modelcontextprotocol/typescript-sdk/pull/1653) [`6bec24a`](https://github.com/modelcontextprotocol/typescript-sdk/commit/6bec24a14cb7e3dbe9f5e04aeb893cd0d6e8cb83) Thanks [@rechedev9](https://github.com/rechedev9)! - Add `validateClientMetadataUrl()`
utility for early validation of `clientMetadataUrl`

Exports a `validateClientMetadataUrl()` function that `OAuthClientProvider` implementations can call in their constructors to fail fast on invalid URL-based client IDs, instead of discovering the error deep in the auth flow.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 PR #1875 (commit 9ed62fe) added a public claims?: Record<string, unknown> option to PrivateKeyJwtProviderOptions but landed without a changeset, so this user-facing feature is absent from the @modelcontextprotocol/client alpha.3 CHANGELOG even though it ships in this release. Either land a changeset on main and let the bot regenerate this PR, or manually add a Minor-change entry crediting #1875 under 2.0.0-alpha.3 before merging.

Extended reasoning...

PR #1875 custom-claims feature absent from alpha.3 CHANGELOG

What the bug is and how it manifests

PR #1875 (commit 9ed62fe, merged 2026-04-14 — after the most recent bot review at 10:29 UTC) added a new public claims?: Record<string, unknown> option to PrivateKeyJwtProviderOptions, letting callers inject custom JWT claims into the private_key_jwt client-assertion flow. PrivateKeyJwtProvider and PrivateKeyJwtProviderOptions are both re-exported from packages/client/src/index.ts (lines ~45–53), so this is a user-facing public-API addition. However, the commit touched only packages/client/src/client/authExtensions.ts and its test file — it created no .changeset/*.md file. As a result, the Changesets action generated no CHANGELOG entry for it.

The specific code path

The Version Packages commit 91dceb0 (this PR's HEAD) sits directly on top of 9ed62fe in git log, so the claims option WILL be published as part of @modelcontextprotocol/client@2.0.0-alpha.3. Yet .changeset/pre.json in this PR adds only four new slugs (fix-streamable-close-reentrant, fix-validate-client-metadata-url, odd-forks-enjoy, zod-json-schema-compat) — none corresponding to #1875 — and packages/client/CHANGELOG.md lines 3–19 list only #1653 and #1655 under the alpha.3 section.

Why existing checks don't catch it

There is no CI gate that diffs the set of merged commits against the set of changeset slugs. Changesets faithfully renders whatever .changeset/*.md files exist; commits that should have included one but did not pass through silently. This is the same root cause already flagged on this PR for #1842 (guard methods) and #1843 (CF Workers validator), but those comments do not cover #1875, which landed afterward.

Impact

Documentation-only: the feature ships and works, but alpha users reading the npm release notes for client@2.0.0-alpha.3 have no way to discover that PrivateKeyJwtProvider now accepts custom claims. Alpha users actively track release notes to find new APIs, so undocumented additions reduce the value of the alpha program.

Step-by-step proof

  1. git show 9ed62fe --stat → only packages/client/src/client/authExtensions.ts and packages/client/src/client/authExtensions.test.ts changed.
  2. git show 9ed62fe -- .changeset/ → empty; no changeset file added.
  3. git log --oneline -2 91dceb091dceb0 Version Packages (alpha) followed by 9ed62fe feat(client): support custom claims in PrivateKeyJwtProvider (#1875)9ed62fe is an ancestor of this release.
  4. grep -r "claims\|1875\|PrivateKeyJwt" .changeset/ → no matches.
  5. packages/client/CHANGELOG.md lines 3–19 (alpha.3 section): only fix: validate clientMetadataUrl at construction time (fail-fast) #1653 and fix(client): preserve custom Accept headers in StreamableHTTPClientTransport #1655 listed; no mention of claims / PrivateKeyJwtProvider / feat(client): support custom claims in PrivateKeyJwtProvider #1875.

How to fix

Preferred: land a minor changeset on main targeting @modelcontextprotocol/client and let the Changesets bot regenerate this PR. Alternatively, hand-edit packages/client/CHANGELOG.md to add under ### Minor Changes for 2.0.0-alpha.3:

- [#1875] Support custom claims in `PrivateKeyJwtProvider` via a new `claims?: Record<string, unknown>` option on `PrivateKeyJwtProviderOptions`.

{
"name": "@modelcontextprotocol/client",
"version": "2.0.0-alpha.2",
"version": "2.0.0-alpha.3",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟣 🟣 Pre-existing — and a correction to the five prior runtime-dependency comments (3035194589 / 3073682357 / 3073682371 / 3073783213 / 3078795515): I built dist and verified that ajv, ajv-formats, and @cfworker/json-schema are fully inlined into the .mjs bundles — the only external import in dist/src-*.mjs is zod/v4, and shimsNode/Browser/Workerd.mjs all import only from the local chunk, so there is no runtime ERR_MODULE_NOT_FOUND and the JSDoc "Bundled" claim is correct. The actual residual issue is type-only: packages/{client,server}/dist/index-*.d.mts lines 2–3 contain import { Ajv } from 'ajv' and import { JSONSchema } from 'json-schema-typed' (referenced by AjvJsonSchemaValidator(ajv?: Ajv) and type JsonSchemaType), and neither package is in dependencies/peerDependencies, so consumers with skipLibCheck: false get TS2307 — a config #1766 explicitly intends to support. Fix by adding ajv + json-schema-typed to both packages' dependencies (zero runtime cost; JS already bundled), or stop exposing the raw Ajv type / inline JSONSchema.Interface.

Extended reasoning...

Correction to prior comments + actual residual issue

Prior comments are factually wrong about runtime. Five earlier comments on this PR (3035194589, 3073682357, 3073682371, 3073783213, 3078795515) assert that dist/shimsNode.mjs / shimsWorkerd.mjs / shimsBrowser.mjs contain bare external import statements for ajv, ajv-formats, and @cfworker/json-schema, causing ERR_MODULE_NOT_FOUND at module load. I verified by running pnpm --filter @modelcontextprotocol/{client,server} build and inspecting the output, and that claim does not hold:

  • packages/client/dist/src-5RQ50XKB.mjs (387 KB shared chunk) has exactly one external import on line 1: import * as z from "zod/v4". ajv (~122 inline string occurrences), ajv-formats, and @cfworker/json-schema (the CfWorkerJsonSchemaValidator class definition lives at ~line 10343) are all inlined. The two require("ajv/...") strings inside the chunk are part of ajv's own codegen template literals, not real import/require statements.
  • shimsNode.mjs, shimsBrowser.mjs, shimsWorkerd.mjs, and validators/cfWorker.mjs all import only from ./src-5RQ50XKB.mjsgrep -E 'from "ajv|from "@cfworker|from "ajv-formats' dist/**/*.mjs returns zero matches.
  • Same for packages/server/dist/src-*.mjs.

Why the earlier reasoning was wrong: those comments assumed tsdown/rolldown externalises every bare import not listed in noExternal. It doesn't — it auto-externalises only packages listed in the building package's own dependencies/peerDependencies. Since ajv/ajv-formats/@cfworker/json-schema are absent from client's and server's package.json (they live only in core's deps, and core is a devDependency), tsdown bundles them regardless of noExternal. So the JSDoc "Bundled — no additional dependencies required" at packages/core/src/index.ts:31,35 is correct for runtime, and the recommended "extend noExternal" fix in 3078795515 is a no-op for the .mjs output.

The actual residual issue (type-only)

What the dist inspection did surface is that the bundled type declarations still reference external packages. packages/client/dist/index-KFyi7eTI.d.mts and packages/server/dist/index-BIf1K4vK.d.mts both contain on lines 2–3:

import { Ajv } from 'ajv';
import { JSONSchema } from 'json-schema-typed';

These are used at type JsonSchemaType = JSONSchema.Interface (~line 9799) and declare class AjvJsonSchemaValidator { constructor(ajv?: Ajv); } (~line 9883). The dts bundler (tsdown's dts.resolver: 'tsc' with paths mapping only @modelcontextprotocol/core) inlines core's declarations but leaves third-party type references external. None of the five prior comments mention json-schema-typed at all, and none distinguish .mjs from .d.mts behaviour.

Neither ajv nor json-schema-typed is in dependencies or peerDependencies of packages/client/package.json or packages/server/package.json. A consumer with "skipLibCheck": false therefore gets TS2307: Cannot find module 'ajv' / 'json-schema-typed' on any import from @modelcontextprotocol/client or /server — not just when using the validators. Per the alpha.1 CHANGELOG entry for #1766, the SDK explicitly intends to support skipLibCheck: false, so this is a supported-config break.

Step-by-step proof

  1. npm install @modelcontextprotocol/client@2.0.0-alpha.3 in a fresh project → node_modules contains zod, cross-spawn, eventsource, etc., but not ajv or json-schema-typed.
  2. node -e "import('@modelcontextprotocol/client').then(m => console.log(typeof m.AjvJsonSchemaValidator))" → prints function. Runtime works — the prior comments' ERR_MODULE_NOT_FOUND does not reproduce.
  3. tsconfig.json with "skipLibCheck": false; source file: import { McpClient } from '@modelcontextprotocol/client';.
  4. tsc --noEmitnode_modules/@modelcontextprotocol/client/dist/index-KFyi7eTI.d.mts(2,21): error TS2307: Cannot find module 'ajv' or its corresponding type declarations. and the same for json-schema-typed on line 3.
  5. Type-check fails for every consumer file that imports anything from the package.

Impact and fix

Runtime is fine; this is a compile-time-only break for the skipLibCheck: false population (which #1766 explicitly targets). Pre-existing since alpha.1 (#1689 introduced AjvJsonSchemaValidator and JsonSchemaType); this PR doesn't change it but is the publish gate for alpha.3.

Two fix options:

  • Add ajv and json-schema-typed to dependencies of both packages/client/package.json and packages/server/package.json. This satisfies TypeScript's module resolution for the .d.mts imports at zero additional runtime cost (the JS is already bundled; the packages are pulled in only so tsc can find their .d.ts).
  • Stop leaking the third-party types: change constructor(ajv?: Ajv) to constructor(ajv?: unknown) (or a locally-declared structural type) and inline/re-declare JSONSchema.Interface instead of re-exporting it. This keeps the dependency list minimal but is a small public-API surface change.

Filed primarily so the maintainers don't act on the incorrect runtime-crash framing / noExternal recommendation above before publish — extending noExternal would not address the .d.mts emission, and re-adding peer deps is unnecessary for runtime.

@github-actions github-actions Bot force-pushed the changeset-release/main branch from 91dceb0 to 0f9910f Compare April 16, 2026 11:44
Comment on lines +3 to +10
## 2.0.0-alpha.3

### Minor Changes

- [#1653](https://github.com/modelcontextprotocol/typescript-sdk/pull/1653) [`6bec24a`](https://github.com/modelcontextprotocol/typescript-sdk/commit/6bec24a14cb7e3dbe9f5e04aeb893cd0d6e8cb83) Thanks [@rechedev9](https://github.com/rechedev9)! - Add `validateClientMetadataUrl()`
utility for early validation of `clientMetadataUrl`

Exports a `validateClientMetadataUrl()` function that `OAuthClientProvider` implementations can call in their constructors to fail fast on invalid URL-based client IDs, instead of discovering the error deep in the auth flow.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 PR #1843 didn't just add the ./validators/cf-worker sub-path (as comment 3068107615 frames it) — it also removed CfWorkerJsonSchemaValidator from the package-root export of both @modelcontextprotocol/client and @modelcontextprotocol/server, so import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server' compiles in alpha.2 and fails in alpha.3. When adding the #1843 CHANGELOG entry, please word it as a breaking change (e.g. "Breaking: CfWorkerJsonSchemaValidator is no longer exported from the package root — import from @modelcontextprotocol/{client,server}/validators/cf-worker instead"), not just "added new sub-path".

Extended reasoning...

What the bug is

Commit 7ba58da (PR #1843, shipping in this alpha.3 release) deleted the line export { CfWorkerJsonSchemaValidator } from '../../validators/cfWorkerProvider.js' from packages/core/src/exports/public/index.ts (it was line 140 at the alpha.2 publish commit). Because both packages/client/src/index.ts:81 and packages/server/src/index.ts:50 do export * from '@modelcontextprotocol/core/public', this removed a value export from the main entry of both published packages. The class is now reachable only via the new ./validators/cf-worker sub-path.

Neither alpha.3 CHANGELOG section mentions this. packages/client/CHANGELOG.md lists only #1653/#1655 and packages/server/CHANGELOG.md lists only #1788.

Why this is distinct from the existing #1843 comment

Inline comment 3068107615 already asks for a CHANGELOG entry for #1843, but frames it as a purely additive feature ("new ./validators/cf-worker sub-path export"). That framing is incomplete: CfWorkerJsonSchemaValidator already existed at the package root in alpha.2, and #1843 moved it. A maintainer following 3068107615 verbatim would write "Added ./validators/cf-worker sub-path" and still leave alpha.2 users with no explanation for why their existing root import stopped compiling. Per REVIEW.md → Correctness: "removed exports — flagged and justified".

Step-by-step proof

  1. git show 0021561:packages/core/src/exports/public/index.ts | grep -n CfWorker → line 140: export { CfWorkerJsonSchemaValidator } from '../../validators/cfWorkerProvider.js' (alpha.2 publish commit).
  2. git show 7ba58da -- packages/core/src/exports/public/index.ts → that line is deleted; only the type export CfWorkerSchemaDraft remains.
  3. packages/client/src/index.ts:81 and packages/server/src/index.ts:50 at HEAD still wildcard-re-export @modelcontextprotocol/core/public and add no compensating direct re-export of the class.
  4. Therefore alpha.2 user code import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server' (or /client) fails in alpha.3 with "has no exported member 'CfWorkerJsonSchemaValidator'".
  5. packages/client/CHANGELOG.md lines 3–19 and packages/server/CHANGELOG.md lines 3–8 (the alpha.3 sections being added in this PR) say nothing about it.

Impact

Any alpha.2 consumer who imported CfWorkerJsonSchemaValidator from the package root (the only path that existed in alpha.2) gets a compile error on upgrade with nothing in the npm release notes pointing at the cause. docs/migration.md was updated in 7ba58da to point at the sub-path, but that guide covers v1→v2, not alpha.2→alpha.3; alpha users follow the per-package CHANGELOGs.

This is filed as a nit because (a) breaking changes between alpha pre-releases are expected, (b) the actionable fix is the same one-line CHANGELOG addition already requested in 3068107615 — just with the correct "Breaking: moved" framing rather than "Added", and (c) docs/migration.md does cover the new location. The novel value is ensuring the entry is worded so alpha.2 users can find it when their import breaks.

How to fix

When adding the #1843 entry to both packages/client/CHANGELOG.md and packages/server/CHANGELOG.md under 2.0.0-alpha.3, word it as:

Breaking: CfWorkerJsonSchemaValidator is no longer exported from the package root. Import it from @modelcontextprotocol/{client,server}/validators/cf-worker instead.

rather than just "Added ./validators/cf-worker sub-path export".

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for adding the cfworker-out-of-barrel entry — it does now tell users where CfWorkerJsonSchemaValidator lives. However, the entry ends with "No public API change", which contradicts the verified history: at the alpha.2 publish commit (0021561), CfWorkerJsonSchemaValidator was a value export at packages/core/src/exports/public/index.ts:140, and #1843 deleted that line — so import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server' (or /client) compiles in alpha.2 and fails in alpha.3. That's a removed public root export.

Suggest dropping the "No public API change" sentence (and the "core internal barrel" wording — #1843 removed it from the public barrel) and replacing it with the migration hint, e.g.:

…so consumers that don't opt into it no longer ship that code. Breaking (alpha): CfWorkerJsonSchemaValidator is no longer exported from the package root — import it from @modelcontextprotocol/{client,server}/validators/cf-worker.

Same applies to the identical entry in packages/server/CHANGELOG.md.

@github-actions github-actions Bot force-pushed the changeset-release/main branch from 0f9910f to f632488 Compare April 20, 2026 11:57
Comment on lines +7 to +8
- [#1930](https://github.com/modelcontextprotocol/typescript-sdk/pull/1930) [`bdfd7f0`](https://github.com/modelcontextprotocol/typescript-sdk/commit/bdfd7f0154e2afd4fd6d2e95e6aadd924c3775f2) Thanks [@Christian-Sidak](https://github.com/Christian-Sidak)! - Fix `requestStream` to
call `tasks/result` for failed tasks instead of yielding a hardcoded `ProtocolError`. When a task reaches the `failed` terminal status, the stream now retrieves and yields the actual stored result (matching the behavior for `completed` tasks), as required by the spec.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Same structural issue as comment 3078795529 (which covers #1768), but for the newly-landed fix-failed-task-result-retrieval changeset (#1930, commit bdfd7f0, merged 2026-04-20 — after that comment was filed): it declares only '@modelcontextprotocol/core': patch, so the requestStream failed-task fix appears only in the private core CHANGELOG and is absent from the client/server alpha.3 CHANGELOGs where users will actually look. When acting on 3078795529, please also add the #1930 entry under ### Patch Changes in both packages/client/CHANGELOG.md and packages/server/CHANGELOG.md for 2.0.0-alpha.3.

Extended reasoning...

What the bug is

.changeset/fix-failed-task-result-retrieval.md (PR #1930, commit bdfd7f0) declares only '@modelcontextprotocol/core': patch in its front-matter. The Changesets action consequently writes the #1930 entry ("Fix requestStream to call tasks/result for failed tasks…") into packages/core/CHANGELOG.md lines 7–8 and nowhere else. But packages/core/package.json has "private": true — core is never published to npm, so its CHANGELOG is invisible to package consumers. The fix actually reaches users inside @modelcontextprotocol/client@2.0.0-alpha.3 and @modelcontextprotocol/server@2.0.0-alpha.3, because both packages set noExternal: ['@modelcontextprotocol/core'] in tsdown.config.ts and inline core's requestStream/TaskManager source into their bundles. Neither published package's alpha.3 CHANGELOG section mentions #1930.

Relationship to existing comment 3078795529

This is not a duplicate of comment 3078795529. That comment specifically addresses the zod-json-schema-compat changeset (#1768) and was posted on 2026-04-14; #1930 landed on 2026-04-20, six days later. A maintainer following 3078795529's actionable recommendation verbatim ("manually add the #1768 entry under Patch Changes in both client/server CHANGELOGs") would fix only #1768 and miss #1930. The "going forward, target core-bundled fixes at the published packages" advice in that comment is generic process guidance, not a specific catch of #1930. The established pattern on this PR is one comment per missing/mis-targeted changeset (#1842, #1843, #1875, #1768 each got separate flags), so a separate flag for #1930 is consistent.

Why changesets doesn't propagate this

As noted in 3078795529, @modelcontextprotocol/core is listed only in devDependencies of both packages/client/package.json and packages/server/package.json. Changesets' updateInternalDependencies only follows runtime/peer dependency edges, so a core-only changeset does not cause client or server to be version-bumped. In this PR, client and server happen to be bumped to alpha.3 by unrelated changesets (#1653, #1655, #1788), so the #1930 fix ships by coincidence. Had #1930 been the only changeset since alpha.2, client/server would have stayed at alpha.2 and the fix would not have been published at all.

Step-by-step proof

  1. .changeset/fix-failed-task-result-retrieval.md front-matter: only '@modelcontextprotocol/core': patch.
  2. git log --oneline shows bdfd7f0 (fix: retrieve stored result from tasks/result for failed tasks #1930) landed 2026-04-20, after comment 3078795529 was posted 2026-04-14 — so that comment could not have named fix: retrieve stored result from tasks/result for failed tasks #1930.
  3. packages/core/CHANGELOG.md lines 7–8: the fix: retrieve stored result from tasks/result for failed tasks #1930 entry appears here, under a package with "private": true.
  4. packages/client/CHANGELOG.md lines 3–19 (alpha.3 section): only fix: validate clientMetadataUrl at construction time (fail-fast) #1653 and fix(client): preserve custom Accept headers in StreamableHTTPClientTransport #1655 listed; no mention of requestStream / tasks/result / fix: retrieve stored result from tasks/result for failed tasks #1930.
  5. packages/server/CHANGELOG.md lines 3–8 (alpha.3 section): only fix: prevent stack overflow in transport close with re-entrancy guard #1788 listed; same omission.
  6. A user reading the npm release notes for client@2.0.0-alpha.3 or server@2.0.0-alpha.3 has no way to discover that failed-task polling behavior changed (failed tasks now yield the stored result instead of a hardcoded ProtocolError) — even though that's a user-visible behavior change in both packages.

Impact and fix

Impact is documentation-only in this release — the fix does ship in alpha.3 because client/server are bumped by other changesets. This is filed as a nit because (a) the structural anti-pattern is already flagged three lines below in the same CHANGELOG section by 3078795529, (b) the alpha.1 core CHANGELOG already contains several core-only entries (#1735, #1790, #1766, #1762) that never appeared in client/server CHANGELOGs, so maintainers may consider this established practice, and (c) it's the same one-line CHANGELOG-addition fix.

Short-term: when acting on 3078795529, also manually add the #1930 entry under ### Patch Changes in both packages/client/CHANGELOG.md and packages/server/CHANGELOG.md for 2.0.0-alpha.3. Longer-term: this confirms the core-only-changeset pattern is recurring (now #1768 and #1930 in this single release cycle) and may warrant a CONTRIBUTING note or a CI check that flags changesets targeting only the private core package.

@github-actions github-actions Bot force-pushed the changeset-release/main branch 2 times, most recently from 6274e0a to 56f4c20 Compare April 27, 2026 12:25
Comment on lines +14 to +15
- [#1834](https://github.com/modelcontextprotocol/typescript-sdk/pull/1834) [`42cb6b2`](https://github.com/modelcontextprotocol/typescript-sdk/commit/42cb6b2b728347d8b58a0d1940b7e63366a29ab9) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Export
`InMemoryTransport` for in-process testing.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 PR #1834 (shipping in this release) updated docs/migration.md to define the v2 import path for InMemoryTransport (@modelcontextprotocol/{client,server}), but did not add the corresponding row to docs/migration-SKILL.md — that file has zero matches for InMemoryTransport/inMemory. CLAUDE.md §Breaking Changes and the REVIEW.md checklist both require breaking changes to be documented in both migration files, so the two are now out of sync. One-row fix: add @modelcontextprotocol/sdk/inMemory.js@modelcontextprotocol/client or @modelcontextprotocol/server to the "Types / shared imports" table.

Extended reasoning...

What the gap is

PR #1834 (commit 42cb6b2, published in this alpha.3 release per the CHANGELOG entries being added here) changed docs/migration.md from "InMemoryTransport — removed from public API" to "InMemoryTransport — moved; now exported from @modelcontextprotocol/client and @modelcontextprotocol/server". That established a clean, mechanical v1→v2 import mapping: @modelcontextprotocol/sdk/inMemory.js@modelcontextprotocol/{client,server}.

However, docs/migration-SKILL.md was not touched in that commit (git show 42cb6b2 --stat confirms only 4 files changed, none of them migration-SKILL.md), and grepping that file for InMemoryTransport or inMemory returns zero matches. The §3 "Import Mapping" tables — which already map e.g. @modelcontextprotocol/sdk/shared/transport.js → "@modelcontextprotocol/client or @modelcontextprotocol/server" — have no row for @modelcontextprotocol/sdk/inMemory.js.

Why this violates a stated repo convention

CLAUDE.md (lines ~27–30) explicitly requires: "When making breaking changes, document them in both: docs/migration.mddocs/migration-SKILL.md". REVIEW.md's Tests & docs checklist repeats: "Breaking changes documented in docs/migration.md and docs/migration-SKILL.md". The v1→v2 inMemory.js import-path change is exactly the kind of mechanical mapping the SKILL file's import tables are designed to encode, and #1834 updated one file but not the other.

Before #1834 there was arguably no clean SKILL row to write — migration.md said "removed from public API", so there was no v2 target to map to. #1834 is precisely the commit that created the canonical v2 target, so it's the commit that should have added the SKILL row. The omission therefore originates with #1834 rather than being strictly pre-existing.

Impact

An LLM-driven migration that follows docs/migration-SKILL.md verbatim (the file's stated purpose) would have no rule for import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js' and would leave it unresolved or guess. The human-readable docs/migration.md is correct, so users following that guide are unaffected — the impact is limited to automated/SKILL-driven migrations and to keeping the two docs in sync per the repo's own policy.

Step-by-step proof

  1. git show 42cb6b2 --stat → 4 files touched; docs/migration-SKILL.md is not among them.
  2. grep -n 'InMemoryTransport\|inMemory' docs/migration-SKILL.md → zero matches.
  3. docs/migration-SKILL.md §3 "Import Mapping" → "Types / shared imports" table (lines ~62–69) maps @modelcontextprotocol/sdk/shared/transport.js → "@modelcontextprotocol/client or @modelcontextprotocol/server" — the exact pattern the inMemory.js row would follow, but no such row exists.
  4. docs/migration.md (post-42cb6b2) → InMemoryTransport section says "moved — now exported from @modelcontextprotocol/client and @modelcontextprotocol/server".
  5. CLAUDE.md:27–30 → requires both files updated for breaking changes.
  6. Result: the two migration docs are out of sync for a change shipping in this release.

Relationship to existing PR comments

None of the existing timeline comments mention migration-SKILL.md — this is not a duplicate. #1834 landed on 2026-04-27, after the most recent review round (2026-04-20).

Fix

Add one row to the "Types / shared imports" table in docs/migration-SKILL.md §3:

v1 import v2 import
@modelcontextprotocol/sdk/inMemory.js @modelcontextprotocol/client or @modelcontextprotocol/server

This is a docs-only, one-line addition; it can land on main alongside this publish gate.

@github-actions github-actions Bot force-pushed the changeset-release/main branch 2 times, most recently from 4fa1cf7 to 73d66f4 Compare April 28, 2026 12:41
@github-actions github-actions Bot force-pushed the changeset-release/main branch 4 times, most recently from 369ec1e to cd754dc Compare April 29, 2026 12:35
Comment on lines +7 to +9
- [#1907](https://github.com/modelcontextprotocol/typescript-sdk/pull/1907) [`7cccc2a`](https://github.com/modelcontextprotocol/typescript-sdk/commit/7cccc2aca81f4cd961d2a0ef53e879f68a01df73) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Add OAuth
Resource-Server glue to the Express adapter: `requireBearerAuth` middleware (token verification + RFC 6750 `WWW-Authenticate` challenges), `mcpAuthMetadataRouter` (serves RFC 9728 Protected Resource Metadata and mirrors RFC 8414 AS metadata at the resource origin), the
`getOAuthProtectedResourceMetadataUrl` helper, and the `OAuthTokenVerifier` interface. These restore the v1 `src/server/auth` Resource-Server pieces as first-class v2 API so MCP servers can plug into an external Authorization Server with a few lines of Express wiring.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 requireBearerAuth (#1907, shipping for the first time in this release) builds the WWW-Authenticate challenge by raw-interpolating error.message into error_description="…" at packages/middleware/express/src/auth/bearerAuth.ts:37, but RFC 6750 §3 forbids " (0x22) and \ (0x5C) in error_description. A user-supplied verifier.verifyAccessToken() that throws new OAuthError(InvalidToken, 'Token "abc" revoked') (which types.ts:17-20 explicitly tells implementers to do) produces a header where the unescaped " terminates the quoted-string early, potentially preventing strict clients from parsing the trailing resource_metadata="…" parameter. Consider stripping characters outside %x20-21 / %x23-5B / %x5D-7E from description before interpolation.

Extended reasoning...

What the bug is

PR #1907 (commit 7cccc2a, published for the first time in @modelcontextprotocol/express@2.0.0-alpha.3 per this CHANGELOG entry) adds buildWwwAuthenticateHeader at packages/middleware/express/src/auth/bearerAuth.ts:31-45. Line 37 builds the RFC 6750 challenge with raw template-literal interpolation:

let header = `Bearer error="${errorCode}", error_description="${description}"`;

description reaches this line as error.message from any OAuthError caught at line 92, and line 73 awaits the user-supplied verifier.verifyAccessToken(token). packages/middleware/express/src/auth/types.ts:17-20 explicitly documents that verifier implementations "should throw an OAuthError … with OAuthErrorCode.InvalidToken" — so user-authored error messages reaching buildWwwAuthenticateHeader is the designed flow, not an edge case. OAuthError itself (packages/core/src/auth/errors.ts:100-108) just calls super(message) with no sanitization.

RFC 6750 §3 states the error_description value "MUST NOT include characters outside the set %x20-21 / %x23-5B / %x5D-7E" — i.e. " (0x22) and \ (0x5C) are explicitly forbidden. Separately, RFC 7235 §2.1 defines auth-param values as HTTP quoted-string, so an unescaped " terminates the quoted-string early at the lexical level.

Why nothing prevents it

The four SDK-hardcoded messages (Missing Authorization header, Invalid Authorization header format…, Insufficient scope, Token has no expiration time/Token has expired) all happen to be quote-free, so the SDK's own throws never trigger this. But the catch at line 92 forwards any OAuthError from the verifier verbatim to line 94, with no escaping or character-set filter between error.message and the template literal. The same raw interpolation applies to resourceMetadataUrl at line 42, though that value is typically generated by getOAuthProtectedResourceMetadataUrl and far less likely to contain quotes.

Step-by-step proof

  1. Integrator implements verifier.verifyAccessToken to relay an upstream introspection-endpoint message: throw new OAuthError(OAuthErrorCode.InvalidToken, 'Token "abc-123" revoked by admin').
  2. Request arrives with Authorization: Bearer abc-123; line 73 calls the verifier, which throws.
  3. Line 92 catches the OAuthError; line 94 calls buildWwwAuthenticateHeader('invalid_token', 'Token "abc-123" revoked by admin', [], 'https://rs.example/.well-known/oauth-protected-resource').
  4. Line 37 produces: Bearer error="invalid_token", error_description="Token "abc-123" revoked by admin", resource_metadata="https://rs.example/.well-known/oauth-protected-resource".
  5. A strict RFC 7235 parser tokenizes error_description as the quoted-string "Token " and then encounters abc-123" revoked… as invalid token68/auth-param syntax. Depending on the parser, it either rejects the whole challenge or stops before reaching resource_metadata="…" — defeating the RFC 9728 discovery flow this CHANGELOG entry advertises ("RFC 6750 WWW-Authenticate challenges").

Impact

This is a real RFC-compliance gap in new public API, but narrow: the trigger is under the integrator's own control (they author the verifier and its error messages, and could sanitize there); most introspection-endpoint messages don't contain quotes; Express's res.set() already blocks CRLF so there's no header-injection vector; and many HTTP clients are lenient about malformed auth-params. The consequence is a degraded discovery hint, not a crash or security hole. Filed as a nit since #1907 is documented as restoring v1 behavior (carried-forward limitation rather than regression) and the fix is one line, but worth noting at the publish gate alongside the other code-level flags on this PR (e.g. comment 3078867429 on #1875).

How to fix

Filter description to the RFC 6750 §3 character set before interpolation, e.g.:

const safeDescription = description.replace(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/g, " ");
let header = `Bearer error="${errorCode}", error_description="${safeDescription}"`;

(or at minimum description.replace(/["\\]/g, "") to keep the quoted-string well-formed). Applying the same filter to resourceMetadataUrl at line 42 would harden that path too.

Comment on lines +7 to +9
- [#1897](https://github.com/modelcontextprotocol/typescript-sdk/pull/1897) [`434b2f1`](https://github.com/modelcontextprotocol/typescript-sdk/commit/434b2f11ecec452f3dca0199f68afccd8b119dd4) Thanks [@felixweinberger](https://github.com/felixweinberger)! - Stop bundling
`@cfworker/json-schema` into the main package barrel. Previously `CfWorkerJsonSchemaValidator` was re-exported from the core internal barrel, so tsdown inlined the `@cfworker/json-schema` dev dependency into every consumer's bundle even when it was never used. The validator is
now reachable only via the `_shims` conditional (workerd/browser) and the explicit `@modelcontextprotocol/{server,client}/validators/cf-worker` subpath, so consumers that don't opt into it no longer ship that code. No public API change.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The newly-landed #1897 entry ends with "No public API change.", but CfWorkerJsonSchemaValidator was a root-level export in alpha.2 (via export * from '@modelcontextprotocol/core/public') and is gone from the root in alpha.3 — so import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server' now fails to compile. The entry's own preceding sentence ("now reachable only via … the explicit … subpath") already contradicts the "No public API change" tail; suggest replacing that last sentence with a Breaking (alpha) note pointing alpha.2 users at the /validators/cf-worker subpath (same wording in packages/client/CHANGELOG.md lines 13–16, and in .changeset/cfworker-out-of-barrel.md on main).

Extended reasoning...

What the bug is

The #1897 (cfworker-out-of-barrel) changeset landed on 2026-04-28 and produced new CHANGELOG text in this PR's diff at packages/server/CHANGELOG.md:7-9 and packages/client/CHANGELOG.md:13-16. The entry ends with the explicit assertion "No public API change." That assertion is false from an alpha.2→alpha.3 consumer's perspective, and is internally contradicted by the sentence immediately preceding it: "The validator is now reachable only via the _shims conditional (workerd/browser) and the explicit @modelcontextprotocol/{server,client}/validators/cf-worker subpath" — i.e., no longer reachable from the package root, which is by definition a public-API change.

The specific code path

At the alpha.2 publish commit (0021561), packages/core/src/exports/public/index.ts:140 contained:

export { CfWorkerJsonSchemaValidator } from '../../validators/cfWorkerProvider.js';

Both packages/client/src/index.ts and packages/server/src/index.ts do export * from '@modelcontextprotocol/core/public', so in alpha.2 CfWorkerJsonSchemaValidator was a value export at the root of both published packages. Commit 7ba58da (#1843) deleted that line from the public barrel; at HEAD, grepping packages/core/src/exports/public/ for CfWorkerJsonSchemaValidator returns zero matches. The class is now reachable only via the ./validators/cf-worker subpath and the _shims conditional (renamed as DefaultJsonSchemaValidator).

Why nothing catches it

The Changesets action renders changeset prose verbatim; there is no check that CHANGELOG text matches the actual export diff. #1843 (which did the public-barrel removal) has no changeset of its own, so the #1897 entry is the only release-note coverage of the cf-worker move — and it explicitly tells readers the opposite of what happened. Per REVIEW.md → Documentation & Changesets: "prose that promises behavior the code no longer ships misleads consumers… Flag any claim the diff doesn't back."; per REVIEW.md → Correctness: "removed exports — flagged and justified."

Relationship to existing comment 3093006800

Inline comment 3093006800 (filed 2026-04-16) was advisory guidance asking that any future "#1843 entry" be worded as breaking — at that time no entry existed. The #1897 changeset landed twelve days later with the opposite wording: an explicit "No public API change" assertion. So this is now a concrete false statement in the diff being merged, not just prospective guidance. A maintainer following 3093006800 verbatim would look to add a "#1843 entry" and might not connect it to the existing #1897 entry that needs its last sentence rewritten. The actionable fix overlaps (same one-sentence edit), which is why this is filed as a nit, but it points at a specific false claim now present in the text.

Step-by-step proof

  1. git show 0021561:packages/core/src/exports/public/index.ts | grep -n CfWorker → line 140: export { CfWorkerJsonSchemaValidator } from '../../validators/cfWorkerProvider.js'.
  2. At HEAD, rg CfWorkerJsonSchemaValidator packages/core/src/exports/public/ → zero matches; packages/{client,server}/src/index.ts add no compensating direct re-export.
  3. Therefore alpha.2 user code import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server' compiles in alpha.2 and fails in alpha.3 with "has no exported member 'CfWorkerJsonSchemaValidator'".
  4. packages/server/CHANGELOG.md:9 (and packages/client/CHANGELOG.md:16) in this PR's diff: "…so consumers that don't opt into it no longer ship that code. No public API change."
  5. Step 3 directly falsifies step 4. The user reads "No public API change", upgrades, and gets a compile error with nothing in the release notes pointing at the cause.

Impact and fix

Docs-only, alpha-to-alpha, and the entry already names the new subpath — so a user who reads the whole entry can figure out where the class went. The problem is the explicit "No public API change" tail, which actively tells them not to look. One-sentence fix in both CHANGELOGs (and in .changeset/cfworker-out-of-barrel.md on main so it doesn't regenerate wrong):

Breaking (alpha): CfWorkerJsonSchemaValidator is no longer exported from the package root — import it from @modelcontextprotocol/{client,server}/validators/cf-worker.

One nuance: #1897 in isolation only removed the export from core/src/index.ts (the internal barrel), so "No public API change" is narrowly defensible for what #1897 alone did. But the entry's own text describes the combined #1843+#1897 outcome ("now reachable only via … subpath"), and since #1843 has no changeset this entry is the de-facto coverage for the alpha.2→alpha.3 root-export removal — so the closing claim should match.

@github-actions github-actions Bot force-pushed the changeset-release/main branch from cd754dc to 17271c3 Compare April 29, 2026 13:50
{
"name": "@modelcontextprotocol/node",
"version": "2.0.0-alpha.2",
"version": "2.0.0-alpha.3",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The typesVersions block in packages/middleware/node/package.json (added by #1898, shipping in this release) maps ssedist/sse.d.mts, but there is no ./sse export, no src/sse.ts, and no tsdown entry to emit that file — and the #1898 changeset prose reproduced in this CHANGELOG documents only zod-schemas and stdio as the intentional forward-declared no-op entries, not sse. Functionally harmless (TypeScript silently ignores unresolvable typesVersions mappings and there's no ./sse in exports to reach it), but it's orphan metadata in the published alpha.3 package.json that the changeset doesn't back — either drop the sse entry (which empties the typesVersions block on this package) or, if a node SSE subpath is actually planned, add it to the documented forward-declaration list.

Extended reasoning...

What the issue is

PR #1898 (commit 2a7611d, shipping in this alpha.3 release) added typesVersions blocks across the workspace so consumers on legacy moduleResolution: "node" can resolve subpath type declarations. In packages/middleware/node/package.json (lines 30–36) it added:

"typesVersions": {
    "*": {
        "sse": [
            "dist/sse.d.mts"
        ]
    }
}

However, nothing else in the @modelcontextprotocol/node package backs this entry:

  • The exports map (lines 23–28) contains only "." — there is no "./sse" subpath, so even with modern nodenext/bundler resolution no one can import @modelcontextprotocol/node/sse.
  • packages/middleware/node/src/ contains only index.ts, streamableHttp.ts, and streamableHttp.examples.ts — there is no sse.ts, and git log --all -- packages/middleware/node/src/sse* returns nothing, so the file never existed.
  • packages/middleware/node/tsdown.config.ts has entry: ['src/index.ts'] only, so dist/sse.d.mts will never be emitted.
  • Grepping the repo for @modelcontextprotocol/node/sse returns zero matches.

Why this isn't covered by the documented "forward-declaration" pattern

The #1898 changeset text — reproduced verbatim in this PR's diff at packages/middleware/node/CHANGELOG.md lines 11–14 — explicitly says: "The typesVersions map includes entries for subpaths added by sibling PRs in this series (zod-schemas, stdio); those entries are no-ops until the corresponding dist/*.d.mts files exist." That sentence justifies the dangling zod-schemas/stdio entries in client/server, but sse is not in that list, and there is no sibling PR adding an SSE subpath to @modelcontextprotocol/node (git log --all -S 'dist/sse.d.mts' shows the string was introduced solely by 2a7611d). Compare with packages/server/package.json (lines 56–67), where the typesVersions entries (validators/cf-worker, zod-schemas, stdio) each correspond to a real or documented-as-forthcoming export. So the sse entry on the node package is either a copy-paste leftover or an undocumented forward-declaration — either way it doesn't match its own changeset prose.

Step-by-step proof

  1. packages/middleware/node/package.json:30-36typesVersions["*"].sse = ["dist/sse.d.mts"].
  2. packages/middleware/node/package.json:23-28exports has only "."; no "./sse".
  3. ls packages/middleware/node/src/index.ts, streamableHttp.ts, streamableHttp.examples.ts. No sse.ts.
  4. packages/middleware/node/tsdown.config.tsentry: ['src/index.ts']. Build emits dist/index.{mjs,d.mts} only; dist/sse.d.mts is never produced.
  5. packages/middleware/node/CHANGELOG.md:11-14 (this PR's diff) → forward-declared subpaths listed as zod-schemas, stdio. sse absent.
  6. Result: the published alpha.3 @modelcontextprotocol/node package.json carries a typesVersions mapping to a file that doesn't exist, isn't built, isn't exported, and isn't documented as forthcoming.

Impact

Functionally none — TypeScript silently ignores typesVersions mappings whose target files don't resolve, and since there is no ./sse in exports, no consumer can write import … from '@modelcontextprotocol/node/sse' to reach the mapping in the first place. The only effect is dead metadata in the published package.json that contradicts the #1898 changeset's own enumeration of which forward-declarations were intentional. Per REVIEW.md → Documentation & Changesets ("Flag any claim the diff doesn't back"), and consistent with the other docs/metadata-consistency nits already filed on this PR, it's worth a one-line cleanup before publish but is not blocking.

It's plausible this was intended as a forward-declaration for a future Node SSE transport (v1 had SSEServerTransport, and the node middleware package is a natural home for it). If so, the changeset prose should say so alongside zod-schemas/stdio; if not, the entry (and the now-empty typesVersions block on this package) should be removed.

How to fix

Either:

  • Remove "sse": ["dist/sse.d.mts"] from packages/middleware/node/package.json and drop the now-empty typesVersions block (the package has no subpaths, so it doesn't need one); or
  • If an @modelcontextprotocol/node/sse subpath is actually planned, update .changeset/legacy-module-resolution-types.md (and the generated CHANGELOG entries) to list sse alongside zod-schemas/stdio as an intentional forward-declared no-op.

@github-actions github-actions Bot force-pushed the changeset-release/main branch 2 times, most recently from 09db93a to 1c2faf3 Compare April 29, 2026 15:40
Comment on lines +28 to +32
- [#1901](https://github.com/modelcontextprotocol/typescript-sdk/pull/1901) [`e15a8ef`](https://github.com/modelcontextprotocol/typescript-sdk/commit/e15a8ef3be19520d8159ae9f5b464ba3ac80a5ab) Thanks [@felixweinberger](https://github.com/felixweinberger)! -
`registerTool`/`registerPrompt` accept a raw Zod shape (`{ field: z.string() }`) for `inputSchema`/`outputSchema`/`argsSchema` in addition to a wrapped Standard Schema. Raw shapes are auto-wrapped with `z.object()`. The raw-shape overloads are `@deprecated`; prefer wrapping
with `z.object()`.

Also widens the `completable()` constraint from `StandardSchemaWithJSON` to `StandardSchemaV1` so v1's `completable(z.string(), fn)` continues to work.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The #1901 CHANGELOG entry being added here says registerTool/registerPrompt now accept raw Zod shapes (auto-wrapped with z.object(), @deprecated), but docs/migration.md and docs/migration-SKILL.md still flatly state raw shapes are "no longer supported / no longer accepted" — #1901 (e15a8ef) didn't touch either file. Following the stale guidance still produces the preferred code so this is a nit, but the categorical "no longer supported" claim is now false and directly contradicts the CHANGELOG text in this same diff; suggest softening to "preferred (raw Zod shapes deprecated but still accepted)" in both migration docs.

Extended reasoning...

What the bug is

PR #1901 (commit e15a8ef, merged 2026-04-29 — after the most recent review round at 14:08 UTC) re-added raw-Zod-shape support to registerTool/registerPrompt as a @deprecated compat overload. Its CHANGELOG entry, being added in this PR's diff at packages/server/CHANGELOG.md:28-32 and packages/core/CHANGELOG.md:10-14, explicitly states:

registerTool/registerPrompt accept a raw Zod shape ({ field: z.string() }) for inputSchema/outputSchema/argsSchema in addition to a wrapped Standard Schema. Raw shapes are auto-wrapped with z.object(). The raw-shape overloads are @deprecated; prefer wrapping with z.object().

However, #1901 did not touch docs/migration.md or docs/migration-SKILL.mdgit show e15a8ef --stat shows only .changeset/register-rawshape-compat.md, packages/core/src/{index.ts,util/zodCompat.ts}, packages/core/test/util/zodCompat.test.ts, packages/server/src/server/{completable.ts,mcp.ts}, and packages/server/test/server/mcp.compat.test.ts. Both migration docs therefore still flatly assert the opposite of what #1901 ships:

Why nothing prevents it

Per REVIEW.md → Documentation & Changesets: "Bugfix or behavior change: check whether docs/**/*.md describes the old behavior and needs updating; flag prose that now contradicts the implementation." The Changesets bot only generates CHANGELOG prose; it doesn't validate that hand-written migration docs agree with it. The whole point of #1901 was to reduce v1→v2 migration friction by accepting the v1 pattern unchanged; the migration docs telling users it's "no longer supported" defeats that purpose, and the SKILL file directs LLM-driven migrations to perform an unnecessary mechanical rewrite as a mandatory step.

Step-by-step proof

  1. git show e15a8ef --stat → only .changeset/, packages/core/, packages/server/ files changed; neither docs/migration.md nor docs/migration-SKILL.md appears.
  2. docs/migration.md:248 at HEAD → heading reads "Standard Schema objects required (raw shapes no longer supported)"; line 250 reads "Raw object shapes are no longer accepted."
  3. docs/migration-SKILL.md:213 at HEAD → "raw shapes like { name: z.string() } are no longer supported"; line 500 step 5 → "Wrap all raw Zod shapes with z.object()" (mandatory).
  4. packages/server/CHANGELOG.md:28-30 in this PR's diff → "registerTool/registerPrompt accept a raw Zod shape … Raw shapes are auto-wrapped with z.object()."
  5. Steps 2–3 directly contradict step 4. A v1→v2 migrator reading the migration guide is told the raw-shape pattern is unsupported; the same release's CHANGELOG (and the actual code) says it is supported.

Relationship to existing PR comments

None of the 21 prior comments cover #1901 or the raw-shape migration-doc claims. The closest is comment 3147334299, which is about #1834/InMemoryTransport missing a row in migration-SKILL.md — a different topic. #1901 (e15a8ef) is the second-most-recent commit on main and landed after the last review round (2026-04-29 14:08 UTC), so this could not have been caught earlier.

Impact and fix

Filed as a nit because following the stale guidance still produces correct, preferred code — the raw-shape overload is @deprecated and z.object() wrapping is what the CHANGELOG itself recommends. The defect is purely the categorical "no longer supported / no longer accepted" assertion, which is now factually false and contradicts text shipping in this same diff.

Suggested edits (docs-only, can land on main alongside this publish gate):

  • docs/migration.md:248 → heading: "Standard Schema objects preferred (raw Zod shapes deprecated)"
  • docs/migration.md:250"Raw Zod shapes are still accepted for backward compatibility (auto-wrapped with z.object()), but the overload is @deprecated — prefer wrapping explicitly."
  • docs/migration-SKILL.md:213 → same softening ("deprecated but still accepted; prefer wrapping with z.object()").
  • docs/migration-SKILL.md:500 → demote step 5 from mandatory to recommended (e.g. "(Recommended) Wrap raw Zod shapes with z.object() — raw shapes still work via a deprecated overload.").

"name": "@modelcontextprotocol/express",
"private": false,
"version": "2.0.0-alpha.2",
"version": "2.0.0-alpha.3",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 #1907 (shipping for the first time in this release) added type-level imports of AuthInfo/OAuthMetadata/OAuthProtectedResourceMetadata from the @modelcontextprotocol/server peerDependency, and tsdown's dts step doesn't externalize the peer — it inlines server's declarations into packages/middleware/express/dist/index.d.mts (lines 92–155, including OAuthMetadataSchema: z.ZodObject<…>) and emits line 2: import * as z from "zod/v4". @modelcontextprotocol/express declares no zod dependency anywhere, so consumers on pnpm-isolated/Yarn-PnP with skipLibCheck:false (a config #1766 explicitly supports) get TS2307. Distinct from comment 3078867455 (different package, different deps, different root cause — unintended peer-dep inlining vs. intentional core inlining), and new in alpha.3 (alpha.2's dist/index.d.mts had only the express import). Preferred fix: make the dts bundler externalize @modelcontextprotocol/server (the paths mapping at packages/middleware/express/tsdown.config.ts:18 points to '../server/src/index.ts', which doesn't resolve from packages/middleware/express), so the emitted .d.mts reads import type { AuthInfo, OAuthMetadata, … } from '@modelcontextprotocol/server'; band-aid: add zod to express's dependencies.

Extended reasoning...

What the bug is

PR #1907 (commit 7cccc2a, published for the first time as @modelcontextprotocol/express@2.0.0-alpha.3 in this release) added src/auth/types.ts and src/auth/metadataRouter.ts, which import AuthInfo, OAuthMetadata, and OAuthProtectedResourceMetadata at the type level from @modelcontextprotocol/server — a peerDependency of the express package. tsdown's dts bundler does not externalize the peer for the .d.mts output: it follows the workspace symlink into packages/server/dist and inlines the referenced declarations into packages/middleware/express/dist/index.d.mts (region marker //#region ../../server/dist/index-KfO1-oQM.d.mts, lines ~92–155). Because OAuthMetadata is defined in server as z.infer<typeof OAuthMetadataSchema>, the inlined block drags in declare const OAuthMetadataSchema: z.ZodObject<{…}> and adds line 2: import * as z from "zod/v4"; to express's .d.mts.

packages/middleware/express/package.json declares no zod anywhere — only cors (dependencies) and express + @modelcontextprotocol/server (peerDependencies). So the published alpha.3 .d.mts references a phantom dependency.

Why nothing prevents it

The runtime .mjs is fine — it correctly externalizes the peer (import { OAuthError, OAuthErrorCode, … } from "@modelcontextprotocol/server"); only the dts step inlines. packages/middleware/express/tsdown.config.ts sets dts.compilerOptions.paths['@modelcontextprotocol/server'] = ['../server/src/index.ts'], but relative to packages/middleware/express that points at packages/middleware/server/src/index.ts, which doesn't exist (should be '../../server/...'), so the mapping is a no-op and the dts bundler falls back to the workspace symlink and inlines from server's built dist. There's no dts.external/respectExternal configured for the peer.

This wasn't visible before #1907: at the alpha.2 publish commit (0021561), packages/middleware/express/src/index.ts only re-exported express.js and hostHeaderValidation.js, which import only values (not types) from server — so the alpha.2 dist/index.d.mts had a single import line (import { Express, RequestHandler } from "express";) and no zod reference. The entire src/auth/ directory was created by 7cccc2a, so the zod/v4 import is genuinely new in alpha.3.

Relationship to existing comment 3078867455

That comment covers a similar symptom (undeclared type-only deps in .d.mts) for @modelcontextprotocol/{client,server} importing ajv/json-schema-typed, where the root cause is intentional core inlining via noExternal: ['@modelcontextprotocol/core'], and which is pre-existing since alpha.1. This express case is a different package, a different undeclared dep (zod/v4), a different root cause (unintended inlining of a peer dependency that should stay external), and is new in alpha.3 via #1907. The same pre-existing pattern also exists in @modelcontextprotocol/node (its alpha.2 .d.mts already had import * as z from "zod/v4"), so a root-cause fix here (externalize the peer in the dts step) would clean that package up too.

Step-by-step proof

  1. Build at HEAD: pnpm --filter @modelcontextprotocol/express build. packages/middleware/express/dist/index.d.mts line 2 is import * as z from "zod/v4";; line ~92 begins //#region ../../server/dist/index-KfO1-oQM.d.mts; lines ~97–155 contain declare const OAuthMetadataSchema: z.ZodObject<{…}>, type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>, and a local interface AuthInfo {…} (not import type { AuthInfo } from '@modelcontextprotocol/server').
  2. packages/middleware/express/dist/index.mjs line 2 is import { OAuthError, OAuthErrorCode, … } from "@modelcontextprotocol/server" — runtime correctly externalizes the peer.
  3. packages/middleware/express/package.json: dependencies = { cors }; peerDependencies = { @modelcontextprotocol/server, express }. No zod.
  4. A consumer installs @modelcontextprotocol/express@2.0.0-alpha.3 + its peers under pnpm isolated mode or Yarn PnP (which don't expose a peer's transitive deps to siblings) with "skipLibCheck": false in tsconfig.
  5. tsc --noEmitnode_modules/@modelcontextprotocol/express/dist/index.d.mts(2,…): error TS2307: Cannot find module 'zod/v4' or its corresponding type declarations. Per the alpha.1 CHANGELOG entry for fix(core): add explicit | undefined to Transport interface optional properties #1766, skipLibCheck:false is an explicitly supported config.
  6. Build at the alpha.2 publish commit (0021561): dist/index.d.mts has only import { Express, RequestHandler } from "express"; — confirms this surfaces with feat(express): restore Resource-Server auth glue (requireBearerAuth, mcpAuthMetadataRouter) #1907 in alpha.3.

(npm/yarn-classic users escape via hoisting, since the @modelcontextprotocol/server peer brings zod to the top of node_modules.)

Impact

Two consequences. (a) Undeclared phantom dependency — TS2307 for the strict-linker + skipLibCheck:false population, on any import from @modelcontextprotocol/express (not just the auth helpers). (b) Peer-dependency types frozen at build timeAuthInfo/OAuthMetadata are now declared locally in express's .d.mts instead of import type { … } from '@modelcontextprotocol/server'. The declare module 'express-serve-static-core' { interface Request { auth?: AuthInfo } } augmentation at lines ~183–191 references the inlined AuthInfo, not the one from the consumer's installed @modelcontextprotocol/server — structurally identical today, but it defeats the point of @modelcontextprotocol/server being a peer (workspace:^) and is a divergence vector if the peer's types evolve.

Filed as a nit because the runtime .mjs is unaffected, the blast radius is the strict-linker + skipLibCheck:false intersection, and most consumers use skipLibCheck:true and/or hoisting linkers — same severity tier as the analogous client/server finding in 3078867455. Worth flagging at the publish gate since it's new in alpha.3.

How to fix

Preferred: stop the dts bundler from inlining the peer so the emitted .d.mts reads import type { AuthInfo, OAuthMetadata, OAuthProtectedResourceMetadata } from '@modelcontextprotocol/server' — e.g. fix the broken paths mapping in packages/middleware/express/tsdown.config.ts ('../server/src/index.ts''../../server/src/index.ts', or drop it) and/or pass @modelcontextprotocol/server to tsdown's dts external/respectExternal option. This removes the zod/v4 import entirely and keeps the peer contract intact. The same fix applied to packages/middleware/node/tsdown.config.ts would resolve the pre-existing instance there.

Band-aid: add "zod": "catalog:runtimeShared" to packages/middleware/express/package.json dependencies so the .d.mts import resolves. This papers over the TS2307 but leaves the peer-dep types frozen/duplicated in express's .d.mts.

Comment on lines +40 to +41
- [#1976](https://github.com/modelcontextprotocol/typescript-sdk/pull/1976) [`55b1f06`](https://github.com/modelcontextprotocol/typescript-sdk/commit/55b1f06cd4569e334f3435b7971f0446f1ef9be9) Thanks [@felixweinberger](https://github.com/felixweinberger)! - refactor: subclasses
override `_wrapHandler` hook instead of redeclaring `setRequestHandler`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟣 Pre-existing (carried forward verbatim by #1976, shipping in alpha.3): Server._wrapHandler and Client._wrapHandler throw ProtocolError(ProtocolErrorCode.InvalidParams, …) when the local handler's return value fails schema validation — six sites: server.ts:252,262 and client.ts:371,382,427,439. The peer's request params already passed validation at that point, so a malformed handler output is a local-side bug and per JSON-RPC 2.0 should map to -32603 InternalError, not -32602 InvalidParams — otherwise the peer wrongly concludes its own arguments were bad and may pointlessly retry/reformat. Fix: swap to ProtocolErrorCode.InternalError at the six result-validation throws (leave the request-validation throws at server.ts:237 / client.ts:346,412 as InvalidParams).

Extended reasoning...

What the bug is

_wrapHandler in both Server and Client performs two rounds of schema validation: first the incoming request is validated (peer-fault domain), then the user-supplied handler runs, then the handler's return value is validated against the result schema (local-fault domain). All six result-validation failure throws use ProtocolErrorCode.InvalidParams (-32602):

  • packages/server/src/server/server.ts:252 — "Invalid task creation result" (CreateTaskResultSchema)
  • packages/server/src/server/server.ts:262 — "Invalid tools/call result" (CallToolResultSchema)
  • packages/client/src/client/client.ts:371 — "Invalid task creation result" (elicitation)
  • packages/client/src/client/client.ts:382 — "Invalid elicitation result" (ElicitResultSchema)
  • packages/client/src/client/client.ts:427 — "Invalid task creation result" (sampling)
  • packages/client/src/client/client.ts:439 — "Invalid sampling result" (CreateMessageResultSchema)

Per JSON-RPC 2.0, -32602 means "Invalid method parameter(s)" — i.e. the caller sent bad params. But by the time any of these six throws fire, the caller's params have already passed validation (server.ts:233-238, client.ts:341-347, client.ts:408-413). What failed is the local user-supplied handler returning something that doesn't match the result schema — a server-internal (or client-internal) implementation bug. JSON-RPC 2.0 reserves -32603 Internal Error for exactly this; ProtocolErrorCode.InternalError exists at packages/core/src/types/enums.ts:11.

Why nothing prevents it

The request-validation throws at server.ts:237 and client.ts:346/412 correctly use InvalidParams. The result-validation throws appear to have been copy-pasted from those without changing the error code — the surrounding structure (parseSchema → !….success → format errorMessage → throw) is identical. #1976 (commit 55b1f06, the CHANGELOG entry at this line) moved these blocks verbatim from the old setRequestHandler overrides into the new _wrapHandler hook without revisiting the codes. #1974, which in this same release added SdkErrorCode.InvalidResult for the analogous outbound result-validation failure in _requestWithSchema, didn't touch this inbound side. REVIEW.md → Recurring Catches → Error Handling explicitly calls this pattern out: "must not emit client-fault JSON-RPC codes (-32602 InvalidParams) for server-internal failures … map those to -32603 InternalError so clients don't retry/reformat pointlessly."

Step-by-step proof

  1. Client sends a well-formed tools/call: { method: 'tools/call', params: { name: 'echo', arguments: { msg: 'hi' } } }.
  2. _wrapHandler runs parseSchema(CallToolRequestSchema, request) at server.ts:233 → success. The peer's params are valid.
  3. handler(request, ctx) is awaited at line 242. The server author's handler has a typo and returns { contnet: [{ type: 'text', text: 'hi' }] } (missing content).
  4. Line 258 parseSchema(CallToolResultSchema, result)failure (content required).
  5. Line 262 throws new ProtocolError(ProtocolErrorCode.InvalidParams, 'Invalid tools/call result: …').
  6. Protocol serialises this into a JSON-RPC error response with code: -32602.
  7. The client receives -32602 Invalid params. Per spec semantics it concludes its own params were malformed. A retrying/agentic client may reformat arguments and re-send — wasting a round-trip and never learning the actual fault is on the server side. The same trace applies to the client-side elicitation/sampling paths with roles reversed (server receives -32602 for what is actually a buggy client-side elicitation handler).

Impact

Wrong fault attribution across the wire. Practically: misleading error telemetry (server bugs bucketed as "client sent bad params"), and any peer logic that branches on -32602 (e.g. "reformat and retry" vs -32603 → "surface to user / don't retry") behaves incorrectly. This is the precise anti-pattern REVIEW.md flags. The error message string ("Invalid tools/call result") does hint at the real cause, but error-code dispatch is what programmatic clients use.

Relationship to this PR / existing comments

The wrong codes themselves predate alpha.3 — they existed in the old setRequestHandler overrides — so this is filed as pre-existing. However, #1976 (the entry at this CHANGELOG line) re-emitted every one of these throws into _wrapHandler, so all six lines were touched in this release cycle. None of the 23 prior comments on this PR cover this; the closest sibling is the #1974 entry's SdkErrorCode.InvalidResult work, which fixed the outbound-response analogue but not these inbound-handler-result throws.

How to fix

Swap ProtocolErrorCode.InvalidParamsProtocolErrorCode.InternalError at the six result-validation sites listed above. Leave the three request-validation throws (server.ts:237, client.ts:346, client.ts:412) and the elicitation-mode-capability throws (client.ts:354,358) on InvalidParams — those are genuine peer-fault cases. Six one-token edits; no behaviour change for the success path.

@github-actions github-actions Bot force-pushed the changeset-release/main branch from 1c2faf3 to 0f60567 Compare April 30, 2026 12:18
workspace zod catalog is also bumped to `^4.2.0`.

## 2.0.0-alpha.1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Update to prior 🔴 comment 3068107613: #1887 (96db044, merged 2026-04-30 — after every prior review on this PR) rewrote both migration docs so the documented safeParse replacement is now isSpecType.CallToolResult(value) (specTypeSchema.ts:246, exact safeParse().success), so 3068107613's "fix the migration guide" recommendation is stale and should not be acted on. Residual nit (new in alpha.3): the package root now exports two near-identically-named guards that disagree — isCallToolResult({})false (guards.ts:80 !('content' in value) early-exit) vs isSpecType.CallToolResult({})true — while migration.md:513 / migration-SKILL.md:474 say isCallToolResult "still works" without noting it's stricter. Suggest either dropping the guards.ts:80 early-exit so both guards align, or qualifying the "still works" sentences.

Extended reasoning...

Why comment 3068107613 is now stale

Prior inline comment 3068107613 (🔴, filed 2026-04-11) made the actionable claim that docs/migration.md:450-454 presents isCallToolResult(value) as a drop-in replacement for CallToolResultSchema.safeParse(value).success, which was false because of the !('content' in value) early-exit at guards.ts:80. Its recommended fix was "either remove the early-exit, or update the migration guide."

#1887 (commit 96db044, merged 2026-04-30 — the immediate parent of this version-bump commit and after every prior bot review on this PR) took the second path. git show 96db044 --stat confirms it rewrote both docs/migration.md and docs/migration-SKILL.md. The migration guide now reads (migration.md:497-503 / migration-SKILL.md:469):

v1: CallToolResultSchema.safeParse(value).success → v2: isSpecType.CallToolResult(value)

isSpecType.CallToolResult is implemented at packages/core/src/types/specTypeSchema.ts:246 as exactly (v) => schema.safeParse(v).success — a true equivalence. The isSpecType JSDoc at specTypeSchema.ts:279-283 even explicitly documents that isSpecType.CallToolResult({}) is true because content has a default. So the false-equivalence claim 3068107613 cites no longer exists in the migration guide; a maintainer following that comment to docs/migration.md:450-454 will find different text and a different (correct) recommended guard. Comment 3068107613's "fix the migration guide" recommendation should not be acted on at this publish gate — #1887 already did it. This follows the established correction pattern on this PR (cf. comment 3078867455 correcting five prior comments).

Residual issue (new in alpha.3)

What #1887 introduced is that alpha.3 now ships two public guards with near-identical names that disagree on the same input:

  • isCallToolResult — guards.ts:79-82, has !('content' in value) early-exit, exported at public/index.ts:109
  • isSpecType.CallToolResult — specTypeSchema.ts:246, bare safeParse().success, exported at public/index.ts:142

Both reach consumers via export * from '@modelcontextprotocol/core/public' in client and server. The migration docs say "The pre-existing isCallToolResult(value) guard still works" (migration.md:513) and "isCallToolResult(value) still works" (migration-SKILL.md:474), which reads as interchangeable — it isn't, for inputs lacking an explicit content key. Per REVIEW.md → API surface ("One way to do things — improving an existing API beats adding a parallel one"), two public guards with the same name modulo a dot that return different answers on the same input is worth a one-line cleanup.

Step-by-step proof

  1. const v = {} — no content key. Per CallToolResultSchema, content carries .default([]), so CallToolResultSchema.safeParse({}).success === true.
  2. isSpecType.CallToolResult(v) → specTypeSchema.ts:246 runs CallToolResultSchema.safeParse({}).successtrue. (Explicitly documented at specTypeSchema.ts:282.)
  3. isCallToolResult(v) → guards.ts:80 evaluates !('content' in {})true → early-return false.
  4. Both functions are imported from the same package root: import { isCallToolResult, isSpecType } from '@modelcontextprotocol/server'.
  5. migration.md:513 says "The pre-existing isCallToolResult(value) guard still works" with no qualifier; a reader who picks it over isSpecType.CallToolResult for the documented safeParse replacement gets a stricter check than the guide describes.

Impact and fix

Filed as a nit because: (a) the isSpecType JSDoc already documents the {} edge case explicitly; (b) per the MCP spec and the TS output type, content is always present in real CallToolResult values (the divergence only affects unparsed/hand-constructed objects without an explicit content key); (c) the "still works" sentence isn't false per se, just imprecise. The primary value of this comment is flagging that a 🔴 prior comment is obsolete so maintainers don't block alpha.3 on it.

Two one-line fix options for the residual divergence:

  • (a) Drop the !('content' in value) clause at guards.ts:80, leaving isCallToolResult as a thin wrapper over CallToolResultSchema.safeParse(value).success — this aligns it with isSpecType.CallToolResult and makes the safeParse equivalence hold for both. Or:
  • (b) Qualify the "still works" sentences at migration.md:513 and migration-SKILL.md:474 to note that isCallToolResult is stricter (requires content to be present on the input), e.g. "isCallToolResult(value) still works but requires an explicit content field; prefer isSpecType.CallToolResult for exact safeParse semantics."

@github-actions github-actions Bot force-pushed the changeset-release/main branch from 0f60567 to 7837598 Compare April 30, 2026 13:04
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.

0 participants