Version Packages (alpha)#1845
Conversation
There was a problem hiding this comment.
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.
35992ec to
1aaa39d
Compare
There was a problem hiding this comment.
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.
| 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. | ||
|
|
There was a problem hiding this comment.
🟡 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
- User sets
requestInit.headers = { Accept: 'application/vnd.proxy-hint' } - Code computes
types = ['application/vnd.proxy-hint', 'text/event-stream'] - Final header:
Accept: application/vnd.proxy-hint, text/event-stream application/vnd.proxy-hinthas higher implicit preference (appears first)- CHANGELOG says custom types are "appended to" spec types, which would produce
text/event-stream, application/vnd.proxy-hint(spec first, custom second) - 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", |
There was a problem hiding this comment.
🟣 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:
dependencies(line 51):"zod": "catalog:runtimeShared"— this means zod is a direct dependency that auto-installs for any consumer of core.peerDependencies(line 55):"zod": "catalog:runtimeShared"— combined withpeerDependenciesMeta.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
- Open
packages/core/package.json. - Observe
"zod": "catalog:runtimeShared"underdependencies— zod will auto-install in any workspace consuming core. - Observe
"zod": "catalog:runtimeShared"underpeerDependencies— a package manager resolving this package would also emit a peer dependency warning. - Observe
peerDependenciesMeta.zod.optional: false— this marks the peer as required, not optional, making the warning an error in strict tools. - Compare with
packages/client/package.json: zod is only independencies, nopeerDependenciesentry — 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.
1aaa39d to
0ac069f
Compare
| - [#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 |
There was a problem hiding this comment.
🟡 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
- Open
packages/server/CHANGELOG.md, lines 7–8: entry saysStreamableHTTPServerTransport.close(). - Search the codebase for
class StreamableHTTPServerTransport— zero matches. - Search for
class WebStandardStreamableHTTPServerTransport— found inpackages/server/src/server/streamableHttp.ts:224. - Search for
class NodeStreamableHTTPServerTransport— found inpackages/middleware/node. - 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".
0ac069f to
fe53cda
Compare
fe53cda to
a1b6ce1
Compare
| ## 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 | ||
|
|
There was a problem hiding this comment.
🟡 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
git show 1eb80c4 -- .changeset/returns nothing — PRv2: add guard methods #1842 created no changeset file.- Open
packages/client/CHANGELOG.mdlines 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. - Open
packages/server/CHANGELOG.mdlines 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. - Open
packages/core/src/exports/public/index.tslines 107–115:isCallToolResult,isJSONRPCResponse, and related guards are exported. - 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.
0cefcf3 to
2bc21b7
Compare
2bc21b7 to
324b11e
Compare
| ## 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. |
There was a problem hiding this comment.
🟡 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
.changeset/zod-json-schema-compat.mdfront-matter: only'@modelcontextprotocol/core': patch.packages/core/package.jsonline 3:"private": true— never published.packages/client/package.jsonandpackages/server/package.json:@modelcontextprotocol/coreappears only underdevDependencies→ changesets does not treat it as an internal dependency to bump.packages/client/CHANGELOG.mdlines 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.packages/server/CHANGELOG.mdlines 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.- 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.requestedSchemaZod-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.
324b11e to
91dceb0
Compare
| { | ||
| "name": "@modelcontextprotocol/client", | ||
| "version": "2.0.0-alpha.2", | ||
| "version": "2.0.0-alpha.3", |
There was a problem hiding this comment.
🔴 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
- User constructs the provider with
claims: { aud: 'https://custom-token-endpoint', jti: 'fixed-id' }, relying on the JSDoc precedence guarantee. - Line 60 produces
claims = { iss, sub, aud: 'https://custom-token-endpoint', exp, iat, jti: 'fixed-id' }— user values win in the merged object. - Line 82 passes that object to
new jose.SignJWT(claims); jose clones it into#payload. - Line 86
.setAudience(audience)runsthis.#payload.aud = audience(the SDK-computedmetadata?.issuer ?? url), overwriting'https://custom-token-endpoint'. - Line 89
.setJti(jti)runsthis.#payload.jti = jti(the SDK-generated random string), overwriting'fixed-id'. - The signed assertion contains the SDK values for
audandjti; the user's values are silently discarded with no warning. Same applies toiss,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/.setJtichain at lines 84–89, leaving justnew jose.SignJWT(claims).setProtectedHeader({ alg, typ: 'JWT' }).sign(key). The mergedclaimsobject 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 viaclaimsand that only additional claims are merged. In that case thebaseClaimsspread at line 60 can also be dropped since it's a no-op for those keys.
| ## 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. |
There was a problem hiding this comment.
🟡 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
git show 9ed62fe --stat→ onlypackages/client/src/client/authExtensions.tsandpackages/client/src/client/authExtensions.test.tschanged.git show 9ed62fe -- .changeset/→ empty; no changeset file added.git log --oneline -2 91dceb0→91dceb0 Version Packages (alpha)followed by9ed62fe feat(client): support custom claims in PrivateKeyJwtProvider (#1875)— 9ed62fe is an ancestor of this release.grep -r "claims\|1875\|PrivateKeyJwt" .changeset/→ no matches.packages/client/CHANGELOG.mdlines 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 ofclaims/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", |
There was a problem hiding this comment.
🟣 🟣 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(theCfWorkerJsonSchemaValidatorclass definition lives at ~line 10343) are all inlined. The tworequire("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, andvalidators/cfWorker.mjsall import only from./src-5RQ50XKB.mjs—grep -E 'from "ajv|from "@cfworker|from "ajv-formats' dist/**/*.mjsreturns 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
npm install @modelcontextprotocol/client@2.0.0-alpha.3in a fresh project →node_modulescontainszod,cross-spawn,eventsource, etc., but notajvorjson-schema-typed.node -e "import('@modelcontextprotocol/client').then(m => console.log(typeof m.AjvJsonSchemaValidator))"→ printsfunction. Runtime works — the prior comments'ERR_MODULE_NOT_FOUNDdoes not reproduce.tsconfig.jsonwith"skipLibCheck": false; source file:import { McpClient } from '@modelcontextprotocol/client';.tsc --noEmit→node_modules/@modelcontextprotocol/client/dist/index-KFyi7eTI.d.mts(2,21): error TS2307: Cannot find module 'ajv' or its corresponding type declarations.and the same forjson-schema-typedon line 3.- 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
ajvandjson-schema-typedtodependenciesof bothpackages/client/package.jsonandpackages/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 sotsccan find their .d.ts). - Stop leaking the third-party types: change
constructor(ajv?: Ajv)toconstructor(ajv?: unknown)(or a locally-declared structural type) and inline/re-declareJSONSchema.Interfaceinstead 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.
91dceb0 to
0f9910f
Compare
| ## 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. |
There was a problem hiding this comment.
🟡 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
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).git show 7ba58da -- packages/core/src/exports/public/index.ts→ that line is deleted; only the type exportCfWorkerSchemaDraftremains.packages/client/src/index.ts:81andpackages/server/src/index.ts:50at HEAD still wildcard-re-export@modelcontextprotocol/core/publicand add no compensating direct re-export of the class.- Therefore alpha.2 user code
import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/server'(or/client) fails in alpha.3 with "has no exported member 'CfWorkerJsonSchemaValidator'". packages/client/CHANGELOG.mdlines 3–19 andpackages/server/CHANGELOG.mdlines 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:
CfWorkerJsonSchemaValidatoris no longer exported from the package root. Import it from@modelcontextprotocol/{client,server}/validators/cf-workerinstead.
rather than just "Added ./validators/cf-worker sub-path export".
There was a problem hiding this comment.
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):
CfWorkerJsonSchemaValidatoris 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.
0f9910f to
f632488
Compare
| - [#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. |
There was a problem hiding this comment.
🟡 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
.changeset/fix-failed-task-result-retrieval.mdfront-matter: only'@modelcontextprotocol/core': patch.git log --onelineshowsbdfd7f0(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.packages/core/CHANGELOG.mdlines 7–8: the fix: retrieve stored result from tasks/result for failed tasks #1930 entry appears here, under a package with"private": true.packages/client/CHANGELOG.mdlines 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 ofrequestStream/tasks/result/ fix: retrieve stored result from tasks/result for failed tasks #1930.packages/server/CHANGELOG.mdlines 3–8 (alpha.3 section): only fix: prevent stack overflow in transport close with re-entrancy guard #1788 listed; same omission.- 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.
6274e0a to
56f4c20
Compare
| - [#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. |
There was a problem hiding this comment.
🟡 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.md … docs/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
git show 42cb6b2 --stat→ 4 files touched;docs/migration-SKILL.mdis not among them.grep -n 'InMemoryTransport\|inMemory' docs/migration-SKILL.md→ zero matches.docs/migration-SKILL.md§3 "Import Mapping" → "Types / shared imports" table (lines ~62–69) maps@modelcontextprotocol/sdk/shared/transport.js→ "@modelcontextprotocol/clientor@modelcontextprotocol/server" — the exact pattern theinMemory.jsrow would follow, but no such row exists.docs/migration.md(post-42cb6b2) →InMemoryTransportsection says "moved — now exported from@modelcontextprotocol/clientand@modelcontextprotocol/server".- CLAUDE.md:27–30 → requires both files updated for breaking changes.
- 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.
4fa1cf7 to
73d66f4
Compare
369ec1e to
cd754dc
Compare
| - [#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. |
There was a problem hiding this comment.
🟡 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
- Integrator implements
verifier.verifyAccessTokento relay an upstream introspection-endpoint message:throw new OAuthError(OAuthErrorCode.InvalidToken, 'Token "abc-123" revoked by admin'). - Request arrives with
Authorization: Bearer abc-123; line 73 calls the verifier, which throws. - Line 92 catches the
OAuthError; line 94 callsbuildWwwAuthenticateHeader('invalid_token', 'Token "abc-123" revoked by admin', [], 'https://rs.example/.well-known/oauth-protected-resource'). - 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". - A strict RFC 7235 parser tokenizes
error_descriptionas the quoted-string"Token "and then encountersabc-123" revoked…as invalid token68/auth-param syntax. Depending on the parser, it either rejects the whole challenge or stops before reachingresource_metadata="…"— defeating the RFC 9728 discovery flow this CHANGELOG entry advertises ("RFC 6750WWW-Authenticatechallenges").
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.
| - [#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. |
There was a problem hiding this comment.
🟡 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
git show 0021561:packages/core/src/exports/public/index.ts | grep -n CfWorker→ line 140:export { CfWorkerJsonSchemaValidator } from '../../validators/cfWorkerProvider.js'.- At HEAD,
rg CfWorkerJsonSchemaValidator packages/core/src/exports/public/→ zero matches;packages/{client,server}/src/index.tsadd no compensating direct re-export. - 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'". packages/server/CHANGELOG.md:9(andpackages/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."- 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):
CfWorkerJsonSchemaValidatoris 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.
cd754dc to
17271c3
Compare
| { | ||
| "name": "@modelcontextprotocol/node", | ||
| "version": "2.0.0-alpha.2", | ||
| "version": "2.0.0-alpha.3", |
There was a problem hiding this comment.
🟡 The typesVersions block in packages/middleware/node/package.json (added by #1898, shipping in this release) maps sse → dist/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
exportsmap (lines 23–28) contains only"."— there is no"./sse"subpath, so even with modernnodenext/bundlerresolution no one can import@modelcontextprotocol/node/sse. packages/middleware/node/src/contains onlyindex.ts,streamableHttp.ts, andstreamableHttp.examples.ts— there is nosse.ts, andgit log --all -- packages/middleware/node/src/sse*returns nothing, so the file never existed.packages/middleware/node/tsdown.config.tshasentry: ['src/index.ts']only, sodist/sse.d.mtswill never be emitted.- Grepping the repo for
@modelcontextprotocol/node/ssereturns 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
packages/middleware/node/package.json:30-36→typesVersions["*"].sse = ["dist/sse.d.mts"].packages/middleware/node/package.json:23-28→exportshas only"."; no"./sse".ls packages/middleware/node/src/→index.ts,streamableHttp.ts,streamableHttp.examples.ts. Nosse.ts.packages/middleware/node/tsdown.config.ts→entry: ['src/index.ts']. Build emitsdist/index.{mjs,d.mts}only;dist/sse.d.mtsis never produced.packages/middleware/node/CHANGELOG.md:11-14(this PR's diff) → forward-declared subpaths listed aszod-schemas,stdio.sseabsent.- Result: the published alpha.3
@modelcontextprotocol/nodepackage.json carries atypesVersionsmapping 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"]frompackages/middleware/node/package.jsonand drop the now-emptytypesVersionsblock (the package has no subpaths, so it doesn't need one); or - If an
@modelcontextprotocol/node/ssesubpath is actually planned, update.changeset/legacy-module-resolution-types.md(and the generated CHANGELOG entries) to listssealongsidezod-schemas/stdioas an intentional forward-declared no-op.
09db93a to
1c2faf3
Compare
| - [#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. |
There was a problem hiding this comment.
🟡 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/registerPromptaccept a raw Zod shape ({ field: z.string() }) forinputSchema/outputSchema/argsSchemain addition to a wrapped Standard Schema. Raw shapes are auto-wrapped withz.object(). The raw-shape overloads are@deprecated; prefer wrapping withz.object().
However, #1901 did not touch docs/migration.md or docs/migration-SKILL.md — git 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:
docs/migration.md:248— section heading: "Standard Schema objects required (raw shapes no longer supported)"docs/migration.md:250— "Raw object shapes are no longer accepted."docs/migration.md:258-260— showsserver.registerTool('greet', { inputSchema: { name: z.string() } }, callback)as the Before (v1) pattern, implying it does not work in v2; feat(compat): registerTool/registerPrompt accept raw Zod shape, auto-wrap with z.object() #1901 makes that exact line work as-is.docs/migration-SKILL.md:213— "raw shapes like{ name: z.string() }are no longer supported. Wrap withz.object()…"docs/migration-SKILL.md:500— step 5 of the mechanical migration: "Wrap all raw Zod shapes withz.object()" — presented as mandatory, when feat(compat): registerTool/registerPrompt accept raw Zod shape, auto-wrap with z.object() #1901 specifically made it optional/deprecated-but-working.
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
git show e15a8ef --stat→ only.changeset/,packages/core/,packages/server/files changed; neitherdocs/migration.mdnordocs/migration-SKILL.mdappears.docs/migration.md:248at HEAD → heading reads "Standard Schema objects required (raw shapes no longer supported)"; line 250 reads "Raw object shapes are no longer accepted."docs/migration-SKILL.md:213at HEAD → "raw shapes like{ name: z.string() }are no longer supported"; line 500 step 5 → "Wrap all raw Zod shapes withz.object()" (mandatory).packages/server/CHANGELOG.md:28-30in this PR's diff → "registerTool/registerPromptaccept a raw Zod shape … Raw shapes are auto-wrapped withz.object()."- 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 withz.object()), but the overload is@deprecated— prefer wrapping explicitly."docs/migration-SKILL.md:213→ same softening ("deprecated but still accepted; prefer wrapping withz.object()").docs/migration-SKILL.md:500→ demote step 5 from mandatory to recommended (e.g. "(Recommended) Wrap raw Zod shapes withz.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", |
There was a problem hiding this comment.
🟡 #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
- Build at HEAD:
pnpm --filter @modelcontextprotocol/express build.packages/middleware/express/dist/index.d.mtsline 2 isimport * as z from "zod/v4";; line ~92 begins//#region ../../server/dist/index-KfO1-oQM.d.mts; lines ~97–155 containdeclare const OAuthMetadataSchema: z.ZodObject<{…}>,type OAuthMetadata = z.infer<typeof OAuthMetadataSchema>, and a localinterface AuthInfo {…}(notimport type { AuthInfo } from '@modelcontextprotocol/server'). packages/middleware/express/dist/index.mjsline 2 isimport { OAuthError, OAuthErrorCode, … } from "@modelcontextprotocol/server"— runtime correctly externalizes the peer.packages/middleware/express/package.json:dependencies={ cors };peerDependencies={ @modelcontextprotocol/server, express }. Nozod.- 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": falsein tsconfig. tsc --noEmit→node_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:falseis an explicitly supported config.- Build at the alpha.2 publish commit (0021561):
dist/index.d.mtshas onlyimport { 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 time — AuthInfo/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.
| - [#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`. |
There was a problem hiding this comment.
🟣 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
- Client sends a well-formed
tools/call:{ method: 'tools/call', params: { name: 'echo', arguments: { msg: 'hi' } } }. _wrapHandlerrunsparseSchema(CallToolRequestSchema, request)at server.ts:233 → success. The peer's params are valid.handler(request, ctx)is awaited at line 242. The server author's handler has a typo and returns{ contnet: [{ type: 'text', text: 'hi' }] }(missingcontent).- Line 258
parseSchema(CallToolResultSchema, result)→ failure (contentrequired). - Line 262 throws
new ProtocolError(ProtocolErrorCode.InvalidParams, 'Invalid tools/call result: …'). Protocolserialises this into a JSON-RPC error response withcode: -32602.- The client receives
-32602 Invalid params. Per spec semantics it concludes its ownparamswere malformed. A retrying/agentic client may reformatargumentsand 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-32602for 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.InvalidParams → ProtocolErrorCode.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.
1c2faf3 to
0f60567
Compare
| workspace zod catalog is also bumped to `^4.2.0`. | ||
|
|
||
| ## 2.0.0-alpha.1 | ||
|
|
There was a problem hiding this comment.
🟡 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:109isSpecType.CallToolResult— specTypeSchema.ts:246, baresafeParse().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
const v = {}— nocontentkey. PerCallToolResultSchema,contentcarries.default([]), soCallToolResultSchema.safeParse({}).success === true.isSpecType.CallToolResult(v)→ specTypeSchema.ts:246 runsCallToolResultSchema.safeParse({}).success→true. (Explicitly documented at specTypeSchema.ts:282.)isCallToolResult(v)→ guards.ts:80 evaluates!('content' in {})→true→ early-returnfalse.- Both functions are imported from the same package root:
import { isCallToolResult, isSpecType } from '@modelcontextprotocol/server'. - migration.md:513 says "The pre-existing
isCallToolResult(value)guard still works" with no qualifier; a reader who picks it overisSpecType.CallToolResultfor the documentedsafeParsereplacement 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, leavingisCallToolResultas a thin wrapper overCallToolResultSchema.safeParse(value).success— this aligns it withisSpecType.CallToolResultand 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
isCallToolResultis stricter (requirescontentto be present on the input), e.g. "isCallToolResult(value)still works but requires an explicitcontentfield; preferisSpecType.CallToolResultfor exactsafeParsesemantics."
0f60567 to
7837598
Compare
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.
mainis currently in pre mode so this branch has prereleases rather than normal releases. If you want to exit prereleases, runchangeset pre exitonmain.Releases
@modelcontextprotocol/client@2.0.0-alpha.3
Minor Changes
#1974
db83829Thanks @felixweinberger! - Add custom (non-spec)method support: a 3-arg
setRequestHandler(method, schemas, handler)/setNotificationHandler(method, schemas, handler)form for vendor-prefixed methods, and arequest(req, resultSchema)overload (also onctx.mcpReq.send) for typed custom-method results. Spec-methodcalls are unchanged.
Response result-schema validation failure now rejects with
SdkError(InvalidResult)instead of a rawZodError. AddsSdkErrorCode.InvalidResult.#1653
6bec24aThanks @rechedev9! - AddvalidateClientMetadataUrl()utility for early validation of
clientMetadataUrlExports a
validateClientMetadataUrl()function thatOAuthClientProviderimplementations 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
96db044Thanks @felixweinberger! - ExportisSpecTypeandspecTypeSchemasrecords for runtime validation of any MCP spec type by name.isSpecType.ContentBlock(value)is a type predicate;specTypeSchemas.ContentBlockis aStandardSchemaV1<ContentBlock>validator. Guards are standalone functions, soarr.filter(isSpecType.ContentBlock)works. Also export theSpecTypeNameandSpecTypestypes.#1871
9fc9070Thanks @felixweinberger! - Move stdio transportsto a
./stdiosubpath export. ImportStdioClientTransport,getDefaultEnvironment,DEFAULT_INHERITED_ENV_VARS, andStdioServerParametersfrom@modelcontextprotocol/client/stdio, andStdioServerTransportfrom@modelcontextprotocol/server/stdio. The@modelcontextprotocol/clientroot entry no longer pulls innode:child_process,node:stream, orcross-spawn, fixing bundling for browser and Cloudflare Workers targets; the@modelcontextprotocol/serverroot entry drops itsnode:streamreference. Node.js, Bun, andDeno consumers update the import path; runtime behavior is unchanged.
Patch Changes
#1897
434b2f1Thanks @felixweinberger! - Stop bundling@cfworker/json-schemainto the main package barrel. PreviouslyCfWorkerJsonSchemaValidatorwas re-exported from the core internal barrel, so tsdown inlined the@cfworker/json-schemadev dependency into every consumer's bundle even when it was never used. The validator isnow reachable only via the
_shimsconditional (workerd/browser) and the explicit@modelcontextprotocol/{server,client}/validators/cf-workersubpath, so consumers that don't opt into it no longer ship that code. No public API change.#1834
42cb6b2Thanks @felixweinberger! - ExportInMemoryTransportfor in-process testing.#1898
2a7611dThanks @felixweinberger! - Add top-leveltypesfield (and
typesVersionson client/server for their subpath exports) so consumers on legacymoduleResolution: "node"can resolve type declarations. Theexportsmap remains the source of truth fornodenext/bundlerresolution. ThetypesVersionsmap includes entriesfor subpaths added by sibling PRs in this series (
zod-schemas,stdio); those entries are no-ops until the correspondingdist/*.d.mtsfiles exist.#1655
1eb3123Thanks @nielskaspers! - fix(client): append customAccept headers to spec-required defaults in StreamableHTTPClientTransport
Custom Accept headers provided via
requestInit.headersare now appended to the spec-mandated Accept types instead of being overwritten. This ensures the required media types (application/json, text/event-streamfor POST;text/event-streamfor GET SSE) are always presentwhile allowing users to include additional types for proxy/gateway routing.
#1976
55b1f06Thanks @felixweinberger! - refactor: subclassesoverride
_wrapHandlerhook instead of redeclaringsetRequestHandler.#1895
b256546Thanks @felixweinberger! - Fix runtime crash ontools/listwhen a tool'sinputSchemacomes from zod 4.0–4.1. The SDK requires~standard.jsonSchema(StandardJSONSchemaV1, added in zod 4.2.0); previously a missingjsonSchemacrashed atundefined[io].standardSchemaToJsonSchemanow detects zod 4 schemas lackingjsonSchemaand falls back to the SDK-bundledz.toJSONSchema(), emitting a one-time console warning. zod 3 schemas (which the bundled zod 4 converter cannot introspect) and non-zod schema libraries withoutjsonSchemaget a clear error pointing tofromJsonSchema(). Theworkspace zod catalog is also bumped to
^4.2.0.@modelcontextprotocol/express@2.0.0-alpha.3
Minor Changes
7cccc2aThanks @felixweinberger! - Add OAuthResource-Server glue to the Express adapter:
requireBearerAuthmiddleware (token verification + RFC 6750WWW-Authenticatechallenges),mcpAuthMetadataRouter(serves RFC 9728 Protected Resource Metadata and mirrors RFC 8414 AS metadata at the resource origin), thegetOAuthProtectedResourceMetadataUrlhelper, and theOAuthTokenVerifierinterface. These restore the v1src/server/authResource-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
2a7611dThanks @felixweinberger! - Add top-leveltypesfield (and
typesVersionson client/server for their subpath exports) so consumers on legacymoduleResolution: "node"can resolve type declarations. Theexportsmap remains the source of truth fornodenext/bundlerresolution. ThetypesVersionsmap includes entriesfor subpaths added by sibling PRs in this series (
zod-schemas,stdio); those entries are no-ops until the correspondingdist/*.d.mtsfiles exist.Updated dependencies [
434b2f1,db83829,42cb6b2,df4b6cc,2a7611d,e15a8ef,96db044,9fc9070,55b1f06,b256546]:@modelcontextprotocol/server@2.0.0-alpha.3
Minor Changes
#1974
db83829Thanks @felixweinberger! - Add custom (non-spec)method support: a 3-arg
setRequestHandler(method, schemas, handler)/setNotificationHandler(method, schemas, handler)form for vendor-prefixed methods, and arequest(req, resultSchema)overload (also onctx.mcpReq.send) for typed custom-method results. Spec-methodcalls are unchanged.
Response result-schema validation failure now rejects with
SdkError(InvalidResult)instead of a rawZodError. AddsSdkErrorCode.InvalidResult.#1887
96db044Thanks @felixweinberger! - ExportisSpecTypeandspecTypeSchemasrecords for runtime validation of any MCP spec type by name.isSpecType.ContentBlock(value)is a type predicate;specTypeSchemas.ContentBlockis aStandardSchemaV1<ContentBlock>validator. Guards are standalone functions, soarr.filter(isSpecType.ContentBlock)works. Also export theSpecTypeNameandSpecTypestypes.#1871
9fc9070Thanks @felixweinberger! - Move stdio transportsto a
./stdiosubpath export. ImportStdioClientTransport,getDefaultEnvironment,DEFAULT_INHERITED_ENV_VARS, andStdioServerParametersfrom@modelcontextprotocol/client/stdio, andStdioServerTransportfrom@modelcontextprotocol/server/stdio. The@modelcontextprotocol/clientroot entry no longer pulls innode:child_process,node:stream, orcross-spawn, fixing bundling for browser and Cloudflare Workers targets; the@modelcontextprotocol/serverroot entry drops itsnode:streamreference. Node.js, Bun, andDeno consumers update the import path; runtime behavior is unchanged.
Patch Changes
#1897
434b2f1Thanks @felixweinberger! - Stop bundling@cfworker/json-schemainto the main package barrel. PreviouslyCfWorkerJsonSchemaValidatorwas re-exported from the core internal barrel, so tsdown inlined the@cfworker/json-schemadev dependency into every consumer's bundle even when it was never used. The validator isnow reachable only via the
_shimsconditional (workerd/browser) and the explicit@modelcontextprotocol/{server,client}/validators/cf-workersubpath, so consumers that don't opt into it no longer ship that code. No public API change.#1834
42cb6b2Thanks @felixweinberger! - ExportInMemoryTransportfor in-process testing.#1788
df4b6ccThanks @claygeo! - Prevent stack overflow inStreamableHTTPServerTransport.close() with re-entrant guard
#1898
2a7611dThanks @felixweinberger! - Add top-leveltypesfield (and
typesVersionson client/server for their subpath exports) so consumers on legacymoduleResolution: "node"can resolve type declarations. Theexportsmap remains the source of truth fornodenext/bundlerresolution. ThetypesVersionsmap includes entriesfor subpaths added by sibling PRs in this series (
zod-schemas,stdio); those entries are no-ops until the correspondingdist/*.d.mtsfiles exist.#1901
e15a8efThanks @felixweinberger! -registerTool/registerPromptaccept a raw Zod shape ({ field: z.string() }) forinputSchema/outputSchema/argsSchemain addition to a wrapped Standard Schema. Raw shapes are auto-wrapped withz.object(). The raw-shape overloads are@deprecated; prefer wrappingwith
z.object().Also widens the
completable()constraint fromStandardSchemaWithJSONtoStandardSchemaV1so v1'scompletable(z.string(), fn)continues to work.#1976
55b1f06Thanks @felixweinberger! - refactor: subclassesoverride
_wrapHandlerhook instead of redeclaringsetRequestHandler.#1895
b256546Thanks @felixweinberger! - Fix runtime crash ontools/listwhen a tool'sinputSchemacomes from zod 4.0–4.1. The SDK requires~standard.jsonSchema(StandardJSONSchemaV1, added in zod 4.2.0); previously a missingjsonSchemacrashed atundefined[io].standardSchemaToJsonSchemanow detects zod 4 schemas lackingjsonSchemaand falls back to the SDK-bundledz.toJSONSchema(), emitting a one-time console warning. zod 3 schemas (which the bundled zod 4 converter cannot introspect) and non-zod schema libraries withoutjsonSchemaget a clear error pointing tofromJsonSchema(). Theworkspace zod catalog is also bumped to
^4.2.0.@modelcontextprotocol/fastify@2.0.0-alpha.3
Patch Changes
#1898
2a7611dThanks @felixweinberger! - Add top-leveltypesfield (and
typesVersionson client/server for their subpath exports) so consumers on legacymoduleResolution: "node"can resolve type declarations. Theexportsmap remains the source of truth fornodenext/bundlerresolution. ThetypesVersionsmap includes entriesfor subpaths added by sibling PRs in this series (
zod-schemas,stdio); those entries are no-ops until the correspondingdist/*.d.mtsfiles exist.Updated dependencies [
434b2f1,db83829,42cb6b2,df4b6cc,2a7611d,e15a8ef,96db044,9fc9070,55b1f06,b256546]:@modelcontextprotocol/hono@2.0.0-alpha.3
Patch Changes
#1898
2a7611dThanks @felixweinberger! - Add top-leveltypesfield (and
typesVersionson client/server for their subpath exports) so consumers on legacymoduleResolution: "node"can resolve type declarations. Theexportsmap remains the source of truth fornodenext/bundlerresolution. ThetypesVersionsmap includes entriesfor subpaths added by sibling PRs in this series (
zod-schemas,stdio); those entries are no-ops until the correspondingdist/*.d.mtsfiles exist.Updated dependencies [
434b2f1,db83829,42cb6b2,df4b6cc,2a7611d,e15a8ef,96db044,9fc9070,55b1f06,b256546]:@modelcontextprotocol/node@2.0.0-alpha.3
Patch Changes
#1896
5433f40Thanks @felixweinberger! - Markhonopeerdependency as optional.
@modelcontextprotocol/nodeonly usesgetRequestListenerfrom@hono/node-server(Node HTTP ↔ Web Standard conversion), which does not require thehonoframework at runtime. Consumers no longer need to installhonoto useNodeStreamableHTTPServerTransport. Note:@hono/node-serveritself still declareshonoas a hard peer, so package managers may emit a warning; this is upstream and harmless forgetRequestListener-only usage.#1898
2a7611dThanks @felixweinberger! - Add top-leveltypesfield (and
typesVersionson client/server for their subpath exports) so consumers on legacymoduleResolution: "node"can resolve type declarations. Theexportsmap remains the source of truth fornodenext/bundlerresolution. ThetypesVersionsmap includes entriesfor subpaths added by sibling PRs in this series (
zod-schemas,stdio); those entries are no-ops until the correspondingdist/*.d.mtsfiles exist.Updated dependencies [
434b2f1,db83829,42cb6b2,df4b6cc,2a7611d,e15a8ef,96db044,9fc9070,55b1f06,b256546]:@modelcontextprotocol/core@2.0.0-alpha.2
Minor Changes
#1974
db83829Thanks @felixweinberger! - Add custom (non-spec)method support: a 3-arg
setRequestHandler(method, schemas, handler)/setNotificationHandler(method, schemas, handler)form for vendor-prefixed methods, and arequest(req, resultSchema)overload (also onctx.mcpReq.send) for typed custom-method results. Spec-methodcalls are unchanged.
Response result-schema validation failure now rejects with
SdkError(InvalidResult)instead of a rawZodError. AddsSdkErrorCode.InvalidResult.Patch Changes
#1930
bdfd7f0Thanks @Christian-Sidak! - FixrequestStreamtocall
tasks/resultfor failed tasks instead of yielding a hardcodedProtocolError. When a task reaches thefailedterminal status, the stream now retrieves and yields the actual stored result (matching the behavior forcompletedtasks), as required by the spec.#1901
e15a8efThanks @felixweinberger! -registerTool/registerPromptaccept a raw Zod shape ({ field: z.string() }) forinputSchema/outputSchema/argsSchemain addition to a wrapped Standard Schema. Raw shapes are auto-wrapped withz.object(). The raw-shape overloads are@deprecated; prefer wrappingwith
z.object().Also widens the
completable()constraint fromStandardSchemaWithJSONtoStandardSchemaV1so v1'scompletable(z.string(), fn)continues to work.#1976
55b1f06Thanks @felixweinberger! - refactor: subclassesoverride
_wrapHandlerhook instead of redeclaringsetRequestHandler.#1768
866c08dThanks @felixweinberger! - Allow additional JSONSchema 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
b256546Thanks @felixweinberger! - Fix runtime crash ontools/listwhen a tool'sinputSchemacomes from zod 4.0–4.1. The SDK requires~standard.jsonSchema(StandardJSONSchemaV1, added in zod 4.2.0); previously a missingjsonSchemacrashed atundefined[io].standardSchemaToJsonSchemanow detects zod 4 schemas lackingjsonSchemaand falls back to the SDK-bundledz.toJSONSchema(), emitting a one-time console warning. zod 3 schemas (which the bundled zod 4 converter cannot introspect) and non-zod schema libraries withoutjsonSchemaget a clear error pointing tofromJsonSchema(). Theworkspace zod catalog is also bumped to
^4.2.0.