-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Add story-style examples suite (29 stories + harness + CI) #2957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
maxisbey
wants to merge
14
commits into
main
Choose a base branch
from
examples-story-suite
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+6,791
−25
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
84d9520
Add story-style examples suite (29 stories + harness + CI)
maxisbey 376fa11
Remove examples superseded by stories/; update dangling references
maxisbey e071daf
Remove no-cover pragmas for paths now exercised by story examples
maxisbey c992ec4
Drop internal tracking codes from conftest docstrings
maxisbey ecfa200
Restore old example directories; fix typing.TypedDict on Python < 3.12
maxisbey 4052b31
Restore original references to old example dirs (revert remainder)
maxisbey 60c8b07
Add lax no cover to both arms of tomllib version-gate
maxisbey 6942ebe
Show Client construction in every story; cut/rename stories per review
maxisbey 137f51c
Add a story shape-check; document the canonical example shape
maxisbey 5d8e097
Adapt the story suite to the merged 2026 client surface
maxisbey 7447bff
Address example-suite review findings
maxisbey 839932f
READMEs: capture the server PID instead of relying on job control
maxisbey 5194790
Let client.py self-host its server for HTTP runs
maxisbey e362d8f
Adapt story suite to the mcp-types package split after rebase
maxisbey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,22 @@ | ||
| # Python SDK Examples | ||
| # Python SDK examples | ||
|
|
||
| This folders aims to provide simple examples of using the Python SDK. Please refer to the | ||
| [servers repository](https://github.com/modelcontextprotocol/servers) | ||
| for real-world servers. | ||
| - [`stories/`](stories/) — **the canonical reference.** One self-verifying | ||
| example per protocol feature, each with its own README. Start with | ||
| [`stories/tools/`](stories/tools/); the [stories README](stories/README.md) | ||
| has the full table and how to run them. | ||
| - [`snippets/`](snippets/) — short extracts embedded into `README.v2.md`. Kept | ||
| minimal and in sync with the top-level README; not intended to be run | ||
| standalone. | ||
| - [`servers/everything-server/`](servers/everything-server/) — the conformance | ||
| target for the cross-SDK | ||
| [conformance suite](https://github.com/modelcontextprotocol/conformance). | ||
| Exercises every server capability in one process. | ||
| - [`mcpserver/`](mcpserver/) — single-file v1-era examples retained for the | ||
| migration guide; superseded by `stories/` and slated for removal. | ||
| - [`clients/`](clients/) and the remaining [`servers/`](servers/) directories | ||
| (`simple-*`, `sse-polling-demo`, `structured-output-lowlevel`) — standalone | ||
| v1-era projects still linked from `README.v2.md`; retained pending | ||
| consolidation into `stories/`. | ||
|
|
||
| For real-world servers see the | ||
| [servers repository](https://github.com/modelcontextprotocol/servers). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| [project] | ||
| name = "mcp-example-stories" | ||
| version = "0.0.0" | ||
| description = "Self-verifying example suite for the MCP Python SDK (dev-only, not published)" | ||
| requires-python = ">=3.10" | ||
| dependencies = [ | ||
| "mcp", | ||
| "tomli>=2.0; python_version < '3.11'", | ||
| ] | ||
|
|
||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["stories"] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| # 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 | | ||
| | [`legacy_elicitation`](legacy_elicitation/) | server pauses a tool to ask the user (form + url) via a push request | legacy | | ||
| | [`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 | | ||
| | [`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 | | ||
| | **— deferred (README only) —** | | | | ||
| | [`caching`](caching/) | `CacheableResult` ttl/scope hints; client honouring | not yet implemented | | ||
| | [`mrtr`](mrtr/) | `InputRequiredResult` round-trip with `requestState` HMAC | not yet implemented — [#2898](https://github.com/modelcontextprotocol/python-sdk/issues/2898) | | ||
| | [`subscriptions`](subscriptions/) | `subscriptions/listen`, `ServerEventBus`, `Client.listen()` | not yet implemented — [#2901](https://github.com/modelcontextprotocol/python-sdk/issues/2901) | | ||
| | [`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/`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| """Self-verifying example suite for the MCP Python SDK. | ||
| Each story directory holds a ``server.py`` (and usually ``server_lowlevel.py``) | ||
| plus a ``client.py`` whose ``main(target, *, mode)`` runs against both. | ||
| ``tests/examples/`` drives every story over an in-process matrix. | ||
| """ |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.