Skip to content

Commit db68fb0

Browse files
committed
Merge branch 'main' into claude/oauth-tokens-null-fields-v2
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
2 parents 0e4a6f1 + 3924de9 commit db68fb0

49 files changed

Lines changed: 1670 additions & 170 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@modelcontextprotocol/core-internal': patch
3+
'@modelcontextprotocol/client': patch
4+
'@modelcontextprotocol/server': patch
5+
---
6+
7+
Treat request id `0` as a real id. Two guards tested a `RequestId` for truthiness, so the legal JSON-RPC ids `0` and `''` were read as absent. Id `0` is not a corner case: the outbound request counter is zero-based, so it is the first id every peer assigns, which on the server→client leg is the first `sampling/createMessage`, `elicitation/create`, or `roots/list` a server sends.
8+
9+
- `notifications/cancelled` carrying id `0` was ignored, and the in-flight handler ran to completion with its `AbortSignal` never fired.
10+
- A notification sent with `relatedRequestId: 0` wrongly passed the debounce gate (for methods opted into `debouncedNotificationMethods`). Because the pending set is keyed by method alone, a second such notification in the same tick was silently dropped rather than sent.
11+
12+
Absent is now the only value that means "no id".
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@modelcontextprotocol/core-internal': patch
3+
'@modelcontextprotocol/client': patch
4+
'@modelcontextprotocol/server': patch
5+
---
6+
7+
Stop sending `notifications/cancelled` for the `initialize` handshake. The spec is explicit that a client MUST NOT attempt to cancel its `initialize` request, but the outbound cancel path fired for any in-flight request: aborting the `AbortSignal` passed to `connect()`, or letting the handshake hit its timeout, put a forbidden cancellation on the wire naming the initialize request id.
8+
9+
The local behaviour is unchanged — the caller's promise still rejects with the same abort/timeout error, and `connect()` still tears the connection down. Only the wire notification is suppressed. Every other method keeps the existing cancellation path.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@modelcontextprotocol/client': patch
3+
---
4+
5+
Let `saveTokens` failures surface after a successful token refresh. In `auth()`, one `try`
6+
wrapped both `refreshAuthorization()` and the `provider.saveTokens()` that persists its
7+
result, and the `catch` deliberately swallows anything that is not an `OAuthError` — plus
8+
`ServerError` — so that a failed refresh falls through to a fresh authorization request.
9+
A persistence error thrown by the provider landed in that same branch: it was discarded
10+
with no log and no rethrow, and `auth()` continued to `startAuthorization()` and returned
11+
`'REDIRECT'`.
12+
13+
Against an authorization server that rotates refresh tokens (the OAuth 2.1 default, and
14+
Keycloak's) this loses credentials rather than merely hiding an error. The exchange has
15+
already succeeded server-side, so the old refresh token is invalidated at the moment the
16+
new one is issued; dropping the new token set leaves nothing usable on either side. On a
17+
headless or CLI client, where `redirectToAuthorization` is typically a no-op, the fallthrough
18+
is silent and the client is left with stale tokens and no indication of why.
19+
20+
The `try`/`catch` now covers only `refreshAuthorization()`. Persisting the result happens
21+
after it, on an unguarded path, so a provider's I/O error propagates to the caller.
22+
23+
Refresh-request failures keep their existing control flow exactly: a `ServerError` or an
24+
unknown error still falls through to a new authorization flow, a non-`ServerError`
25+
`OAuthError` is still rethrown, and `InsecureTokenEndpointError` is still surfaced. The
26+
SEP-2352 `issuer` stamp written with the refreshed tokens is unchanged.
27+
28+
Those fallbacks no longer happen in silence, though. Both routes to an unexplained
29+
re-authorization now emit a `console.warn` naming the cause: the in-place fallthrough in
30+
the refresh block, and `auth()`'s outer recovery for `invalid_grant`, `invalid_client`,
31+
and `unauthorized_client`, which discards stored credentials and retries. The second one
32+
matters most in practice — an expired, revoked, or rotation-reuse-detected refresh token
33+
is reported as `invalid_grant`, which is precisely the state a dropped token set leaves
34+
behind for the next call.
35+
36+
Consumers whose `OAuthClientProvider.saveTokens` can reject should note that `auth()` may
37+
now reject where it previously returned `'REDIRECT'` — that rejection is the failure that
38+
was being discarded.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@modelcontextprotocol/core-internal': patch
3+
'@modelcontextprotocol/server': patch
4+
---
5+
6+
Reject a modern (2026-07-28) POST that omits the required `MCP-Protocol-Version` header.
7+
8+
`createMcpHandler` accepted a request whose body carried a valid per-request `_meta`
9+
envelope but whose `MCP-Protocol-Version` header was absent: the request was classified
10+
modern, dispatched, and answered `200` — tool handlers ran. Only the _mismatch_ case
11+
(header present, disagreeing with the body) was rejected, so of the standard headers
12+
SEP-2243 requires on a modern POST, presence was enforced for `Mcp-Method` (and for
13+
`Mcp-Name` on the methods that mirror `params.name` / `params.uri`) but not for
14+
`MCP-Protocol-Version`.
15+
16+
Such a request is now refused with `400 Bad Request` and JSON-RPC `-32020`
17+
(`HeaderMismatch`), matching the shape the sibling missing-header cells already emit and
18+
echoing the request id — per the Streamable HTTP spec, which requires the header on every
19+
POST and lists a missing required standard header as a `HeaderMismatch` failure. The
20+
spec's allowance to treat a header-less request as `2025-03-26` is available only to a
21+
server that also serves pre-2025-06-18 clients, and permits routing it to _legacy_
22+
handling — never serving it as 2026-07-28; under `legacy: 'reject'` the requirement is
23+
unconditional.
24+
25+
Era classification is deliberately unchanged and stays body-primary: a proxy that strips
26+
the header still must not change the era, so such a request is still _classified_ modern
27+
and is refused one rung later, at `standard-header-validation` — the same rung that
28+
already answers a missing `Mcp-Method`. Legacy-era traffic is untouched, notifications
29+
are unaffected, body-less `GET` / `DELETE` session operations are method-routed before
30+
any header validation, and stdio serving (which has no HTTP headers) is not involved.
31+
32+
Clients built with this SDK always send the header, so no first-party client is affected;
33+
hand-rolled clients that omitted it must add it.

DEPENDENCY_POLICY.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Dependency Policy
2+
3+
As a library consumed by downstream projects, the MCP TypeScript SDK takes a conservative approach to dependency updates. Dependencies are kept stable unless there is a specific reason to update, such as a security vulnerability, a bug fix, or a need for new functionality.
4+
5+
This policy applies to every published package in this monorepo (`@modelcontextprotocol/core`, `client`, `server`, `server-legacy`, `codemod`, `node`, `express`, `hono`, `fastify`) and to the `v1.x` maintenance line (`@modelcontextprotocol/sdk`).
6+
7+
## Update Triggers
8+
9+
Dependencies are updated when:
10+
11+
- A **security vulnerability** is disclosed (via GitHub security alerts).
12+
- A bug in a dependency directly affects the SDK.
13+
- A new dependency feature is needed for SDK development.
14+
- A dependency drops support for a Node.js version the SDK still targets.
15+
- A new MCP specification revision requires it.
16+
17+
Routine version bumps without a clear motivation are avoided to minimize churn for downstream consumers.
18+
19+
## What We Don't Do
20+
21+
The SDK does not run scheduled version bumps for npm dependencies. Updating a dependency can force downstream consumers to adopt that update transitively, which can be disruptive for projects with strict dependency policies.
22+
23+
Dependencies are only updated when there is a concrete reason, not simply because a newer version is available.
24+
25+
## Automated Tooling
26+
27+
- **GitHub security updates** are enabled at the repository level and automatically open pull requests for npm packages with known vulnerabilities. This is a GitHub repo setting, separate from the `dependabot.yml` configuration.
28+
- **GitHub Actions versions** are kept up to date via Dependabot on a weekly schedule (see `.github/dependabot.yml`).
29+
- **Supply-chain cooldown**: pnpm's `minimumReleaseAge` (see `pnpm-workspace.yaml`) keeps newly published versions out of the lockfile for 7 days (`minimumReleaseAgeExclude` lists the exceptions, currently the MCP conformance suite), and only an allow-listed set of dependencies may run install scripts (`onlyBuiltDependencies`).
30+
31+
## Pinning and Ranges
32+
33+
Ranges shared by more than one package live in the pnpm workspace catalogs (`pnpm-workspace.yaml`), so a version is declared once. Runtime dependencies use caret ranges (`^`) to allow compatible updates within a major version; exact versions of third-party runtime dependencies are pinned only when necessary to work around a specific issue. Dependencies between the SDK's own packages (`workspace:*`) publish as exact pins by design, so a released `client` or `server` always resolves the `core` it was built against. Framework integrations (`express`, `hono`, `fastify`) declare the framework as a peer dependency rather than bundling a copy.
34+
35+
Runtime dependencies of published packages are kept to a minimum; adding one is a significant change under the discuss-before-you-code rule in `CONTRIBUTING.md`.

ROADMAP.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Roadmap
2+
3+
## Spec Implementation Tracking
4+
5+
The SDK tracks implementation of MCP spec components via GitHub Projects, with a dedicated project board for each spec revision:
6+
7+
- [2026-07-28 spec revision board](https://github.com/orgs/modelcontextprotocol/projects/41) — implemented in v2.0.0 (released 2026-07-27, alongside the spec).
8+
- [2025-11-25 spec revision board](https://github.com/orgs/modelcontextprotocol/projects/26) — implemented in v1.23.0 (2025-11-25) and carried into v2, minus the experimental tasks component (SEP-1686), which v2 does not serve.
9+
10+
Conformance against the 2025-11-25 and 2026-07-28 revisions runs on each push to `main` (and against 2025-11-25 on `v1.x`) via the [conformance workflow](https://github.com/modelcontextprotocol/typescript-sdk/actions/workflows/conformance.yml) using the [MCP conformance suite](https://github.com/modelcontextprotocol/conformance).
11+
12+
## Current Focus Areas
13+
14+
### v2 hardening
15+
16+
v2.0.0 is the stable release line (`main`). Post-release work is tracked as issues on this repository and released as 2.x patch and minor releases (see `VERSIONING.md`):
17+
18+
- Migration tooling and guides (`@modelcontextprotocol/codemod`, `docs/migration/`).
19+
- Runtime coverage beyond Node.js (Bun, Deno, Cloudflare Workers, Vercel) and the framework integrations (`node`, `express`, `hono`, `fastify`).
20+
- Documentation completeness for every non-experimental spec feature at https://ts.sdk.modelcontextprotocol.io/v2/.
21+
22+
### Next Spec Revision
23+
24+
The next MCP specification revision is being developed in the [protocol repository](https://github.com/modelcontextprotocol/modelcontextprotocol). The SDK implements accepted SEPs as they are finalized so that support ships with the spec release, with a dedicated project board tracking component-level progress for that revision.
25+
26+
### Extensions
27+
28+
Protocol extensions are implemented as they stabilize and are not part of the core tier requirements:
29+
30+
- Tasks (`io.modelcontextprotocol/tasks`) — [#2189](https://github.com/modelcontextprotocol/typescript-sdk/issues/2189).
31+
- Client authentication extensions: Workload Identity Federation (SEP-1933) — [#2576](https://github.com/modelcontextprotocol/typescript-sdk/issues/2576); DPoP (SEP-1932).
32+
33+
### v1.x Maintenance
34+
35+
The `v1.x` branch (`@modelcontextprotocol/sdk`) continues to receive bug fixes and security updates for at least six months after the v2 release (2026-07-27). It targets the 2025-11-25 spec revision; new spec revisions are implemented on `main` only.

VERSIONING.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Versioning Policy
2+
3+
The MCP TypeScript SDK follows [Semantic Versioning 2.0.0](https://semver.org/) for every published package.
4+
5+
## Packages and Version Groups
6+
7+
The v2 SDK is a monorepo. Versions are managed with [Changesets](https://github.com/changesets/changesets) (`.changeset/`):
8+
9+
- `@modelcontextprotocol/core`, `client`, `server`, `server-legacy` and `codemod` form a **fixed group** and always release together with the same version.
10+
- The framework integrations `@modelcontextprotocol/node`, `express`, `hono` and `fastify` are versioned through Changesets alongside the fixed group: they bump whenever their `@modelcontextprotocol/server` peer range has to move, and independently for their own changes.
11+
- `@modelcontextprotocol/core-internal` is private and carries no compatibility promise; the `@modelcontextprotocol/core/internal` entry point is likewise not covered by this policy and may change in any release.
12+
- The `v1.x` branch continues to publish `@modelcontextprotocol/sdk` 1.x under the same rules (patch releases on `release-X.Y` npm tags; see `CONTRIBUTING.md`).
13+
14+
## Version Format
15+
16+
`MAJOR.MINOR.PATCH`
17+
18+
- **MAJOR**: Incremented for breaking changes (see below).
19+
- **MINOR**: Incremented for new features that are backward-compatible.
20+
- **PATCH**: Incremented for backward-compatible bug fixes.
21+
22+
## What Constitutes a Breaking Change
23+
24+
The following changes are considered breaking and require a major version bump:
25+
26+
- Removing or renaming a public API export (class, function, type, or constant).
27+
- Changing the signature of a public function or method in a way that breaks existing callers (removing parameters, changing required/optional status, changing types).
28+
- Removing or renaming a public type or interface field.
29+
- Changing the behavior of an existing API in a way that breaks documented contracts.
30+
- Dropping support for a Node.js LTS version.
31+
- Removing support for a transport type.
32+
- Dropping support for an MCP protocol revision the SDK previously negotiated (see `docs/protocol-versions.md`).
33+
34+
The following are **not** considered breaking:
35+
36+
- Adding new optional parameters to existing functions.
37+
- Adding new exports, types, or interfaces.
38+
- Adding new optional fields to existing types.
39+
- Bug fixes that correct behavior to match documented intent.
40+
- Internal refactoring that does not affect the public API.
41+
- Adding support for new MCP spec revisions or features.
42+
- Changes to dev dependencies or build tooling.
43+
44+
## How Breaking Changes Are Communicated
45+
46+
1. **Changelog**: Every consumer-facing change ships with a changeset; the per-package `CHANGELOG.md` and the GitHub release for each package tag document breaking changes with migration instructions.
47+
2. **Deprecation**: When feasible, APIs are deprecated for at least one minor release before removal using `@deprecated` JSDoc annotations, which surface warnings through TypeScript tooling and editors. Protocol features the specification deprecates stay available for as long as the specification keeps them.
48+
3. **Migration guide**: Major version releases include a migration guide (see `docs/migration/`) and, where practical, a codemod (`@modelcontextprotocol/codemod`).
49+
4. **PR labels**: Pull requests containing breaking changes are labeled with `breaking change`.

docs/clients/calling.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,30 @@ The updates stream in while the call is still pending; the return type does not
169169
[ { type: 'text', text: '2 orders exported as csv' } ]
170170
```
171171

172+
## Check the connection
173+
174+
`ping` sends a `ping` request and resolves with the empty result the server returns; the SDK answers a `ping` on both sides automatically, so neither side registers a handler.
175+
176+
```ts source="../../examples/guides/clients/calling.examples.ts#ping_basic"
177+
const pong = await client.ping({ timeout: 5000 });
178+
console.log(pong);
179+
```
180+
181+
The `orders` server answers at once:
182+
183+
```
184+
{}
185+
```
186+
187+
A server that stops answering rejects the call with an `SdkError` coded `REQUEST_TIMEOUT` once `timeout` elapses.
188+
189+
`ping` is a 2025-era method — see [Protocol versions](../protocol-versions.md).
190+
172191
## Recap
173192

174193
- `listTools`, `listResources`, `listResourceTemplates`, and `listPrompts` aggregate every page; `{ cursor }` fetches a single raw page and `listMaxPages` caps the walk.
175194
- `callTool` returns `content` for the model and, when the tool declares an `outputSchema`, `structuredContent` for your application.
176195
- `readResource({ uri })` and `getPrompt({ name, arguments })` follow the same list-then-fetch shape as tools.
177196
- `complete()` returns the server's suggestions for a prompt or resource-template argument.
178197
- `onprogress` in the request options streams progress updates without changing the call's return type.
198+
- `ping()` checks that the server still answers; both sides answer pings automatically.

docs/migration/support-2026-07-28.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,11 @@ coverage, spawn `serveStdio` as a child process.
236236
On a 2026-07-28 Streamable HTTP connection, aborting an in-flight client request
237237
(`signal` / timeout) closes that request's SSE response stream — the spec cancellation
238238
signal — instead of POSTing `notifications/cancelled`. Nothing to change in calling
239-
code. 2025-era connections and stdio at any era still send `notifications/cancelled`.
240-
Custom `Transport` implementations that open one underlying request per outbound message
241-
and honor `TransportSendOptions.requestSignal` may opt in by declaring
239+
code. 2025-era connections and stdio at any era still send `notifications/cancelled`
240+
(except for the `initialize` handshake, which the spec forbids cancelling — an aborted
241+
or timed-out `connect()` rejects locally and sends nothing). Custom `Transport`
242+
implementations that open one underlying request per outbound message and honor
243+
`TransportSendOptions.requestSignal` may opt in by declaring
242244
`readonly hasPerRequestStream = true`.
243245

244246
### `ctx.mcpReq.log()` and the per-request `logLevel`
@@ -636,7 +638,19 @@ present body value, malformed, or disagree with the body — `400 Bad Request` w
636638
JSON-RPC `-32020` (`HeaderMismatch`). The Streamable HTTP transport also emits the
637639
`Mcp-Name` standard header on every modern-enveloped request, and `createMcpHandler`
638640
validates the SEP-2243 standard headers (`MCP-Protocol-Version`, `Mcp-Method`,
639-
`Mcp-Name`) against the body on the modern path with the same rejection.
641+
`Mcp-Name`) against the body on the modern path with the same rejection — both their
642+
**presence** (all three are required on every modern **request** POST; `Mcp-Name` only
643+
for the methods that mirror `params.name` / `params.uri`) and their agreement with the
644+
body. A modern-enveloped request POST that omits `MCP-Protocol-Version` is refused rather
645+
than served, even though the body claim alone still determines the era — so a hand-rolled
646+
client that relied on the body envelope without sending the header must add it. Clients
647+
built with this SDK send all three already.
648+
649+
Notification POSTs are exempt from the presence half: this revision defines no
650+
client-to-server notifications over Streamable HTTP, and states that header requirements
651+
for notification POSTs are not defined by it — so a modern-enveloped notification is
652+
dispatched and answered `202` even with no standard headers at all. Do not rely on the
653+
entry to reject one.
640654

641655
**Modern-era exception** to the `SdkHttpError` mapping: on a modern-enveloped request,
642656
an HTTP `400` whose body is a well-formed JSON-RPC error response addressed to the

0 commit comments

Comments
 (0)