Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
a941791
refactor(core): reduce the protocol-layer inbound consult to a drop-o…
felixweinberger Jun 17, 2026
104927e
feat(server): add the connection-pinned serveStdio entry; remove Serv…
felixweinberger Jun 17, 2026
e04bdb2
test: migrate dual-era stdio examples and coverage to serveStdio
felixweinberger Jun 17, 2026
15fa66d
docs: document serveStdio and the eraSupport removal
felixweinberger Jun 17, 2026
dcf1ac0
fix(server): answer the opening request when serveStdio fails to buil…
felixweinberger Jun 17, 2026
9ef7fe5
fix(server): keep the serveStdio negotiation window open across repea…
felixweinberger Jun 17, 2026
064e88f
fix(server): deliver the probe answer before serveStdio discards the …
felixweinberger Jun 17, 2026
b450d45
refactor(core): rename the protocol inbound consult to _shouldDropInb…
felixweinberger Jun 17, 2026
85273ff
test(server): pin the outbound era gate for handlers on a modern-pinn…
felixweinberger Jun 17, 2026
e70d007
docs: split the deprecated-accessor behavior on 2026-pinned serveStdi…
felixweinberger Jun 17, 2026
d4be53a
fix(server): keep a serveStdio connection closed when a close races t…
felixweinberger Jun 18, 2026
0a09fd1
fix(server): do not let an enveloped notification commit the serveStd…
felixweinberger Jun 18, 2026
23866bf
docs: cover serveStdio in the McpServerFactory and McpRequestContext …
felixweinberger Jun 18, 2026
caf8c9d
docs: frame the stdio migration entries for readers coming from v1
felixweinberger Jun 18, 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
9 changes: 0 additions & 9 deletions .changeset/server-era-support.md

This file was deleted.

11 changes: 11 additions & 0 deletions .changeset/server-serve-stdio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@modelcontextprotocol/server': minor
---

Add `serveStdio(factory, options?)` (exported from `@modelcontextprotocol/server/stdio`), the connection-pinned stdio entry point for serving the 2026-07-28 draft revision on long-lived connections. The entry owns the transport and the era decision: the client's opening
exchange selects the era (a 2025 `initialize` handshake, 2026-07-28 per-request `_meta` envelope traffic, or a `server/discover` probe followed by either), and ONE instance from the factory is pinned to the connection and serves only that era — mirroring how
`createMcpHandler` classifies each HTTP request before constructing an instance. 2025-era openings are served by default; `legacy: 'reject'` answers them with the unsupported-protocol-version error naming the supported modern revisions instead. A `transport` option
accepts a bring-your-own `StdioServerTransport` (for example over a Unix domain socket); `onerror` reports out-of-band errors; the returned handle's `close()` tears the connection down.

Removed: `ServerOptions.eraSupport` (introduced in an earlier 2.0 alpha, never in a stable release). A hand-constructed `Server`/`McpServer` serves only the 2025-era protocol it was written for; serving the 2026-07-28 revision always goes through a serving entry. Migrate
`new McpServer(info, { eraSupport: 'dual-era' })` + `connect(new StdioServerTransport())` to `serveStdio(() => new McpServer(info))`, and `eraSupport: 'modern'` to `serveStdio(factory, { legacy: 'reject' })`.
11 changes: 6 additions & 5 deletions docs/migration-SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -552,11 +552,12 @@ These can require code changes:

### Server (stdio / long-lived connections)

- `ServerOptions.eraSupport?: 'legacy' | 'dual-era' | 'modern'` declares which protocol eras a hand-constructed `Server`/`McpServer` serves on its long-lived connection. Default `'legacy'` = today's behavior, byte-identical: do not add the option during a mechanical migration.
- Serving the 2026-07-28 draft revision on stdio is the explicit opt-in `new McpServer(info, { eraSupport: 'dual-era' })` with an unchanged `connect(new StdioServerTransport())`. `'modern'` is strict 2026-only (envelope-less requests, including `initialize`, get the
unsupported-protocol-version error).
- A 2026-era revision in `supportedProtocolVersions` now requires `eraSupport: 'dual-era' | 'modern'`; on a default (`'legacy'`) instance it throws a `TypeError` at construction (previously it silently installed the `server/discover` handler).
- On dual-era instances `getClientCapabilities()` / `getClientVersion()` / `getNegotiatedProtocolVersion()` keep `initialize`-scoped semantics and are never backfilled from 2026-era requests; handlers read per-request identity from `ctx.mcpReq.envelope`.
- A hand-constructed `Server`/`McpServer` connected to a `StdioServerTransport` serves only the 2025-era protocol it was written for: today's behavior, byte-identical — no change required during a mechanical migration.
- Serving the 2026-07-28 draft revision (or both eras) on stdio goes through the connection-pinned entry: `serveStdio(() => new McpServer(info, options))` from `@modelcontextprotocol/server/stdio`. The opening exchange selects the connection's era (2025 `initialize` vs
2026 per-request envelope, with `server/discover` answered as a probe); one factory instance is pinned per connection. There is no per-instance option that makes a hand-constructed server serve the 2026 revision: move the v1 `server.connect(new StdioServerTransport())`
call into `serveStdio(() => buildServer())`. `serveStdio(factory, { legacy: 'reject' })` refuses 2025-era openings with the unsupported-protocol-version error.
- On 2026-pinned stdio connections `getClientCapabilities()` / `getClientVersion()` return `undefined` (no `initialize` ever runs there) and handlers read per-request identity from `ctx.mcpReq.envelope`; `getNegotiatedProtocolVersion()` reports the pinned revision
(`2026-07-28`), as on instances served through `createMcpHandler`. 2025-pinned connections keep the `initialize`-scoped semantics for all three accessors.
- A client whose connection negotiated a modern era drops inbound server→client JSON-RPC requests (the 2026 era has no such channel) instead of answering them; legacy-era connections are unchanged.

## 14. Runtime-Specific JSON Schema Validators (Enhancement)
Expand Down
56 changes: 30 additions & 26 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1025,9 +1025,9 @@ versionNegotiation: {
}
```

On the server side, a `Server`/`McpServer` serves `server/discover` (advertising only its modern revisions) when it declares modern-era support via the `eraSupport` option (see the stdio section below); servers constructed without it 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). Serving the 2026 revision to ordinary HTTP traffic is done with the `createMcpHandler` entry point described in the
next section; serving it on stdio (and other long-lived connections) is the `eraSupport` server option described after that. The client can also issue the request directly via `client.discover()` on a 2026-era connection — a full typed round trip needs each request to carry the per-request `_meta` envelope (the negotiation probe
On the server side, `server/discover` (advertising only the modern revisions) is served by instances hosted through one of the 2026-era serving entries; a hand-constructed `Server`/`McpServer` is byte-identical to before (it keeps answering `-32601`, and the `initialize`
handshake only ever negotiates 2025-era versions — a 2026-era revision is never accepted or counter-offered there). Serving the 2026 revision to ordinary HTTP traffic is done with the `createMcpHandler` entry point described in the next section; serving it on stdio (and
other long-lived connections) is the `serveStdio` entry point described after that. The client can also issue the request directly via `client.discover()` on a 2026-era connection — a full typed round trip needs each request to carry the per-request `_meta` envelope (the negotiation probe
already does; automatic envelope emission for every request is a client-side follow-up) — while on a 2025-era connection the method is rejected locally with a typed error, since it does not exist on that protocol revision.

### Serving the 2026-07-28 draft revision over HTTP: `createMcpHandler`
Expand Down Expand Up @@ -1068,38 +1068,41 @@ The entry performs no Origin/Host validation (see the origin-validation middlewa
request headers. Power users who want to compose routing themselves can use the exported `classifyInboundRequest` and `PerRequestHTTPServerTransport` building blocks directly; the handler faces are bound properties, so they can be detached and passed around
(`const { fetch } = handler`).

### Serving the 2026-07-28 draft revision on stdio: `eraSupport`
### Serving the 2026-07-28 draft revision on stdio: `serveStdio`

A hand-constructed `Server`/`McpServer` — the shape every stdio server has — now takes an `eraSupport` option declaring which protocol eras it serves on its long-lived connection. **The default is `'legacy'`: if you do nothing, your server keeps speaking exactly the
2025-era protocol it was written for** — the `initialize` handshake, the same wire bytes, no `server/discover`, nothing new advertised — and upgrading the SDK changes nothing about what it puts on the wire.

Serving the 2026-07-28 draft revision is one explicit option; the transport stays unchanged:
The server package ships a stdio entry point that mirrors `createMcpHandler` for long-lived connections: the entry owns the transport and the era decision, the client's opening exchange selects the era for the connection, and ONE instance from your factory is pinned to
that connection and serves only that era.

```typescript
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import { serveStdio } from '@modelcontextprotocol/server/stdio';

const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { eraSupport: 'dual-era' });
await server.connect(new StdioServerTransport());
serveStdio(() => {
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
// register tools/resources/prompts once — the same factory serves both eras
return server;
});
```

What the values mean:
How the connection's era is decided:

- **`'legacy'` (default)** — today's behavior, unchanged: 2025-era serving negotiated via `initialize`. `server/discover` is not registered or advertised. Declaring a 2026-era revision in `supportedProtocolVersions` without changing `eraSupport` is now a construction-time
`TypeError` (previously it silently installed the discover handler) — serving the new revision is always an explicit declaration, never a side effect of a version list.
- **`'dual-era'`** — both eras on the same connection, selected per message: plain 2025 clients keep using `initialize` and are served exactly as before, while 2026-capable clients negotiate via `server/discover` on the same pipe and every request carrying the per-request
`_meta` envelope is served on the modern era. Methods that exist in only one era stay invisible to the other: a 2025-era client asking for a 2026-only method (such as `server/discover` without an envelope) gets the same plain `-32601` a 2025 server would send, and a
2026-era request for a removed method (such as `logging/setLevel`) gets `-32601` too.
- **`'modern'`** — strict 2026-only: requests without the per-request envelope (including `initialize`) are answered with the unsupported-protocol-version error naming the supported revisions; legacy-era notifications are dropped.
- A plain 2025 client opens with the `initialize` handshake (or any request without the per-request `_meta` envelope): the connection is pinned to a 2025-era instance and served exactly as a hand-wired stdio server serves it today. Pass `legacy: 'reject'` to refuse
2025-era openings instead — they are answered with the unsupported-protocol-version error naming the supported modern revisions, and there is no silent 2025 serving.
- A 2026-capable client opens with requests carrying the per-request `_meta` envelope: the connection is pinned to a 2026-era instance.
- A `server/discover` probe is answered (from an instance built with your factory, so the advertisement reflects your real server definition) without pinning the connection: the client either continues with enveloped modern requests — pinning the connection to the 2026
era — or falls back to `initialize` when it shares no modern revision with the advertisement, in which case the probe instance is discarded and a fresh 2025-era instance serves the handshake. Once the modern era is pinned, a later `initialize` is rejected with the
unsupported-protocol-version error naming the supported revisions.

Declaring `'dual-era'` or `'modern'` automatically adds the SDK's supported modern revisions to `supportedProtocolVersions`, and `'modern'` serves only those: a strict instance's supported list (what `server/discover` advertises and version-mismatch errors name) is modern-only.
Because the entry may construct an instance for a probe that is later discarded (and `createMcpHandler` constructs one per request), factories should be cheap and side-effect-free. Bring your own transport with the `transport` option (for example a
`StdioServerTransport` over a Unix domain socket or TCP stream); by default the entry serves the current process's stdio. The returned handle's `close()` tears down the pinned instance and the transport.

Directionality follows the era of the traffic: the 2026-07-28 revision has no server→client JSON-RPC request channel, so a `'modern'` instance cannot emit `sampling`/`elicitation`/`roots` wire requests (they fail locally with a typed error), while a `'dual-era'` instance
can still send them to the 2025-era clients it serves via `initialize`. On a `'dual-era'` instance the same local typed error applies per request: a handler that is serving a 2026-era request cannot send server→client requests through its request context
(`ctx.mcpReq.send`, `ctx.mcpReq.elicitInput`, `ctx.mcpReq.requestSampling`) — only handlers serving 2025-era requests can. Symmetrically, a client whose connection negotiated a modern era drops inbound JSON-RPC requests instead of answering them.
Directionality follows the connection's era: the 2026-07-28 revision has no server→client JSON-RPC request channel, so handlers serving a 2026-pinned connection cannot emit `sampling`/`elicitation`/`roots` wire requests (they fail locally with a typed error), while a
2025-pinned connection keeps today's behavior. Symmetrically, a client whose connection negotiated a modern era drops inbound JSON-RPC requests instead of answering them.

Declaring `eraSupport: 'dual-era'` is also an assertion that your handlers are ready to serve modern-era requests (for example, that they read per-request client identity from `ctx.mcpReq.envelope` rather than the connection-scoped accessors — see the next section). A
future release may add per-handler era declarations as the basis for a safe automatic default; for now the connection-level `eraSupport` option is the whole opt-in surface.
**The v1 stdio pattern keeps working and stays 2025-only.** A hand-constructed `Server`/`McpServer` connected directly to a `StdioServerTransport` — the way every v1 stdio server is written — still works and serves only the 2025-era protocol it was written for: upgrading
the SDK changes nothing about what it puts on the wire, and no per-instance option turns such a server into a 2026-era server. Serving the 2026-07-28 revision (or both eras) on stdio always goes through `serveStdio`. To migrate an existing v1 stdio server, move its
construction into the factory: replace `await server.connect(new StdioServerTransport())` with `serveStdio(() => buildServer())`, registering tools/resources/prompts inside the factory as before — and pass `{ legacy: 'reject' }` if 2025-era clients should be refused
instead of served.

### Cache fields and cache hints for cacheable 2026-07-28 results

Expand Down Expand Up @@ -1137,8 +1140,9 @@ capabilities, and `ProtocolError.fromError` recognizes the code/data shape (reco
handlers as `ctx.mcpReq.envelope`; instances serving that revision through `createMcpHandler` are backfilled per request, so existing code that calls the accessors keeps working on both eras. On 2025-era connections the accessors keep returning the `initialize`-scoped
values, as before.

On a long-lived dual-era instance (`eraSupport: 'dual-era'`, e.g. a stdio server) the accessors are **not** backfilled from modern requests: 2025-era and 2026-era messages interleave on one connection, so instance-level backfill would race. There the accessors keep their
`initialize`-scoped semantics — they reflect what the legacy handshake negotiated (or `undefined` when none ran) — and handlers serving 2026-era requests read the per-request identity from `ctx.mcpReq.envelope`.
On a connection pinned to the 2026-07-28 era by `serveStdio` the identity accessors are **not** backfilled: the modern era carries client identity per request, so connection-scoped identity has nothing stable to report there.
`getClientCapabilities()` and `getClientVersion()` return `undefined` (no `initialize` handshake ever ran on such a connection) and handlers read the per-request identity from `ctx.mcpReq.envelope`. `getNegotiatedProtocolVersion()` reports the pinned revision
(`2026-07-28`) — the entry era-marks the instance when it binds it, so the accessor reports the same value as on instances serving that revision through `createMcpHandler`. On 2025-pinned connections the accessors keep their `initialize`-scoped semantics, as before.

### Origin validation middleware and default arming

Expand Down
19 changes: 12 additions & 7 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,22 @@ await server.connect(transport);

#### Serving the 2026-07-28 draft revision on stdio

By default a stdio server speaks the 2025-era protocol it was written for (`eraSupport: 'legacy'`): nothing about its wire behavior changes when you upgrade the SDK. Serving the 2026-07-28 draft revision is one explicit option — the transport stays unchanged:
A hand-constructed stdio server speaks the 2025-era protocol it was written for: nothing about its wire behavior changes when you upgrade the SDK. Serving the 2026-07-28 draft revision goes through the connection-pinned `serveStdio` entry, which mirrors `createMcpHandler`
for long-lived connections — the entry owns the transport and the era decision, and one instance from your factory serves the era the client opened the connection with:

```typescript
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { eraSupport: 'dual-era' });
await server.connect(new StdioServerTransport());
import { serveStdio } from '@modelcontextprotocol/server/stdio';

serveStdio(() => {
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
// register tools/resources/prompts once — the same factory serves both eras
return server;
});
```

With `eraSupport: 'dual-era'` the same long-lived connection serves both eras, selected per message: plain 2025 clients keep using `initialize` and are served exactly as before, while 2026-capable clients negotiate via `server/discover` and send each request with the
per-request `_meta` envelope. Methods that exist in only one era stay invisible to the other (a 2025-era client asking for a 2026-only method gets a plain `-32601`). `eraSupport: 'modern'` is strict 2026-only. On dual-era instances, read per-request client identity from
`ctx.mcpReq.envelope` in your handlers rather than the connection-scoped accessors (see the [migration guide](./migration.md) for details). A runnable example lives at `examples/server/src/dualEraStdio.ts`, with a two-legged client at
`examples/client/src/dualEraStdioClient.ts`.
Plain 2025 clients open with `initialize` and are served exactly as before; 2026-capable clients negotiate via `server/discover` and send each request with the per-request `_meta` envelope, and their connection is pinned to a 2026-era instance. Pass `legacy: 'reject'` to
refuse 2025-era openings with the unsupported-protocol-version error. On 2026-pinned connections, read per-request client identity from `ctx.mcpReq.envelope` in your handlers rather than the connection-scoped accessors (see the [migration guide](./migration.md) for
details). A runnable example lives at `examples/server/src/dualEraStdio.ts`, with a two-legged client at `examples/client/src/dualEraStdioClient.ts`.

## Server instructions

Expand Down
Loading
Loading