Skip to content

Commit 71bd40c

Browse files
committed
Address story-suite review findings
- error_handling: use the public e.data property and drop the now-covered pragma on MCPError.data; remove the README workaround note - serve_one: SingleExchangeContext.send_raw_request raises NoBackChannelError(method) per the DispatchContext contract - parallel_calls README: stop claiming progress-token demux on a shared wire (the example uses two separate connections) - roots/sampling READMEs: align Caveats and See-also with the SEP-2577 deprecation banner instead of pointing at MRTR as a successor - starlette_mount README: correct the forgotten-lifespan failure mode (immediate 500 with RuntimeError, not a hang) - _harness: pin era from the manifest when a story is single-era - README spec links: /specification/2026-07-28/ -> /specification/draft/
1 parent e362d8f commit 71bd40c

11 files changed

Lines changed: 32 additions & 28 deletions

File tree

examples/stories/_harness.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ def run_client(main: Callable[..., Awaitable[None]]) -> None:
157157
# even though it often matches the ``Client`` default of "auto". stdio is legacy-only
158158
# until the SDK's stdio entry can negotiate the era, so only --http gets a modern arm.
159159
era = "modern" if transport == "http" and "--legacy" not in sys.argv else "legacy"
160+
if cfg["era"] in ("legacy", "modern"):
161+
era = cfg["era"]
160162
if cfg["era"] == "dual-in-body":
161163
# The story pins its connection modes inside ``main`` itself, so hand it "auto"
162164
# (the ``Client`` default) and let those in-body pins decide. A hard version pin

examples/stories/error_handling/README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ uv run python -m stories.error_handling.client --http --server server_lowlevel
4141
- `MCPServer` prefixes the execution-error message with
4242
`"Error executing tool {name}: "`; build a `CallToolResult` directly from a
4343
lowlevel handler if you need verbatim control.
44-
- `client.py` reads `e.error.data` rather than `e.data`; the convenience
45-
property carries a `no cover` pragma that `strict-no-cover` would trip.
4644

4745
## Spec
4846

examples/stories/error_handling/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def main(target: Target, *, mode: str = "auto") -> None:
2929
except MCPError as e:
3030
assert e.code == INVALID_PARAMS
3131
assert e.message == "this tool is gated"
32-
assert e.error.data == {"reason": "demo"}
32+
assert e.data == {"reason": "demo"}
3333
else:
3434
raise AssertionError("expected MCPError for a protocol-level rejection")
3535

examples/stories/json_response/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ kill "$SERVER_PID"
6060

6161
## Spec
6262

63-
[Streamable HTTP — 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http)
64-
· [SEP-2243 standard headers](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#standard-request-headers)
63+
[Streamable HTTP — 2026-07-28](https://modelcontextprotocol.io/specification/draft/basic/transports/streamable-http)
64+
· [SEP-2243 standard headers](https://modelcontextprotocol.io/specification/draft/basic/transports/streamable-http#standard-request-headers)
6565

6666
## See also
6767

examples/stories/parallel_calls/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ Two `Client`s connected to the same server, each with a `call_tool` in flight
44
at once. The `meet` tool is a rendezvous: a handler signals its own arrival,
55
then blocks until every named peer has arrived too — so neither call can return
66
unless the server runs both handlers concurrently. Each caller's
7-
`progress_callback=` sees only the notifications for *its* request — the SDK
8-
demultiplexes by progress token, not by arrival order.
7+
`progress_callback=` sees only the notifications for *its* request — each
8+
`Client` is a separate connection, so there's no shared wire for them to cross
9+
on.
910

1011
## Run it
1112

@@ -36,8 +37,9 @@ subprocess per connection, so two clients there could never rendezvous.
3637
sequentially would never set the second event, so the client would time out —
3738
the timeout *is* the concurrency assertion. No sleeps.
3839
- **`client.py``progress_callback=` per call.** Each call passes its own
39-
callback; `received == {"a": ["a"], "b": ["b"]}` proves the SDK routes
40-
in-flight progress per request.
40+
callback; `received == {"a": ["a"], "b": ["b"]}` shows each connection
41+
delivered its own progress, and — combined with the rendezvous — that both
42+
calls were genuinely in flight at once.
4143
- **`server_lowlevel.py`** — same wire contract on the lowlevel `Server`,
4244
reporting via `ctx.session.report_progress(...)`.
4345

examples/stories/roots/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ uv run python -m stories.roots.client --http --legacy --server server_lowlevel
3838
## Caveats
3939

4040
- **Legacy-era only.** `roots/list` is a server-initiated request with no
41-
2026-07-28 wire carrier until the multi-round-trip runtime lands
42-
([#2898](https://github.com/modelcontextprotocol/python-sdk/issues/2898)), so
43-
this story runs with `era = "legacy"` and the harness pins the handshake path.
41+
2026-07-28 wire carrier, so this story runs with `era = "legacy"` and the
42+
harness pins the handshake path.
4443
- `ctx.session.list_roots()` is `@deprecated`; the
45-
`# pyright: ignore[reportDeprecated]` is deliberate. There is no
46-
non-deprecated server-side path until the multi-round-trip runtime lands.
44+
`# pyright: ignore[reportDeprecated]` is deliberate. The non-deprecated
45+
replacement is to accept directory paths as ordinary tool parameters (see the
46+
banner above) — there is no successor server→client call.
4747
- `ctx.session.*` is the interim 2-hop path; a later release will shorten it.
4848
- `notifications/roots/list_changed` is intentionally not shown — removed in
4949
2026-07-28 (SEP-2575) and deprecated on the legacy path.
@@ -54,5 +54,5 @@ uv run python -m stories.roots.client --http --legacy --server server_lowlevel
5454

5555
## See also
5656

57-
`legacy_elicitation/`, `sampling/` — sibling server→client requests on the same
58-
MRTR migration path.
57+
`legacy_elicitation/`, `sampling/` — sibling stories that exercise the same
58+
legacy server→client request shape.

examples/stories/sampling/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ uv run python -m stories.sampling.client --http --legacy --server server_lowleve
4141
## Caveats
4242

4343
- **Legacy-era only.** `sampling/createMessage` is a server-initiated request
44-
with no 2026-07-28 wire carrier until the multi-round-trip runtime lands
45-
([#2898](https://github.com/modelcontextprotocol/python-sdk/issues/2898)), so
46-
this story runs with `era = "legacy"` and the harness pins the handshake path.
44+
with no 2026-07-28 wire carrier, so this story runs with `era = "legacy"` and
45+
the harness pins the handshake path.
4746
- `ctx.session.create_message()` is `@deprecated`; the
48-
`# pyright: ignore[reportDeprecated]` is deliberate. There is no
49-
non-deprecated server-side path until the multi-round-trip runtime lands.
47+
`# pyright: ignore[reportDeprecated]` is deliberate. The non-deprecated
48+
replacement is to call your LLM provider directly from the server (see the
49+
banner above) — there is no successor server→client call.
5050
- `ctx.session.*` is the interim 2-hop path; a later release will shorten it.
5151
- `Client` has no `sampling_capabilities=` kwarg, so the `sampling.tools`
5252
sub-capability (tools-in-sampling) is unreachable from the high-level client.
@@ -58,5 +58,5 @@ uv run python -m stories.sampling.client --http --legacy --server server_lowleve
5858

5959
## See also
6060

61-
`legacy_elicitation/`, `roots/` — sibling server→client requests on the same
62-
MRTR migration path.
61+
`legacy_elicitation/`, `roots/` — sibling stories that exercise the same legacy
62+
server→client request shape.

examples/stories/serve_one/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ uv run python -m stories.serve_one.client
5252
## Spec
5353

5454
[Architecture — lifecycle](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle)
55-
· [2026 versioning — discover](https://modelcontextprotocol.io/specification/2026-07-28/server/discover)
55+
· [2026 versioning — discover](https://modelcontextprotocol.io/specification/draft/server/discover)
5656

5757
## See also
5858

examples/stories/serve_one/server.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from mcp.server.lowlevel import Server
2222
from mcp.server.runner import serve_connection, serve_one # deep-path import; shorter re-export planned
2323
from mcp.server.stdio import stdio_server
24+
from mcp.shared.exceptions import NoBackChannelError
2425
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher
2526
from mcp.shared.transport_context import TransportContext
2627

@@ -58,7 +59,7 @@ class SingleExchangeContext:
5859
cancel_requested: anyio.Event = field(default_factory=anyio.Event)
5960

6061
async def send_raw_request(self, method: str, params: Mapping[str, Any] | None, opts: Any = None) -> dict[str, Any]:
61-
raise NotImplementedError # no back-channel on the single-exchange path
62+
raise NoBackChannelError(method)
6263

6364
async def notify(self, method: str, params: Mapping[str, Any] | None, opts: Any = None) -> None:
6465
return None

examples/stories/starlette_mount/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ kill "$SERVER_PID"
3232
slash required — Starlette's `Mount` forwards `/api` as an empty path that
3333
the inner `/` route won't match).
3434
- `server.py` `lifespan``mcp.session_manager.run()` **must** be entered by
35-
the parent app. Forget it and every MCP request hangs (the sub-app's own
36-
lifespan never fires under `Mount`).
35+
the parent app. Forget it and every MCP request fails immediately with a 500
36+
(`RuntimeError: Task group is not initialized. Make sure to use run().`) —
37+
the sub-app's own lifespan never fires under `Mount`.
3738
- `server.py` `Route("/health", ...)` — non-MCP routes live alongside the
3839
mount; FastAPI users do the same with `app.mount("/api", mcp_app)`.
3940

0 commit comments

Comments
 (0)