Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

9.4 KiB
Raw Permalink Blame History

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:

"""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, 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:

# 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:

uv run --frozen pytest tests/examples/          # everything
uv run --frozen pytest tests/examples/ -k tools # one story

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: 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 @mcp.tool(), schema inference, structured output, annotations current
prompts @mcp.prompt(), list/get, argument completion current
resources @mcp.resource(), list/read, URI templates current
lifespan startup/shutdown lifespan, per-request state injection current
dual_era one server factory serving both protocol eras; era-neutral accessors current
— feature stories —
streaming progress notifications, in-flight logging, cancellation current
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 server pauses a tool to ask the user (form + url) via a push request legacy
refund_desk resolver DI: Annotated[T, Resolve(fn)] params filled server-side, hidden from the input schema current
sampling server asks the client's LLM mid-tool (push request) deprecated
stickynotes capstone: tools mutate state → resources + list_changed + elicit guard current
custom_methods vendor-prefixed JSON-RPC via add_request_handler / send_request current
schema_validators tool input schema from pydantic / TypedDict / dataclass / dict current
middleware server-side request/response middleware current
parallel_calls two clients rendezvous in one tool; per-call progress attribution current
roots client-declared roots, server reads them via ctx deprecated
pagination manual cursor loop over list endpoints current
error_handling is_error results vs MCPError; ToolError current
serve_one building a Connection by hand and calling serve_one directly current
— HTTP hosting —
stateless_legacy streamable_http_app(stateless_http=True); the one-liner deploy current
json_response json_response=True mode; raw 2026 POST envelope on the wire current
legacy_routing classify_inbound_request() era routing in front of a sessionful 1.x deploy current
starlette_mount mounting streamable_http_app() under a Starlette/FastAPI sub-path current
sse_polling SEP-1699 closeSSE() + Last-Event-ID resume via EventStore legacy
standalone_get server-initiated list_changed over the sessionful GET stream legacy
subscriptions subscriptions/listen streams: ctx.notify_*, SubscriptionBus, ListenHandler current
reconnect explicit discover(), persist DiscoverResult, zero-RTT reconnect current
bearer_auth TokenVerifier + AuthSettings bearer gate, PRM metadata, get_access_token() current
oauth full authorization_code grant against an in-process AS current
oauth_client_credentials client_credentials grant; minimal in-process token endpoint current
identity_assertion SEP-990 enterprise IdP flow: present an ID-JAG under the jwt-bearer grant current
— deferred (README only) —
caching CacheableResult ttl/scope hints; client honouring not yet implemented
tasks io.modelcontextprotocol/tasks extension not yet implemented
apps MCP Apps: ui:// resource + _meta.ui not yet implemented — #2896
skills SEP-2640 skills extension not yet implemented — #2896
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/.