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
Copy file name to clipboardExpand all lines: docs/migration.md
+8-5Lines changed: 8 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -749,7 +749,7 @@ Transport-specific parameters have been moved off the `MCPServer` constructor an
749
749
-`host`, `port` - HTTP server binding, on `run()` only. The app factories have no `port` (`streamable_http_app(port=...)` raises `TypeError`; a mounted app binds wherever the outer ASGI server does) but do take `host` (default `"127.0.0.1"`), used only to decide whether DNS rebinding protection auto-enables (see the note below)
750
750
-`sse_path`, `message_path` - SSE transport paths, on `run(transport="sse", ...)` and `sse_app()`
751
751
-`streamable_http_path` - StreamableHTTP endpoint path, on `run(transport="streamable-http", ...)` and `streamable_http_app()`
752
-
-`json_response`, `stateless_http` - StreamableHTTP behavior, same two places
752
+
-`json_response`, `stateless_http` - StreamableHTTP behavior, same two places; each also removes a server-to-client channel, see [Server-initiated sampling, elicitation, and roots raise `NoBackChannelError`](#server-initiated-sampling-elicitation-and-roots-raise-nobackchannelerror)
753
753
-`max_request_body_size` - StreamableHTTP request-body limit, same two places
754
754
-`event_store`, `retry_interval` - StreamableHTTP event handling, same two places
755
755
-`transport_security` - DNS rebinding protection, on `run()` for both HTTP transports and on both app methods
@@ -963,8 +963,11 @@ enforce the spec's egress rule: an undeclared capability (form-mode `elicitation
963
963
or `tool_choice`) fails the call with a `-32021`
964
964
`MISSING_REQUIRED_CLIENT_CAPABILITY` JSON-RPC error instead of sending a
965
965
request the client cannot handle. This applies on 2025-11-25 sessions with a
966
-
live back-channel too; a session with no back-channel keeps failing with its
967
-
no-back-channel error. To migrate, declare the capability: the SDK client
966
+
live back-channel too; a pre-`2026-07-28` session with no back-channel
967
+
(stateless HTTP, or streamable HTTP with `json_response=True`) keeps failing
968
+
with its no-back-channel error. At `2026-07-28` a resolver never uses a
969
+
back-channel — it answers with an `InputRequiredResult` — so the `-32021`
970
+
check applies there unconditionally. To migrate, declare the capability: the SDK client
968
971
declares `elicitation`, `sampling`, and `roots` when the matching callback is
969
972
set, and `sampling.tools` needs an explicit
970
973
`Client(sampling_capabilities=SamplingCapability(tools=...))`. Direct
@@ -1617,7 +1620,7 @@ Nothing changes for callers of the built-in client: abandoning a call (cancellin
1617
1620
1618
1621
The `stateless: bool` parameter on the lowlevel `Server.run()` has been removed. Stateless serving is now a property of how the connection is constructed (the streamable-HTTP manager builds a born-ready `Connection` per request), not a flag the loop driver inspects.
1619
1622
1620
-
Server-initiated requests that have no channel to travel on — a legacy session against a `stateless_http=True` server, or any connection negotiated at 2026-07-28 — now raise `NoBackChannelError` instead of stalling as they did in v1 (the transport silently dropped the outbound message), so a stateless-HTTP `ctx.elicit()` that used to hang now fails fast; see [Server-initiated sampling, elicitation, and roots raise `NoBackChannelError`](#server-initiated-sampling-elicitation-and-roots-raise-nobackchannelerror) for the exception and the migration paths.
1623
+
Server-initiated requests that have no channel to travel on — a legacy session against a `stateless_http=True` server, the request-scoped channel of a stateful legacy session against a `json_response=True` server, or any connection negotiated at 2026-07-28 — now raise `NoBackChannelError` instead of stalling as they did in v1 (the transport silently dropped the outbound message), so a stateless-HTTP or JSON-mode`ctx.elicit()` that used to hang now fails fast; see [Server-initiated sampling, elicitation, and roots raise `NoBackChannelError`](#server-initiated-sampling-elicitation-and-roots-raise-nobackchannelerror) for the exception and the migration paths.
@@ -2801,7 +2804,7 @@ rest of this guide stays focused on the v1-to-v2 upgrade itself.
2801
2804
2802
2805
The 2026-07-28 protocol has no server-initiated requests, so a handler that reaches back to the client mid-request — `ctx.elicit()`, `ctx.elicit_url()`, `ctx.session.create_message()`, `ctx.session.list_roots()`, or any other `ServerSession` request helper — raises `NoBackChannelError` on such a connection instead of sending. An in-process `Client(server)` negotiates 2026-07-28 by default (see [`Client` defaults to `mode='auto'`](#client-defaults-to-modeauto)), so the first smoke test of an unchanged v1 sampling or elicitation tool fails, and setting `sampling_callback=` / `elicitation_callback=` on the client changes nothing because no request ever reaches the client.
2803
2806
2804
-
`NoBackChannelError` lives in `mcp.shared.exceptions` and subclasses `MCPError` (code `-32600`, message `Cannot send '<method>': this transport context has no back-channel for server-initiated requests.`). Raised inside an `@mcp.tool()` it reaches the client as a top-level JSON-RPC error, not `CallToolResult(is_error=True)` — see [`MCPError` raised from an `@mcp.tool()` handler now surfaces as a JSON-RPC error](#mcperror-raised-from-an-mcptool-handler-now-surfaces-as-a-json-rpc-error) — and the [Troubleshooting](troubleshooting.md) page walks through the client-side traceback. The same exception is raised on a legacy session against a `stateless_http=True` server, where v1 dropped the message and stalled ([`Server.run()` no longer takes a `stateless` flag](#serverrun-no-longer-takes-a-stateless-flag)). Notifications never raise it: `send_log_message()`, `send_tool_list_changed()`, and the other notification helpers are dropped with a debug log where no channel exists, and `UrlElicitationRequiredError` from a tool is unaffected (it is an error response, not a request).
2807
+
`NoBackChannelError` lives in `mcp.shared.exceptions` and subclasses `MCPError` (code `-32600`, message `Cannot send '<method>': this transport context has no back-channel for server-initiated requests.`). Raised inside an `@mcp.tool()` it reaches the client as a top-level JSON-RPC error, not `CallToolResult(is_error=True)` — see [`MCPError` raised from an `@mcp.tool()` handler now surfaces as a JSON-RPC error](#mcperror-raised-from-an-mcptool-handler-now-surfaces-as-a-json-rpc-error) — and the [Troubleshooting](troubleshooting.md) page walks through the client-side traceback. The same exception is raised on a legacy session against a `stateless_http=True` server, and on the request-scoped channel of a stateful legacy session against a `json_response=True` server (a JSON body carries exactly one response, so a mid-request `ctx.elicit()` cannot ride it; the session's standalone `GET` stream still carries unrelated messages) — both places v1 dropped the message and stalled ([`Server.run()` no longer takes a `stateless` flag](#serverrun-no-longer-takes-a-stateless-flag)). Notifications never raise it: `send_log_message()`, `send_tool_list_changed()`, and the other notification helpers are dropped with a debug log where no channel exists, and `UrlElicitationRequiredError` from a tool is unaffected (it is an error response, not a request).
Copy file name to clipboardExpand all lines: docs/run/index.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
@@ -65,7 +65,7 @@ Each transport has its own keyword arguments, all on `run()`:
65
65
66
66
*`host` / `port`: where to listen. Defaults `127.0.0.1` and `8000`.
67
67
*`streamable_http_path`: where the MCP endpoint lives. Default `/mcp`.
68
-
*`json_response=True`: answer with plain JSON instead of an SSE stream.
68
+
*`json_response=True`: answer each POST with a single JSON body instead of an SSE stream. That body has room for the response and nothing else, so a tool that calls back into the client mid-request (`ctx.elicit()`, sampling) raises `NoBackChannelError` on this leg, and notifications tied to the in-flight call (progress from `ctx.report_progress()`, per-call log messages) are dropped; the standalone `GET` stream still carries unrelated ones.
69
69
*`stateless_http=True`: a fresh transport per request, no session tracking.
70
70
*`max_request_body_size`: largest accepted POST body in bytes. Defaults to 4 MiB; larger requests
71
71
receive HTTP 413 before parsing or session creation. Raise it only when legitimate MCP messages
Copy file name to clipboardExpand all lines: docs/run/legacy-clients.md
+7Lines changed: 7 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -72,6 +72,13 @@ Two things about it matter more than what it does.
72
72
73
73
**It costs both server-to-client channels on that leg.** A session that lives for one `POST` has no stream for the server to push a request down and no standalone stream for it to push notifications down. Every server-initiated request raises `NoBackChannelError`: `ctx.elicit()`, the retired sampling and roots calls (**[Deprecated features](../deprecated.md)**), and, yes, `Resolve` asking a *legacy* client its question. Notifications don't even get an error; they are silently dropped.
74
74
75
+
!!! note
76
+
`json_response=True` is not that knob, but it takes half the same cost on *every* legacy
77
+
session: a `POST` answered with one JSON body has no stream for the request-scoped channel,
78
+
so a mid-request `ctx.elicit()` raises the same `NoBackChannelError` and notifications tied to
79
+
the request are dropped. The session's standalone stream is untouched: unrelated notifications
80
+
still arrive.
81
+
75
82
!!! check
76
83
Do the wrong thing. `reserve` is the exact tool that just served both clients. Deploy it with
77
84
`stateless_http=True`, connect the same two clients over HTTP, and call it from each.
Copy file name to clipboardExpand all lines: docs/troubleshooting.md
+9-6Lines changed: 9 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -305,7 +305,7 @@ You see this one from `ctx.elicit()` on a legacy connection, and on any connecti
305
305
306
306
## `MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.`
307
307
308
-
Your handler tried to reach the client mid-request, on a connection where nothing can carry a request from the server. There are exactly two ways to be on one.
308
+
Your handler tried to reach the client mid-request, on a connection whose call has no channel that can carry a request from the server. There are three server configurations that put a call there.
309
309
310
310
**A `2026-07-28` connection: any transport, always.** The modern protocol has no server-initiated requests at all, so the server refuses before anything is sent. `ctx.elicit()` inside a tool is the classic way to meet this (on the very first in-memory test, since `Client(server)` negotiates `2026-07-28` without being asked), and passing `elicitation_callback=` changes nothing, because no request ever reaches the client for it to answer:
311
311
@@ -329,20 +329,23 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport
329
329
--8<--"docs_src/troubleshooting/tutorial008.py"
330
330
```
331
331
332
+
**A legacy connection on a `json_response=True` server.** The `POST` is answered with one JSON body, and one body carries only the response, so the request-scoped stream a mid-request `ctx.elicit()` needs does not exist here either. The session, its `Mcp-Session-Id`, and its standalone stream are all still there; only the request-scoped channel is gone.
333
+
332
334
The message names the method it could not send. `NoBackChannelError` is the class the server raises, but the wire carries only the base `MCPError`, so the sentence above is your traceback's last line, not the class name.
333
335
334
-
The fix is the same for both: don't reach back mid-call. Move the question into a **resolver** (or return an `InputRequiredResult` yourself) and it becomes part of the *response*, which every connection can carry:
336
+
For a `2026-07-28` client the fix is the same on all three: don't reach back mid-call. Move the question into a **resolver** (or return an `InputRequiredResult` yourself) and it becomes part of the *response*, which every connection can carry:
335
337
336
338
```python title="server.py" hl_lines="15-17 21"
337
339
--8<--"docs_src/troubleshooting/tutorial007.py"
338
340
```
339
341
340
-
Same question, same `elicitation_callback` on the client. The difference is under the hood: a resolver lets the server *return* the question from the call instead of pushing it, so nothing ever flows server-to-client. **[Elicitation](handlers/elicitation.md)** covers resolvers; **[Multi-round-trip requests](handlers/multi-round-trip.md)** covers what happens on the wire.
342
+
Same question, same `elicitation_callback` on the client. The difference is under the hood: a resolver lets the server *return* the question from the call instead of pushing it, so nothing ever flows server-to-client. That rescues every `2026-07-28` client, whichever of the three configurations the server is in. A *legacy* client is not rescued by the rewrite alone: `2025-11-25` has no way to return a question, so on a legacy connection the resolver still sends `elicitation/create` down the request-scoped channel, and still needs a server that keeps it — neither `stateless_http=True` nor `json_response=True`. **[Elicitation](handlers/elicitation.md)** covers resolvers; **[Multi-round-trip requests](handlers/multi-round-trip.md)** covers what happens on the wire.
341
343
342
344
!!! check
343
345
The tool with `ctx.elicit()` is not wrong, it is *pre-2026*. Connect with `mode="legacy"`
344
-
(the classic `initialize` handshake, spec `2025-11-25` and earlier) to a server that is not
345
-
`stateless_http=True`, and it works, because the server-to-client channel exists there.
346
+
(the classic `initialize` handshake, spec `2025-11-25` and earlier) to a server that is neither
347
+
`stateless_http=True` nor `json_response=True`, and it works, because the server-to-client
348
+
channel exists there.
346
349
**[Protocol versions](protocol-versions.md)** is the page on what each version has.
* One 421, three spellings: `Server returned an error response` (the python `Client`), `421 Misdirected Request` / `Invalid Host header` (everything else), `Invalid Host header: <host>` (the server log). Fix: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`.
408
411
*`Task group is not initialized` -> a mounted app whose host lifespan never entered `mcp.session_manager.run()`.
409
412
*`Session not found` -> the server restarted; reconnect.
410
-
*`Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` needs a server-to-client channel: a `2026-07-28` connection never has one, and `stateless_http=True` takes away the legacy one. Use a resolver. Its neighbour `Method not found` is a request for a method the other side's protocol revision doesn't have.
413
+
*`Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` needs a server-to-client channel: a `2026-07-28` connection never has one, `stateless_http=True` takes away the legacy one, and `json_response=True` takes away the request-scoped one. Use a resolver (a legacy client also needs a server that keeps the channel). Its neighbour `Method not found` is a request for a method the other side's protocol revision doesn't have.
411
414
*`Client did not declare the form elicitation capability ...` and `Elicitation not supported` -> the client is missing `elicitation_callback=`.
412
415
*`Invalid or expired requestState` never says why on the wire. The server log does; `unknown key` means share `RequestStateSecurity(keys=[...])` across workers.
0 commit comments