49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
167 lines
9.4 KiB
Markdown
167 lines
9.4 KiB
Markdown
# Story examples
|
||
|
||
One feature per folder. Each story is a small, self-verifying program: a
|
||
`server.py` (plus, where the wire contract is worth seeing by hand, a
|
||
`server_lowlevel.py`) and a `client.py` whose `main()` makes assertions and
|
||
exits non-zero on failure. The code you read here is the same code CI runs —
|
||
there is no separate test double.
|
||
|
||
## Canonical shape
|
||
|
||
Every `client.py` starts from this skeleton — copy it, then replace the body
|
||
with the story's assertions:
|
||
|
||
```python
|
||
"""One line: what this client proves."""
|
||
|
||
from mcp.client import Client
|
||
from stories._harness import Target, run_client
|
||
|
||
|
||
async def main(target: Target, *, mode: str = "auto") -> None:
|
||
async with Client(target, mode=mode) as client:
|
||
... # the story's assertions
|
||
|
||
|
||
if __name__ == "__main__":
|
||
run_client(main)
|
||
```
|
||
|
||
There are exactly two `main` shapes. A story that opens **one** connection
|
||
takes `main(target: Target, ...)`. A story that opens **more than one** sets
|
||
`multi_connection = true` in [`manifest.toml`](manifest.toml), takes
|
||
`main(targets: TargetFactory, ...)`, and calls `targets()` once per fresh
|
||
connection — a `Client` cannot be re-entered after exit. Nothing else changes
|
||
shape.
|
||
|
||
Story files import from `stories._harness` only these names: `run_client`,
|
||
`target_from_args`, `Target`, `TargetFactory` — plus `AuthBuilder` for the
|
||
auth stories. Everything else a story uses comes from public `mcp.*` modules.
|
||
|
||
The repetition this produces across stories is deliberate, not a refactor
|
||
waiting to happen: each `client.py` is a standalone, compiled doc page, so
|
||
when a public API changes, N red example files flag N doc pages. Don't pull
|
||
the `Client(target, mode=mode)` line (or anything around it) into a shared
|
||
helper. A story that can't be the canonical shape says why in its module
|
||
docstring's first line.
|
||
|
||
## How to read a story
|
||
|
||
Start with the story's README, then `server.py`, then `client.py`. Every
|
||
`client.py` exports `async def main(target, *, mode="auto")` — or
|
||
`main(targets, ...)` for the stories that open more than one connection — and
|
||
constructs the `Client` itself, so the body opens with the one line a client
|
||
example exists to teach: `async with Client(target, mode=mode) as client:`.
|
||
The `run_client(main)` call in the `__main__` block is only argv plumbing
|
||
(stdio vs `--http`, which `mode` to pass); it never hides how the client
|
||
connects.
|
||
|
||
## Running a story
|
||
|
||
From the repository root:
|
||
|
||
```bash
|
||
# stdio (default — the client spawns the server as a subprocess)
|
||
uv run python -m stories.tools.client
|
||
|
||
# HTTP, self-hosted — the client spawns the server on a real uvicorn socket on a
|
||
# port it owns, waits for it, runs, then terminates it. Nothing to background or kill.
|
||
uv run python -m stories.tools.client --http
|
||
|
||
# the same self-hosted run against the story's lowlevel-API server variant
|
||
uv run python -m stories.tools.client --http --server server_lowlevel
|
||
|
||
# HTTP against a server you run yourself
|
||
uv run python -m stories.tools.server --http --port 8000 # separate terminal
|
||
uv run python -m stories.tools.client --http http://127.0.0.1:8000/mcp
|
||
```
|
||
|
||
`--http` takes two forms. Bare `--http` is the canonical HTTP run — it is
|
||
complete on its own, and it is what every per-story README shows. `--http
|
||
<url>` connects to a server you started yourself; the per-story READMEs spell
|
||
that out only where hosting is the lesson (the HTTP-hosting and auth stories).
|
||
`--server <stem>` swaps in a sibling server module on stdio and on the
|
||
self-hosted `--http` run; with `--http <url>` you already picked the server
|
||
when you started it. The auth stories (`bearer_auth/`, `oauth/`,
|
||
`oauth_client_credentials/`) self-host on their fixed `:8000` instead of a
|
||
free port because their issuer/PRM metadata bake it in — `:8000` must be
|
||
free, and the run refuses to start (rather than silently testing whatever is
|
||
there) if it is not.
|
||
|
||
The full matrix (every story × transport × era × server-variant) runs under
|
||
pytest:
|
||
|
||
```bash
|
||
uv run --frozen pytest tests/examples/ # everything
|
||
uv run --frozen pytest tests/examples/ -k tools # one story
|
||
```
|
||
|
||
[`manifest.toml`](manifest.toml) declares each story's transports, era, status,
|
||
and variants; `tests/examples/` expands it.
|
||
|
||
## Layout
|
||
|
||
`_hosting.py` adapts a story's `build_server()` / `build_app()` to argv (stdio
|
||
vs `--http` serving); `_harness.py` is the client-side mirror — it picks the
|
||
`target` that `main()` connects to (a stdio subprocess by default, a self-hosted
|
||
HTTP subprocess under bare `--http`, your URL under `--http <url>`). They
|
||
isolate the parts of the SDK's hosting surface
|
||
that are still moving — **don't copy them into your own project**; copy the
|
||
`server.py` / `client.py` bodies instead. `_shared/` holds an in-process OAuth
|
||
authorization server reused by the auth stories.
|
||
|
||
## Stories
|
||
|
||
The **status** column is the feature's standing in the protocol, from
|
||
[`manifest.toml`](manifest.toml): `current`, `legacy` (a 2025 handshake-era
|
||
mechanism with a 2026-era replacement), or `deprecated` (deprecated by
|
||
SEP-2577; functional through the deprecation window). Each non-`current` story's README
|
||
opens with a banner saying what replaces it.
|
||
|
||
| story | what it shows | status |
|
||
|---|---|---|
|
||
| **— start here —** | | |
|
||
| [`tools`](tools/) | `@mcp.tool()`, schema inference, structured output, annotations | current |
|
||
| [`prompts`](prompts/) | `@mcp.prompt()`, list/get, argument completion | current |
|
||
| [`resources`](resources/) | `@mcp.resource()`, list/read, URI templates | current |
|
||
| [`lifespan`](lifespan/) | startup/shutdown lifespan, per-request state injection | current |
|
||
| [`dual_era`](dual_era/) | one server factory serving both protocol eras; era-neutral accessors | current |
|
||
| **— feature stories —** | | |
|
||
| [`streaming`](streaming/) | progress notifications, in-flight logging, cancellation | current |
|
||
| [`mrtr`](mrtr/) | `InputRequiredResult` round-trip: the `Client` auto-loop, a manual session-level loop, and the default `requestState` sealing (a tampered echo gets one frozen error) | current |
|
||
| [`legacy_elicitation`](legacy_elicitation/) | server pauses a tool to ask the user (form + url) via a push request | legacy |
|
||
| [`refund_desk`](refund_desk/) | resolver DI: `Annotated[T, Resolve(fn)]` params filled server-side, hidden from the input schema | current |
|
||
| [`sampling`](sampling/) | server asks the client's LLM mid-tool (push request) | deprecated |
|
||
| [`stickynotes`](stickynotes/) | capstone: tools mutate state → resources + `list_changed` + elicit guard | current |
|
||
| [`custom_methods`](custom_methods/) | vendor-prefixed JSON-RPC via `add_request_handler` / `send_request` | current |
|
||
| [`schema_validators`](schema_validators/) | tool input schema from pydantic / TypedDict / dataclass / dict | current |
|
||
| [`middleware`](middleware/) | server-side request/response middleware | current |
|
||
| [`parallel_calls`](parallel_calls/) | two clients rendezvous in one tool; per-call progress attribution | current |
|
||
| [`roots`](roots/) | client-declared roots, server reads them via `ctx` | deprecated |
|
||
| [`pagination`](pagination/) | manual cursor loop over list endpoints | current |
|
||
| [`error_handling`](error_handling/) | `is_error` results vs `MCPError`; `ToolError` | current |
|
||
| [`serve_one`](serve_one/) | building a `Connection` by hand and calling `serve_one` directly | current |
|
||
| **— HTTP hosting —** | | |
|
||
| [`stateless_legacy`](stateless_legacy/) | `streamable_http_app(stateless_http=True)`; the one-liner deploy | current |
|
||
| [`json_response`](json_response/) | `json_response=True` mode; raw 2026 POST envelope on the wire | current |
|
||
| [`legacy_routing`](legacy_routing/) | `classify_inbound_request()` era routing in front of a sessionful 1.x deploy | current |
|
||
| [`starlette_mount`](starlette_mount/) | mounting `streamable_http_app()` under a Starlette/FastAPI sub-path | current |
|
||
| [`sse_polling`](sse_polling/) | SEP-1699 `closeSSE()` + `Last-Event-ID` resume via `EventStore` | legacy |
|
||
| [`standalone_get`](standalone_get/) | server-initiated `list_changed` over the sessionful GET stream | legacy |
|
||
| [`subscriptions`](subscriptions/) | `subscriptions/listen` streams: `ctx.notify_*`, `SubscriptionBus`, `ListenHandler` | current |
|
||
| [`reconnect`](reconnect/) | explicit `discover()`, persist `DiscoverResult`, zero-RTT reconnect | current |
|
||
| [`bearer_auth`](bearer_auth/) | `TokenVerifier` + `AuthSettings` bearer gate, PRM metadata, `get_access_token()` | current |
|
||
| [`oauth`](oauth/) | full `authorization_code` grant against an in-process AS | current |
|
||
| [`oauth_client_credentials`](oauth_client_credentials/) | `client_credentials` grant; minimal in-process token endpoint | current |
|
||
| [`identity_assertion`](identity_assertion/) | SEP-990 enterprise IdP flow: present an ID-JAG under the `jwt-bearer` grant | current |
|
||
| **— deferred (README only) —** | | |
|
||
| [`caching`](caching/) | `CacheableResult` ttl/scope hints; client honouring | not yet implemented |
|
||
| [`tasks`](tasks/) | `io.modelcontextprotocol/tasks` extension | not yet implemented |
|
||
| [`apps`](apps/) | MCP Apps: `ui://` resource + `_meta.ui` | not yet implemented — [#2896](https://github.com/modelcontextprotocol/python-sdk/issues/2896) |
|
||
| [`skills`](skills/) | SEP-2640 skills extension | not yet implemented — [#2896](https://github.com/modelcontextprotocol/python-sdk/issues/2896) |
|
||
| [`events`](events/) | `io.modelcontextprotocol/events` extension | not yet implemented |
|
||
|
||
The TypeScript SDK's `repl`, `client-quickstart`, and `server-quickstart`
|
||
examples are intentionally not ported (interactive / external network deps);
|
||
its `hono` example maps to `starlette_mount/`.
|