Skip to content

Commit 01128d6

Browse files
committed
docs: ctx.elicit() at 2026-07-28 is refused server-side since #3040
Rebasing onto main picked up #3040, which hardened the era semantics: a server-initiated request on a 2026-07-28 connection is now refused by the SERVER, as MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests. instead of reaching the client and being rejected there as "Method not found". Two of this branch's docs tests failed on the rebase -- which is the point of testing every example -- and the troubleshooting page keyed its biggest elicitation entry on the old string. So the two entries merge into the one that owns the surviving string. "Method not found" is now short and generic (an era mismatch: a method one protocol revision has and the other does not), and says explicitly that ctx.elicit() at 2026-07-28 no longer produces it. The "Cannot send 'elicitation/create' ..." entry becomes the single home for "your handler reached back and nothing can carry it", with its two real triggers -- any 2026-07-28 connection, and a legacy connection on a stateless_http=True server -- both shown from tested examples, and the one fix (a resolver). The tests pin the new behaviour, the same way #3040 itself updated tests/docs_src/test_client_callbacks.py.
1 parent 235f60d commit 01128d6

2 files changed

Lines changed: 43 additions & 40 deletions

File tree

docs/troubleshooting.md

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -260,37 +260,9 @@ For the server operator, the matching log line is `Rejected request with unknown
260260

261261
## `MCPError: Method not found`
262262

263-
One side sent a JSON-RPC request the other side has no handler for, and `e.error.data` names the method. The one everybody hits has `data` equal to `elicitation/create`:
263+
One side sent a JSON-RPC request the other has no handler for, and `e.error.data` names the method. The usual cause is an **era mismatch**: a method that exists in one protocol revision and not in the other, sent to a peer on the wrong one — a `2025`-era `resources/subscribe` arriving at a `2026-07-28` connection, a `2026`-only `subscriptions/listen` sent by a client pinned to `mode="legacy"`. **[Protocol versions](protocol-versions.md)** is the map of which side speaks what, and the other honest cause — an optional capability you never registered a handler for — is on **[Completions](servers/completions.md)**.
264264

265-
```python title="server.py" hl_lines="16"
266-
--8<-- "docs_src/troubleshooting/tutorial006.py"
267-
```
268-
269-
```python
270-
async def main() -> None:
271-
async with Client(mcp) as client:
272-
await client.call_tool("book_table", {"date": "Friday"})
273-
```
274-
275-
```text
276-
mcp.shared.exceptions.MCPError: Method not found
277-
```
278-
279-
`ctx.elicit()` (and `ctx.elicit_url()`) is a request from the *server* to the *client*, and the **2026-07-28** protocol has no server-initiated requests. The in-memory `Client(server)` negotiates `2026-07-28` without being asked, so this fails on the very first test, and passing `elicitation_callback=` changes nothing: the method itself is what's missing, not the handler.
280-
281-
The fix is to move the question out of the tool body and into a **resolver**, which works on every protocol version:
282-
283-
```python title="server.py" hl_lines="15-17 21"
284-
--8<-- "docs_src/troubleshooting/tutorial007.py"
285-
```
286-
287-
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 actually happens on the wire.
288-
289-
!!! check
290-
The tool with `ctx.elicit()` is not wrong, it is *pre-2026*. Connect with `mode="legacy"`
291-
(the classic `initialize` handshake, spec `2025-11-25` and earlier) and it works, because the
292-
server-to-client channel exists there. **[Protocol versions](protocol-versions.md)** is the
293-
page on what each version has.
265+
One thing does **not** produce this error, despite being a request the modern protocol removed: a tool calling `ctx.elicit()` on a `2026-07-28` connection. The server refuses to *send* that request at all, so what you get instead is `Cannot send 'elicitation/create': ...`, further down this page.
294266

295267
## `MCPError: Client did not declare the form elicitation capability required by resolver '<name>'`
296268

@@ -333,17 +305,45 @@ You see this one from `ctx.elicit()` on a legacy connection, and — on any conn
333305

334306
## `MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.`
335307

336-
Your handler tried to reach the client mid-request, on a transport where nothing can.
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.
337309

338-
Stateless HTTP is the usual trigger. `stateless_http=True` means every request is its own world: no session, no server-to-client stream, and so nowhere to send an `elicitation/create` (or `sampling/createMessage`, or `roots/list`):
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+
312+
```python title="server.py" hl_lines="16"
313+
--8<-- "docs_src/troubleshooting/tutorial006.py"
314+
```
315+
316+
```python
317+
async def main() -> None:
318+
async with Client(mcp) as client:
319+
await client.call_tool("book_table", {"date": "Friday"})
320+
```
321+
322+
```text
323+
mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests.
324+
```
325+
326+
**A legacy connection on a `stateless_http=True` server.** Statelessness means every request is its own world: no session, no server-to-client stream, and so nowhere to send an `elicitation/create` (or `sampling/createMessage`, or `roots/list`) even for the era that has them:
339327

340328
```python title="server.py" hl_lines="16 23"
341329
--8<-- "docs_src/troubleshooting/tutorial008.py"
342330
```
343331

344332
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.
345333

346-
The fix is the same as for `Method not found`: don't reach back mid-call. A **resolver** (or a returned `InputRequiredResult`) turns the question into part of the *response*, which every transport can carry, stateless or not. **[Multi-round-trip requests](handlers/multi-round-trip.md)** is that mechanism.
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:
335+
336+
```python title="server.py" hl_lines="15-17 21"
337+
--8<-- "docs_src/troubleshooting/tutorial007.py"
338+
```
339+
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.
341+
342+
!!! check
343+
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+
**[Protocol versions](protocol-versions.md)** is the page on what each version has.
347347

348348
## `MCPError: Invalid or expired requestState`
349349

@@ -407,6 +407,6 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key
407407
* 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=[...])`.
408408
* `Task group is not initialized` -> a mounted app whose host lifespan never entered `mcp.session_manager.run()`.
409409
* `Session not found` -> the server restarted; reconnect.
410-
* `Method not found` on `elicitation/create` -> `ctx.elicit()` needs a server-to-client channel and `2026-07-28` has none. Use a resolver.
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.
411411
* `Client did not declare the form elicitation capability ...` and `Elicitation not supported` -> the client is missing `elicitation_callback=`.
412412
* `Invalid or expired requestState` never says why on the wire. The server log does; `unknown key` means share `RequestStateSecurity(keys=[...])` across workers.

tests/docs_src/test_troubleshooting.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from mcp_types import (
99
INVALID_PARAMS,
1010
INVALID_REQUEST,
11-
METHOD_NOT_FOUND,
1211
MISSING_REQUIRED_CLIENT_CAPABILITY,
1312
ElicitRequestParams,
1413
ElicitResult,
@@ -183,21 +182,25 @@ async def test_a_session_id_the_server_never_issued_gets_a_404_session_not_found
183182
assert response.json() == {"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Session not found"}}
184183

185184

186-
async def test_ctx_elicit_at_2026_raises_method_not_found() -> None:
187-
"""tutorial006: at 2026-07-28 there is no server-to-client `elicitation/create` for the tool to send."""
185+
async def test_ctx_elicit_at_2026_has_no_back_channel() -> None:
186+
"""tutorial006: at 2026-07-28 the server refuses to send `elicitation/create` at all."""
188187
async with Client(tutorial006.mcp) as client:
189188
assert client.protocol_version == "2026-07-28"
190189
with pytest.raises(MCPError) as exc_info:
191190
await client.call_tool("book_table", {"date": "Friday"})
192191
assert exc_info.value.error == ErrorData(
193-
code=METHOD_NOT_FOUND, message="Method not found", data="elicitation/create"
192+
code=INVALID_REQUEST,
193+
message=(
194+
"Cannot send 'elicitation/create': "
195+
"this transport context has no back-channel for server-initiated requests."
196+
),
194197
)
195198

196199

197200
async def test_an_elicitation_callback_does_not_fix_ctx_elicit_at_2026() -> None:
198-
"""The page's `!!! warning`: registering the callback changes nothing. The method itself is gone."""
201+
"""The page's claim: registering the callback changes nothing. No request ever reaches the client."""
199202
async with Client(tutorial006.mcp, elicitation_callback=_confirm) as client:
200-
with pytest.raises(MCPError, match="^Method not found$"):
203+
with pytest.raises(MCPError, match="no back-channel for server-initiated requests"):
201204
await client.call_tool("book_table", {"date": "Friday"})
202205

203206

0 commit comments

Comments
 (0)