You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Remove the Posture setting; always serve both protocol eras
Delete the Posture enum, its export, and the posture= constructor
parameter on Server and MCPServer. The lowlevel Server is a registry
of handlers and carries no serving configuration; a server offers both
the 2025 handshake era and the 2026 per-request era on every transport,
and each stream connection's era is decided by the client's opening
message, as the default already did.
_StreamConnection no longer takes a starting era; it starts undecided
and pins on the first era-distinctive message. The streamable-HTTP
manager's header routing is unchanged: streams it has already routed
to the handshake era are entered born-legacy through the transport-
internal serve_legacy_stream, so the legacy leg keeps refusing
enveloped requests with -32600 exactly as before.
The MODERN_ONLY / LEGACY_ONLY tests and doc sections go with it, and
the streamable-HTTP orphan test file is renamed now that its posture
half is gone.
Copy file name to clipboardExpand all lines: docs/advanced/low-level-server.md
+1-4Lines changed: 1 addition & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -223,10 +223,7 @@ async with server.lifespan() as state, anyio.create_task_group() as tg:
223
223
224
224
The lifespan is entered **outside** the task group on purpose: on the way out the task group joins the still-running connection tasks first and only then does the lifespan tear down, so the shared pool outlives every connection using it (the other order tears the pool down while connections are still being served).
225
225
226
-
Two things about the shared-`Server` shape that the single-connection rungs never surface:
227
-
228
-
* Which protocol eras a server offers is `Server(posture=...)` (`Posture.DUAL` by default, `Posture.MODERN_ONLY`, `Posture.LEGACY_ONLY`); it rides along on the server object, so none of these drivers takes a posture argument you could forget.
229
-
* The cross-connection listen behaviour called out under `serve_listener` above applies to any shared `Server`, including this rung; the per-connection-`Server` recipe there is how you sidestep it.
226
+
One thing about the shared-`Server` shape that the single-connection rungs never surface: the cross-connection listen behaviour called out under `serve_listener` above applies to any shared `Server`, including this rung; the per-connection-`Server` recipe there is how you sidestep it.
230
227
231
228
An `MCPServer` reaches every one of these through `mcp.lowlevel_server`, and `mcp.close_subscriptions()` (or `Server.close_subscriptions()` down here) ends the open listen streams gracefully from the server's side.
Copy file name to clipboardExpand all lines: docs/migration.md
+1-11Lines changed: 1 addition & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1363,17 +1363,7 @@ The concrete dispatcher's `inline_methods=` constructor keyword is also gone: th
1363
1363
1364
1364
`server/discover` no longer pins the connection to the modern era: it is a probe, answered with modern semantics, and the era is decided by the first non-probe message. A client whose discover probe was answered can therefore still fall back to `initialize` on the same connection, and `subscriptions/listen` is now served over stdio and every other duplex stream (previously refused with `-32601`).
1365
1365
1366
-
A stray leading notification (for example a client's `notifications/roots/list_changed`) no longer decides the era: only `initialize` (or a bare pre-handshake request) opens the legacy era, only `notifications/initialized` opens it among notifications, and any other early notification is ignored, so a 2026 client is never pinned to the legacy era by a courtesy frame it sent first. A legacy-committed connection also now refuses a request that carries the modern envelope with `-32600` instead of processing it under legacy semantics, and `initialize` is answered `-32022` (naming the modern versions the server serves) whenever it reaches the modern side, on every transport.
1367
-
1368
-
The new `posture` constructor parameter on `Server` and `MCPServer` narrows which eras a server offers, on stream transports and streamable HTTP alike:
1369
-
1370
-
```python
1371
-
from mcp.server import Posture, Server
1372
-
1373
-
server = Server("app", posture=Posture.MODERN_ONLY) # or Posture.LEGACY_ONLY; Posture.DUAL is the default
1374
-
```
1375
-
1376
-
A `MODERN_ONLY` server answers a 2025 handshake with `-32022` and its supported versions (on stdio and streamable HTTP); a `LEGACY_ONLY` server refuses envelope-bearing requests in legacy vocabulary and never enters the modern era. Posture is the one place a server restricts its eras; the walkthrough is in [Serving one era only](run/legacy-clients.md#serving-one-era-only).
1366
+
A stray leading notification (for example a client's `notifications/roots/list_changed`) no longer decides the era: only `initialize` (or a bare pre-handshake request) opens the legacy era, only `notifications/initialized` opens it among notifications, and any other early notification is ignored, so a 2026 client is never pinned to the legacy era by a courtesy frame it sent first. A legacy-committed connection also now refuses a request that carries the modern envelope with `-32600` instead of processing it under legacy semantics, and `initialize` is answered `-32022` (naming the modern versions the server serves) whenever it reaches the modern side, on every transport. Every server offers both eras on every transport; there is no era switch on `Server` or `MCPServer`.
1377
1367
1378
1368
At stdin EOF a stdio server now winds down gracefully: it gives in-flight requests a short bounded window to finish, ends every open `subscriptions/listen` stream with its empty `SubscriptionsListenResult` (the spec's graceful closure), and then writes nothing further onto a stdout nobody reads. In particular, an in-flight request no longer receives a `CONNECTION_CLOSED` error at EOF, since the peer that would read it is gone.
Copy file name to clipboardExpand all lines: docs/run/legacy-clients.md
+6-25Lines changed: 6 additions & 25 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,14 +9,11 @@ The SDK routes every request by its `MCP-Protocol-Version` header. A request nam
9
9
So a legacy client is not something you build *for*. It is something that connects *to* the server you already wrote. You configure nothing.
10
10
11
11
!!! note
12
-
Nothing on the transport, literally. There is no `legacy=` option and no version
13
-
allowlist on `streamable_http_app()`, `run()`, or the session manager; by default both
14
-
eras are always on, and the version header routes each request to the right one. The one
15
-
place a server can restrict eras is its own posture:
16
-
`MCPServer(name, posture=Posture.LEGACY_ONLY)` (or `MODERN_ONLY`) is a constructor
17
-
property every transport honours (see **[Serving one era only](#serving-one-era-only)**),
18
-
and the default `DUAL` is this page's assumption. The nearest thing to a per-request
19
-
switch in the HTTP signature is `stateless_http`, and it is most of this page.
12
+
Nothing on the transport, and nothing on the server either. There is no `legacy=` option
13
+
and no version allowlist on `MCPServer`, `Server`, `streamable_http_app()`, `run()`, or the
14
+
session manager; both eras are always on, and the version header routes each request to
15
+
the right one. The nearest thing to a per-request switch in the HTTP signature is
16
+
`stateless_http`, and it is most of this page.
20
17
21
18
## One handler, both eras
22
19
@@ -114,25 +111,9 @@ Over HTTP, neither call reaches the other era's clients. To tell everyone, call
114
111
115
112
Two lines, no `if`, no version check, and you are done. That is the entire list of things a handler does differently because a legacy client exists.
116
113
117
-
## Serving one era only
118
-
119
-
Everything above assumes the default: your server offers both eras. That default is a constructor property called the server's **posture**, and it is the one place you narrow it:
`Posture.DUAL` is the default and the whole rest of this page. `Posture.MODERN_ONLY` retires the handshake era: a pre-2026 client's `initialize` is answered with a `-32022` error naming the modern versions the server does speak, so a well-behaved client learns to go modern rather than being silently dropped. `Posture.LEGACY_ONLY` is the mirror image, a server that speaks only the `initialize` handshake and refuses requests carrying the modern envelope. The low-level `Server` takes the same argument.
129
-
130
-
Posture lives on the server object, not on any driver, so `run()`, `streamable_http_app()`, and the stream drivers all honour the same declaration and there is no per-transport switch to keep in step. Reach for it deliberately: `MODERN_ONLY` turns away every deployed pre-2026 client, and `LEGACY_ONLY` turns away 2026 ones, so `DUAL` is what you want unless you know exactly who connects.
131
-
132
114
## Recap
133
115
134
-
* One `streamable_http_app()` serves both protocol eras. The SDK routes each request by its `MCP-Protocol-Version` header; there is nothing to configure on the transport and no per-request era knob to look for.
135
-
* The one era switch is the server's posture: `MCPServer(name, posture=Posture.MODERN_ONLY)` or `LEGACY_ONLY`, a constructor property every transport honours. The default `DUAL` is what this whole page assumes.
116
+
* One `streamable_http_app()` (and one stdio server) serves both protocol eras, always. The SDK routes each request by its `MCP-Protocol-Version` header, and a stream connection's era is decided by the client's opening message; there is nothing to configure on the transport or the server and no era switch to look for.
136
117
* A legacy client costs you a session: an in-process `Mcp-Session-Id` record with no distributed store behind it. More than one worker means **sticky routing**, or the wrong worker answers `404 Session not found`. **[Deploy & scale](deploy.md)** has the multi-worker story.
137
118
*`stateless_http=True` is the one knob, and it is **legacy-leg-only**. It buys free load balancing for legacy clients at the price of both server-to-client channels on that leg: server-initiated requests raise `NoBackChannelError` (a top-level error at the client, not an `is_error` result), and notifications are dropped.
138
119
* A `2026-07-28` connection is sessionless either way. `stateless_http` never touches it.
Copy file name to clipboardExpand all lines: docs/whats-new.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -153,7 +153,7 @@ Each of these is a section in the **[Migration Guide](migration.md)**:
153
153
154
154
## The protocol: 2025-11-25 to 2026-07-28
155
155
156
-
v2 implements the 2026-07-28 revision, and it serves **both** revisions at once: the same `streamable_http_app()` (and the same stdio server) answers a 2025-era client's `initialize` and a 2026-era client's requests with nothing to configure, no flag to flip, and no separate deployment. Serving the new revision does not strand a client on the old one. If you want to serve only one revision, that is a single constructor property, `Server(posture=...)` / `MCPServer(posture=...)`, honoured by every transport; see [Serving one era only](run/legacy-clients.md#serving-one-era-only). What follows is what the new revision itself changes.
156
+
v2 implements the 2026-07-28 revision, and it serves **both** revisions at once: the same `streamable_http_app()` (and the same stdio server) answers a 2025-era client's `initialize` and a 2026-era client's requests with nothing to configure, no flag to flip, and no separate deployment. Serving the new revision does not strand a client on the old one. What follows is what the new revision itself changes.
0 commit comments