| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Some authorization servers serialize absent optional members as JSON null, which RFC 6749 does not sanction but is common in the wild. Previously, OAuthTokensSchema rejected refresh_token/scope/id_token when null with a Zod validation error, and expires_in: null silently coerced to 0 (Number(null) === 0), producing a token the client treated as already expired. This broke token exchange and refresh against such servers. Normalize null optional members to absent (undefined) before validation. Inferred output types are unchanged (string | undefined, number | undefined), so OAuthTokens consumers are unaffected. Related: #754 (same null-serialization pattern hitting the client registration schema).
🦋 Changeset detectedLatest commit: db68fb0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Sorry, something went wrong.
|
@modelcontextprotocol/client
npm i https://pkg.pr.new/@modelcontextprotocol/client@2462
npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2462
npm i https://pkg.pr.new/@modelcontextprotocol/core@2462
npm i https://pkg.pr.new/@modelcontextprotocol/server@2462
npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2462
npm i https://pkg.pr.new/@modelcontextprotocol/express@2462
npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2462
npm i https://pkg.pr.new/@modelcontextprotocol/hono@2462
npm i https://pkg.pr.new/@modelcontextprotocol/node@2462 commit: db68fb0 |
Sorry, something went wrong.
…in OAuthTokensSchema
Rework after review: the per-field z.preprocess mechanic left null-valued
keys present-as-undefined in the parsed output rather than absent, so
refreshAuthorization's spread over the previous refresh token was clobbered
by the explicit refresh_token: undefined — for exactly the null-emitting
servers this fix targets, every refresh silently destroyed the stored
refresh token. It also degraded z.input of the exported schema on zod <4.4.
- Revert OAuthTokensSchema to its original plain object definition,
restoring .shape/.extend/z.input for consumers (and the derived
specTypeSchemas/isSpecType input types).
- Add OAuthTokenResponseSchema, which removes null-valued optional members
(derived from the schema shape, not a hardcoded field list) before
validation, mirroring ElicitResult's null-leniency idiom, and use it at
the SDK's own token-response parse sites: executeTokenRequest, the
JWT-grant cross-app exchange, and server-legacy's proxyProvider.
- Harden refreshAuthorization's merge to
{ ...tokens, refresh_token: tokens.refresh_token ?? refreshToken } so a
present-but-undefined key can never clobber the preserved token.
- Tests now pin strict key absence (toStrictEqual / 'in' checks), null
access_token and missing token_type rejection, the exported schema's
unchanged shape/extend/input behavior, a shape-driven drift guard for
future optional members, and a refreshAuthorization e2e with
refresh_token: null that fails against the previous mechanic.
Related: #754 (same null-serialization pattern hitting the client
registration schema).
Stripping a null scope from a token response makes it indistinguishable from an omitted scope, which RFC 6749 §5.1 defines as an assertion that the granted scope is identical to the requested scope. Document that consumers must not infer the granted scope from its absence and should use token introspection for the authoritative grant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
The changeset no longer bumps @modelcontextprotocol/core: its exported OAuthTokensSchema is deliberately unchanged and OAuthTokenResponseSchema is not part of core's shipped surface (not exported from core's index, not in core-internal's authSchemas registry that core's export group is test-pinned to), matching the sibling changeset convention of patching core-internal without core. The OAuthTokenResponseSchema JSDoc no longer claims the SDK never uses tokens.scope for authorization decisions: the 403 insufficient_scope step-up path feeds tokens.scope into isStrictScopeSuperset, where an absent scope is treated as the empty set and forces a fresh authorization request rather than a refresh. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
There was a problem hiding this comment.
No new issues found on this revision — the latest commit resolves two of the three items from my earlier review (the changeset no longer bumps @modelcontextprotocol/core, and the OAuthTokenResponseSchema JSDoc now correctly describes the step-up path's conservative use of tokens.scope) — but since this touches OAuth token handling across client, cross-app, and proxy-provider flows, it should get a human maintainer's sign-off.
What was reviewed on this pass:
This PR makes the SDK's own OAuth token-response parse sites tolerant of authorization servers that serialize absent optional members as JSON null (nonconformant with RFC 6749 §5.1 but common in the wild, e.g. Ory Hydra per #754). It introduces OAuthTokenResponseSchema in packages/core-internal/src/shared/auth.ts — an object-level z.preprocess that deletes null-valued keys whose member schema accepts undefined, then delegates to the unchanged OAuthTokensSchema — and switches four wire-parse sites to it (executeTokenRequest, exchangeJwtAuthGrant, and both proxyProvider exchanges). It also hardens refreshAuthorization's merge to { ...tokens, refresh_token: tokens.refresh_token ?? refreshToken } so a present-but-undefined key can never clobber the stored refresh token.
This is auth code, and the change is a leniency change in token-response validation. The risk surface is narrow: only null-valued optional members are stripped (the drift-guard test derives the member list from the schema shape and the tests pin that access_token: null and a missing token_type still reject), the exported OAuthTokensSchema is unchanged, and expires_in: null now parses as absent rather than silently coercing to 0. The semantic subtlety — a stripped scope: null becoming indistinguishable from RFC 6749 §5.1's "granted = requested" assertion — is documented in both the JSDoc and the changeset, and the step-up path treats absence conservatively (forces re-authorization). I did not find a way for the normalization to widen what a malicious AS can assert.
High — this is production OAuth flow code shipped in three packages, and it deliberately relaxes validation against RFC-nonconformant servers. That trade-off (interop vs. strictness), plus the introduction of a new internal schema alongside the exported one, is exactly the kind of judgment a human maintainer should ratify, per the repo's minimalism/burden-of-proof conventions. The mechanics themselves are sound and unusually well-tested (strict key-absence assertions, drift guard over future optional members, end-to-end refresh-token-preservation regression test).
This revision responds to prior review: commit e3d7aab drops @modelcontextprotocol/core from the changeset frontmatter (verified — the frontmatter now lists only core-internal/client/server-legacy, matching changeset-bot's 3-package report) and corrects the JSDoc's overbroad "never uses tokens.scope" claim to accurately describe the 403 insufficient_scope step-up behavior. The remaining open item from my earlier inline comments — sibling parse sites (OAuthErrorResponseSchema at both executeTokenRequest error branches, IdJagTokenExchangeResponseSchema in requestJwtAuthorizationGrant) still rejecting null-valued optionals — is unchanged; the PR description explicitly scopes out the registration schema, and whether to extend the normalization to error/exchange responses is a scoping decision for the maintainer rather than a defect in what this PR ships. No bugs were found by this run's bug-hunting pass.
Sorry, something went wrong.
Resolves the conflict with #2477 (schema sources moved into @modelcontextprotocol/core): OAuthTokenResponseSchema now lives in packages/core/src/auth.ts next to OAuthTokensSchema, and core-internal's shared/auth.ts re-export shim forwards it, per the schemaShims purity pins. The normalization design is unchanged; all parse sites keep importing from @modelcontextprotocol/core-internal, which now forwards from core. Because the schema is now part of core's shipped /internal entry, the changeset re-adds the @modelcontextprotocol/core patch bump that was dropped earlier today (that removal was correct against the pre-#2477 base, where core's shipped surface was untouched). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
…uide The guide claimed core's exports are the exact constants the SDK validates OAuth payloads against, and pointed gateway authors at OAuthTokensSchema for token responses. Since the SDK's own token parse sites now validate with OAuthTokenResponseSchema (an internal null-tolerant wrapper around OAuthTokensSchema), qualify the intro and note the wrapper next to the OAuth naming-convention list. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
There was a problem hiding this comment.
I found no bugs on the current revision, and the earlier review findings (JSDoc scope-usage claim, the core changeset bump, and the wire-schemas doc drift) are all addressed by e3d7aab, 0c105c1, and 791c379 — but this changes OAuth token-exchange/refresh behavior across client, server-legacy, and core's shipped /internal surface, so a maintainer should make the final call.
What was reviewed this run:
This PR makes the SDK tolerate authorization servers that serialize absent optional token-response members as JSON null (Ory-Hydra-style, cf. #754). It adds OAuthTokenResponseSchema — an object-level z.preprocess wrapper around the unchanged OAuthTokensSchema that deletes null-valued optional keys before validation — defined in packages/core/src/auth.ts and forwarded through core-internal's re-export shim. All four SDK token parse sites migrate to it (executeTokenRequest, exchangeJwtAuthGrant, and both proxyProvider exchanges), and refreshAuthorization's merge is hardened to { ...tokens, refresh_token: tokens.refresh_token ?? refreshToken }. Test coverage is unusually thorough: strict key-absence assertions, per-field and all-null cases, expires_in null-vs-0, required-field rejection pins, a shape-driven drift guard, and end-to-end exchange/refresh tests.
Low. The change loosens parsing only for optional members whose value is null; required members (access_token, token_type) still reject null/absence, so no token can be minted from a degenerate response. The refresh-merge reorder is the one behavioral edge — verified that the parsed response's keys are strictly absent (not present-but-undefined) so the stored refresh token can't be clobbered, and the test suite pins exactly that failure mode from the PR's first revision. Still, this is the OAuth client token path, which is squarely auth-sensitive code.
High — token exchange, refresh, and the cross-app JWT-grant flow are production auth paths spanning three published packages, and the PR adds a symbol to core's shipped /internal surface (with a deliberate decision, recorded in 0c105c1, to re-add the core patch bump after #2477 moved the schema source into core). Those are exactly the API-surface and release-hygiene calls a maintainer should ratify, per this repo's minimalism/burden-of-proof conventions.
All three earlier inline findings from prior review runs are resolved in the current head: the JSDoc no longer over-claims about tokens.scope (e3d7aab), the core changeset bump is now justified by the schema shipping in core's /internal entry post-#2477 (0c105c1), and docs/advanced/wire-schemas.md now documents the null-tolerant wrapper (791c379). The remaining strict sibling schemas (OAuthErrorResponseSchema, IdJagTokenExchangeResponseSchema) are an explicitly documented scope carve-out in the PR description rather than an oversight. Nothing here blocks the PR; deferring only because auth-path behavior changes and a new shipped-surface symbol warrant human sign-off.
Sorry, something went wrong.
…to-v2 codemod The sibling v1.x PR makes OAuthTokenResponseSchema public v1 API, so the codemod must route its imports somewhere that exports it. Core's root barrel now exports the schema, AUTH_SCHEMA_NAMES includes it, and the drift-guard tests pin the new membership. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q2uDjqaRGTv8vFth6iiU17
…ctness OAuthTokenResponseSchema is a public core export as of the codemod mapping commit, so the wire-schemas guide no longer calls it internal and now points raw-wire consumers at it directly. Both the guide and the changeset also stop claiming the plain OAuthTokensSchema rejects nulls outright: it rejects null for its string-typed members, but expires_in: null coerces to 0 there (verified empirically against the branch schemas). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
…nalogy RFC 6749 sends no scope parameter on refresh, so a refresh response without a scope member (including one whose scope: null was stripped by OAuthTokenResponseSchema) asserts the grant is unchanged. The refresh branch previously saved the response as-is, erasing the stored scope; after a restart the insufficient_scope step-up would compute its union without the original grant and force interactive re-authorization. Mirror the refresh_token hardening: preserve the stored scope when the response has none, while a scope the server does return stays authoritative. Tests pin null, omitted, and server-narrowed scope on the auth() refresh path. Also reword the OAuthTokenResponseSchema JSDoc: it claimed the key removal mirrors ElicitResult's content null normalization, but that mechanic maps null to a present-but-undefined member — exactly what this schema rejects. The analogy now states the shared goal and the stronger mechanic. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
The scope-preservation merge wrote scope: undefined into the saveTokens payload when both the refresh response and the stored tokens lacked a scope — the present-but-undefined key shape this PR's normalization exists to prevent. A conditional spread now includes the scope member only when a preserved value exists. New test pins the no-scope-anywhere refresh payload with toStrictEqual (asserting on the refresh save, not the SEP-2352 issuer back-stamp call). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
… export Additive public API on core's root barrel is a minor under semver, and core's changelog ships export-surface additions as minor (#2354, #2513). The changeset fixed group lifts core, client, server, server-legacy, and codemod together to 2.1.0. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
Flipping core to minor major-bumps express, fastify, hono, and node to 3.0.0: they peer-depend on server via workspace:^, server rides the fixed group to minor, and .changeset/config.json does not set onlyUpdatePeerDependentsWhenOutOfRange, so changesets' default major-bumps peer-dependents on any non-patch peer bump. The additive public export arguably warrants a minor per repo precedent, but that blast radius is a maintainer decision; the PR body records the options. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
There was a problem hiding this comment.
No new issues found this run — the follow-up commits resolve all of my earlier inline findings, but this is OAuth token-handling code with an explicitly open maintainer decision (changeset bump level), so it needs a human sign-off rather than a shadow approval.
What was reviewed this run:
This PR makes the SDK tolerate OAuth token responses whose optional members are serialized as JSON null (nonconformant but common, e.g. Ory-Hydra-style servers). It adds OAuthTokenResponseSchema — an object-level z.preprocess wrapper that removes null-valued optional keys before OAuthTokensSchema validates — and switches all four SDK token parse sites to it (client exchange/refresh, JWT-grant cross-app exchange, server-legacy proxy provider). It also hardens the refresh merge (refresh_token and scope preservation), exports the new schema publicly from core, updates the codemod allowlist, and documents everything in wire-schemas.md and a changeset.
This is auth-flow code, so the risk surface is real: token persistence, refresh-token preservation, and scope handling. The changes are conservative — required members (access_token, token_type) still reject null, only optional members are normalized, and the merge hardening only fills absent values from previously-stored tokens (a server-returned scope remains authoritative, so no scope-widening path is introduced). Prior review rounds on this PR specifically adversarially examined refresh-token clobbering and scope erasure; the current revision fixes both with strict key-absence tests (toStrictEqual / 'key' in assertions).
High. OAuth client/server token handling is security-sensitive per the approval guidelines, which alone rules out shadow approval regardless of correctness. Additionally, the PR body explicitly leaves a versioning decision to maintainers: the new public core export arguably warrants a minor bump, but flipping it currently major-bumps the middleware packages via changesets' peer-dependent default — a repo-policy call no bot should make.
Test coverage is thorough: unit tests for the new schema (per-field null stripping, key-absence pins, expires_in coercion behavior, a shape-driven drift guard for future members), end-to-end exchangeAuthorization/refreshAuthorization/auth() tests including all four scope-preservation cases, and drift guards in core and the codemod pinning the export surface. All previous inline findings from earlier review runs (doc drift, JSDoc analogy inversion, scope erasure, present-but-undefined scope key, bump level) were verified as addressed in the follow-up commits. The remaining open item is purely the bump-level maintainer call documented in the PR body.
Sorry, something went wrong.
Resolves conflicts with #2053 (propagate saveTokens errors after refresh), which moved the post-refresh saveTokens call out of the try block so persistence failures surface instead of falling through to a new authorization request. This branch's scope preservation moves with it: the conditional spread now applies at the relocated save site, so a refresh response without scope still keeps the stored grant and the payload never carries a present-but-undefined scope key. In the tests, both sides had added a discovery fixture for the auth() refresh path: main's mockDiscoveryWithTokenEndpoint (per-call token endpoint factory, needed by the #2034 regression tests) is kept as the shared base, and this branch's mockRefreshFetchWithTokenResponse is now a thin wrapper over it. All four scope-preservation tests and all of main's #2034 tests are retained. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
There was a problem hiding this comment.
I re-reviewed this PR after the merge with main and found no bugs this pass, but because it changes OAuth token handling and leaves the core patch-vs-minor changeset question open for maintainers, a human sign-off is still worthwhile.
What was reviewed:
The PR adds a null-tolerant OAuthTokenResponseSchema in packages/core/src/auth.ts (a z.preprocess around OAuthTokensSchema that deletes null-valued keys whose member schema accepts undefined), switches the SDK's token-response parse sites to it (executeTokenRequest in packages/client/src/client/auth.ts, crossAppAccess.ts, server-legacy proxyProvider.ts), hardens the refreshAuthorization merge ({ ...tokens, refresh_token: tokens.refresh_token ?? refreshToken }), and preserves stored scope across refresh via a conditional spread. It also exports the new schema from core's root, forwards it through core-internal's shim, updates the v1-to-v2 codemod allowlist, docs, tests, and a changeset. The only change since my last review round is the db68fb0 merge of main, which resolved conflicts with #2053's relocation of the post-refresh saveTokens call.
This is auth-critical code: it affects how OAuth token responses are validated and how refresh tokens and scopes are persisted. I verified the preprocess only removes keys that are exactly null and whose member schema accepts undefined (so access_token: null and a missing token_type still fail), passes non-object values through untouched, and copies the input rather than mutating it. The scope-preservation logic is conservative: absence is treated as unchanged-grant on refresh only, and the step-up path's treatment of absent scope is unchanged. No injection or data-exposure surface is introduced; the main residual risk is semantic (accepting nonconformant server responses), which is the PR's stated purpose and is bounded to the six optional members.
High, because it touches the OAuth client flow and token persistence. The bug-hunting run went dry with no findings; the two ruled-out candidates were duplicate quality nits about re-deriving the optional-member set per parse (cheap, six members, not a correctness issue). My prior review rounds raised several findings (JSDoc accuracy, doc drift, changeset packaging, present-but-undefined scope key, refresh merge); the current head addresses all of them, and the merge commit correctly carried the scope-preservation fix to the relocated save site with #2053's error-propagation behavior intact. Because the paths are security-sensitive and the author explicitly left the core patch-vs-minor versioning decision (with its peer-dependency major-bump side effect) to maintainers, approval without a human is not appropriate.
Test coverage is substantial and shape-driven: strict key-absence assertions, per-field null cases, expires_in: null vs 0, rejection pins for access_token: null, end-to-end exchange/refresh flows, drift guards in core and the codemod, and the merge retained both branches' test suites. The exported OAuthTokensSchema is deliberately unchanged, so downstream .shape/.extend consumers are unaffected. The change follows the repo's schema-source conventions (defined in core, forwarded via core-internal) and the codemod-mapping rule from CLAUDE.md.
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Requested by Den Delimarsky · Slack thread
Before: connecting to an MCP server whose authorization server returns "refresh_token": null (or "scope": null / "id_token": null) fails with a Zod validation error ("expected string, received null") inside exchangeAuthorization/refreshAuthorization, before any tokens are saved. Worse, "expires_in": null passed validation but silently coerced to 0 (Number(null) === 0), producing a token the client treats as already expired.
After: those responses are accepted, and the null-valued fields are treated exactly as if they had been omitted. expires_in: null parses as absent, not 0.
How: the exported OAuthTokensSchema is unchanged from main — still a plain object schema that rejects nulls, with its .shape/.extend and z.input behavior intact (so specTypeSchemas/isSpecType input types are untouched). A new OAuthTokenResponseSchema wraps it in an object-level z.preprocess that removes null-valued optional members before validation — the member list is derived from the schema's shape, not hardcoded, mirroring the ElicitResult content null-leniency idiom — and the SDK's own token-response parse sites now use it: executeTokenRequest (client token exchange + refresh), the JWT-grant cross-app exchange (crossAppAccess.ts), and server-legacy's proxyProvider. Following the #2477 schema-source move, the new schema is defined in packages/core/src/auth.ts (next to OAuthTokensSchema) and forwarded through core-internal's shared/auth.ts re-export shim, so every existing import path keeps working. Removing the key (rather than mapping it to undefined) means null members are strictly absent from the parsed output, so refreshAuthorization's merge with previously-stored tokens keeps the prior refresh token; that merge is additionally hardened to { ...tokens, refresh_token: tokens.refresh_token ?? refreshToken }. access_token: null and a missing token_type are still rejected.
Revision after review: an adversarial review of the first version of this PR found that its per-field z.preprocess(v => v ?? undefined, ...) mechanic left null-valued keys present with an undefined value rather than absent, so refreshAuthorization's spread of the previous refresh token was clobbered by the explicit refresh_token: undefined — for exactly the null-emitting servers this PR targets, every refresh would have silently destroyed the stored refresh token (it also degraded z.input of the exported schema on zod <4.4). This revision fixes that by normalizing at the SDK's parse sites via the new response schema and hardening the merge.
Tests (packages/core-internal/test/shared/auth.test.ts, packages/client/test/client/auth.test.ts): strict key-absence assertions (toStrictEqual / 'field' in parsed checks, which distinguish absent keys from present-but-undefined ones), each null optional field, all-optionals-null, expires_in: null !== 0, string expires_in coercion, access_token: null and missing token_type rejection, regression pins that the exported OAuthTokensSchema still rejects nulls and keeps its ZodObject shape/extend/input behavior, a shape-driven drift guard covering every optional member, an end-to-end exchangeAuthorization with an all-null-optional response, and an end-to-end refreshAuthorization where the server returns refresh_token: null asserting the original refresh token is preserved (verified to fail against the previous mechanic). The changeset (patch for core, core-internal, client, server-legacy) is updated to describe the final mechanic; core is bumped because, after #2477, the new schema ships in core's /internal entry (core's public barrel and the authSchemas registry are deliberately unchanged).
RFC 6749 doesn't sanction null for absent members, but real-world servers emit it anyway — see #754 for the same null-emitting-server pattern (Ory Hydra) hitting the client registration response schema (that schema is intentionally left out of scope here).
Scope of the normalization: this change deliberately covers only RFC 6749 §5.1 token responses at the SDK's token parse sites. Two sibling parse sites intentionally remain strict about null members: the OAuth error-response schema (OAuthErrorResponseSchema, used by parseErrorResponse) and the RFC 8693 §2.2.1 ID-JAG token-exchange schema (IdJagTokenExchangeResponseSchema in crossAppAccess.ts). That is consistent with the #754 registration-schema carve-out above; either can be revisited if field evidence of null-emitting servers turns up for those responses.
Sibling PR with the same fix against v1.x: #2461.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KXnNnp3fxQYR5HF9BUVhYP
Generated by Claude Code
Update (2026-08-05)
OAuthTokenResponseSchema is now also exported from @modelcontextprotocol/core's public root, since the sibling v1.x PR #2461 makes it public v1 API and migrating code needs a v2 home for it. The v1-to-v2 codemod's AUTH_SCHEMA_NAMES allowlist now includes the name, so imports of it from @modelcontextprotocol/sdk/shared/auth.js rewrite to @modelcontextprotocol/core instead of landing on a package that does not export it. Drift-guard tests in core and the codemod pin the new membership, and the changeset now covers the codemod package.
Versioning note for maintainers: the new public core export arguably warrants a minor per this repo's precedent for export-surface additions (#2354, #2513). However, flipping the changeset's core entry to minor currently major-bumps @modelcontextprotocol/express/fastify/hono/node to 3.0.0: they peer-depend on @modelcontextprotocol/server via workspace:^, server rides the fixed group to 2.1.0, and .changeset/config.json does not set onlyUpdatePeerDependentsWhenOutOfRange: true, so changesets' default major-bumps peer-dependents on any non-patch peer bump. The changeset is left at patch pending a maintainer decision: either add that config flag and flip core to minor, or accept the additive export shipping in a patch.