Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ac7579b
feat(core): add protocol-era helpers and a typed era-negotiation fail…
felixweinberger Jun 12, 2026
96025b5
feat(client): add the probe-outcome classifier pure module
felixweinberger Jun 12, 2026
c42e3cf
feat(client): opt-in connect-time version negotiation on ClientOptions
felixweinberger Jun 12, 2026
adb8869
test(integration): wire-real negotiation fixtures against deployed-sh…
felixweinberger Jun 12, 2026
d1600c7
feat(core): wire server/discover into the typed request funnel
felixweinberger Jun 12, 2026
e97d22b
feat(server): era-aware version lists — legacy counter-offer + modern…
felixweinberger Jun 12, 2026
8ec466f
feat(client): typed discover() request
felixweinberger Jun 12, 2026
efe2e06
fix(node): forward setSupportedProtocolVersions to the wrapped transport
felixweinberger Jun 12, 2026
07a2dbb
test(integration): discover round-trip against a modern server over r…
felixweinberger Jun 12, 2026
5759300
feat(client): transport-aware probe timeout verdict — stdio falls bac…
felixweinberger Jun 15, 2026
8589cb7
refactor(core): review follow-ups for the era seams
felixweinberger Jun 15, 2026
bb23780
test(e2e): activate the 2026-07-28 spec-version matrix axis
felixweinberger Jun 11, 2026
e12218f
fix(client): satisfy the docs build for the negotiation module
felixweinberger Jun 15, 2026
0a6044c
refactor(core): make the negotiated protocol version a protected Prot…
felixweinberger Jun 15, 2026
fa5230a
docs(client): slim comments in the version negotiation modules
felixweinberger Jun 15, 2026
07a19a4
refactor(client): drop speculative negotiation surface
felixweinberger Jun 15, 2026
393f5b9
fix(client): restore the transport's start() when negotiation fails
felixweinberger Jun 15, 2026
59874c0
docs: clarify that server-side discover serving lands with the server…
felixweinberger Jun 15, 2026
79631bb
fix(client): keep 2026-era revisions out of the legacy initialize fal…
felixweinberger Jun 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/add-version-negotiation-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@modelcontextprotocol/client': minor
'@modelcontextprotocol/core': minor
---

Add opt-in protocol version negotiation on `ClientOptions.versionNegotiation`. The default is unchanged: without the option (or with `mode: 'legacy'`) the client performs today's 2025 connect sequence byte-identically. `mode: 'auto'` probes the server with `server/discover` at
connect time and conservatively falls back to the plain legacy `initialize` handshake on the same connection unless the outcome is definitive modern evidence; a network outage rejects with a typed connect error, and a probe timeout is transport-aware — on stdio it indicates
a legacy server and falls back to `initialize` on the same stream, on HTTP it rejects with a typed timeout error.
`mode: { pin: '<version>' }` negotiates exactly the pinned modern revision with no fallback. Probe policy lives under `probe: { timeoutMs? }` — the probe inherits the standard request timeout. The probe's `MCP-Protocol-Version`/`Mcp-Method` headers derive from the probe
message body; the transport version slot is never touched during negotiation, so legacy-era traffic carries zero 2026 headers by construction. Adds the `SdkErrorCode.EraNegotiationFailed` code for negotiation-phase connect failures.
6 changes: 6 additions & 0 deletions .changeset/node-forward-supported-versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/node': patch
---

Forward `setSupportedProtocolVersions` from `NodeStreamableHTTPServerTransport` to the wrapped Web Standard transport. Previously a server's `supportedProtocolVersions` option never reached the Node adapter's `MCP-Protocol-Version` header validation, which silently kept
validating against the default version list.
11 changes: 11 additions & 0 deletions .changeset/wire-server-discover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/server': minor
'@modelcontextprotocol/client': minor
---

Wire `server/discover` (protocol revision 2026-07-28) into the typed request funnel and serve it era-aware. The request joins `ClientRequestSchema`/`ServerResultSchema`/`ResultTypeMap` (per-era availability stays with the wire registries: only the 2026-era registry serves
it), and `Client.discover()` issues it as a typed request on 2026-era connections. A `Server` whose `supportedProtocolVersions` list carries a modern (2026-07-28+) revision installs the `server/discover` handler, advertising ONLY its modern revisions and excluding the
listChanged/subscribe-class capabilities until the `subscriptions/listen` flow ships; servers with today's default list are unchanged and keep answering `-32601`. The `initialize` handshake is now era-aware in the other direction: its accept check and counter-offer consult
only the legacy subset of the supported versions — a 2026-era revision is never negotiated via `initialize` — so a 2025-era client can never be offered a 2026 version string; with the default list this is byte-identical to previous behavior. Serving the 2026 revision to
ordinary HTTP/stdio traffic arrives with an upcoming server-side entry point: today the negotiation surface is client-side, and `mode: 'auto'` falls back cleanly against current SDK servers.
46 changes: 46 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,52 @@ protocol.setRequestHandler(

## Enhancements

### Opt-in protocol version negotiation (2026-07-28 draft)

The client can now negotiate the protocol era at connect time. This is **opt-in**: if you do nothing, `connect()` performs exactly the same 2025 `initialize` handshake as before, byte for byte.

```typescript
import { Client } from '@modelcontextprotocol/client';

// Auto-negotiate: try the 2026-07-28 draft revision, fall back to the 2025
// handshake automatically when the server is a 2025-era deployment.
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{ versionNegotiation: { mode: 'auto' } }
);
await client.connect(transport);

client.getNegotiatedProtocolVersion(); // e.g. '2026-07-28' or '2025-11-25'
```

How the modes behave:

- **absent / `mode: 'legacy'`** (default): today's behavior, unchanged. No probe, no new headers.
- **`mode: 'auto'`**: `connect()` first sends a single `server/discover` probe. A modern server answers it and no `initialize` is sent; a 2025-era server rejects it (deployed servers answer fast, e.g. `-32601` or a `400`), and the client falls back to the plain legacy
handshake **on the same connection** — byte-equivalent to a 2025 client, including the `initialize` body version and with zero 2026 headers. The probe costs one round trip against an old server and nothing else.
- **`mode: { pin: '2026-07-28' }`**: modern era at exactly that revision. No fallback — if the server does not offer the pinned version, `connect()` rejects with a typed error. Use `pin` where a silent downgrade would be worse than an error (tests, CI, servers you control).

Failure semantics under `'auto'` are deliberately conservative but never silent about infrastructure problems: anything the probe does not positively recognize as modern falls back to the legacy era, while a network outage rejects with a typed connect error (`SdkError`
with `EraNegotiationFailed`). A probe timeout is transport-aware, following the specification's backward-compatibility rules: on **stdio**, a server that does not answer the probe within the timeout is treated as a legacy server (some legacy servers never respond to unknown
pre-`initialize` requests at all) and the client falls back to `initialize` on the same stream; on **HTTP**, where a deployed server answers and silence means an outage, the timeout rejects with a typed `RequestTimeout` error — a dead HTTP server is never misreported as a
legacy server. One browser-specific exception: an opaque CORS/preflight `TypeError` during the probe falls back to the legacy era, because deployed 2025 servers commonly have CORS allow-lists that predate the 2026 headers and the legacy handshake sends none of them.

Probe policy is configured under `versionNegotiation.probe`:

```typescript
versionNegotiation: {
mode: 'auto',
probe: {
timeoutMs: 10_000 // default: the standard request timeout
}
}
```

On the server side, a `Server`/`McpServer` whose `supportedProtocolVersions` list includes a 2026-era revision installs a `server/discover` handler, advertising only its modern revisions; servers with the default version list are byte-identical to before (they keep
answering `-32601`, and the `initialize` handshake only ever negotiates 2025-era versions — a 2026-era revision is never accepted or counter-offered there). Note that serving the 2026 revision to ordinary HTTP/stdio traffic arrives with an upcoming server-side entry
point: today the negotiation surface is client-side, and `mode: 'auto'` falls back cleanly against current SDK servers. The client can also issue the request directly via `client.discover()` on a 2026-era connection — though a full typed round-trip against an SDK
server additionally needs the per-request envelope support that lands with that server entry — while on a 2025-era connection the method is rejected locally with a typed error, since it does not exist on that protocol revision.

### Automatic JSON Schema validator selection by runtime

The SDK now automatically selects the appropriate JSON Schema validator based on your runtime environment:
Expand Down
Loading
Loading