Add story-style examples suite (29 stories + harness + CI)#2957
Add story-style examples suite (29 stories + harness + CI)#2957maxisbey wants to merge 7 commits into
Conversation
- examples/stories/: 29 narrative example scripts, each a self-contained client+server scenario that can be read top-to-bottom and run directly - examples/pyproject.toml: workspace member so stories resolve against the in-tree SDK - tests/examples/: pytest harness that imports and runs every story under the existing coverage gate (in-memory, no subprocesses) - .github/workflows/shared.yml: wire the stories harness into the test job - pyproject.toml / uv.lock: register examples workspace member
- Delete examples/clients/* and examples/servers/simple-*, sse-polling-demo (each has an equivalent under examples/stories/) - examples/mcpserver/*.py: add TODO pointers to the corresponding story - README.v2.md, examples/snippets/clients/oauth_client.py: update paths that pointed at the removed examples - src/mcp/server/auth/middleware/bearer_auth.py: drop pragma now that the bearer_auth story exercises this path
- Revert removal of examples/{clients,servers/simple-*,servers/sse-polling-demo}
so the old examples remain alongside stories/ for now.
- schema_validators: use typing_extensions.TypedDict so pydantic accepts
it as a tool parameter on Python 3.10/3.11.
- pyright: add explicit extraPaths for examples/servers/simple-auth — the
mcp-example-stories editable install puts examples/ on sys.path, which
defeats pyright's package-root auto-detection for that one example.
The else arm had it; the if arm did not, so on 3.10 the unreachable import tomllib line counted as a miss.
|
|
||
| async def scenario(client: Client) -> None: | ||
| # client.session is the ClientSession that Client.__aenter__ connected for you. | ||
| session: ClientSession = client.session |
There was a problem hiding this comment.
I don't think accessing Client.session is something we want to have clear examples for. Client is supposed to be high level and client.session honestly feels temporary.
at least, it would make sense to instead just have an example of only using ClientSession without using Client, like how to construct it etc.
| from stories._harness import connect_from_args, run_client | ||
|
|
||
|
|
||
| async def scenario(client: Client) -> None: |
There was a problem hiding this comment.
this is why I don't really like that we don't show the Client getting constructed, this is super unclear to users.
I think we need to rethink how client construction works to make it both clear to a user how to construct it, while also making it possible to run in our harness.
|
|
||
| ## Not yet: overriding the supported-version set | ||
|
|
||
| The TypeScript SDK lets a server declare `supportedProtocolVersions: [...]` to |
There was a problem hiding this comment.
not sure we want this in the python sdk documentation tbh, kinda confusing. I think tracking it ourselves is important tho
|
|
||
|
|
||
| def build_server() -> MCPServer: | ||
| mcp = MCPServer("custom-version-example") |
There was a problem hiding this comment.
is that string the version, or the name of the server? either annotate or we need to make it clear how to set a custom version
| assert params.name == "protocol_info" | ||
| return types.CallToolResult(content=[types.TextContent(text=ctx.protocol_version)]) | ||
|
|
||
| return Server("custom-version-example", on_list_tools=list_tools, on_call_tool=call_tool) |
There was a problem hiding this comment.
yea also even for me this is confusing, like is "custom-version-example" the custom version? seems wrong?
if this is literally the first arg of Server then I mean even that's confusing but i dont' think that's right
|
|
||
| # ── legacy leg: a fresh client at mode="legacy" runs the initialize handshake against | ||
| # the SAME server factory. The era-neutral accessors are populated identically. | ||
| async with connect(mode="legacy") as legacy: |
There was a problem hiding this comment.
this is IMO confusing for an example users are going to read. I'd prefer them to not have to go and look at the source code to connect to understand how to construct this client
| text = f"Hello, {params.arguments['name']}! (served on the {era} era at {ctx.protocol_version})" | ||
| return types.CallToolResult(content=[types.TextContent(text=text)]) | ||
|
|
||
| return Server( |
There was a problem hiding this comment.
prolly worth a comment saying that this handles both old and new automatially or something without requiring config
| @@ -0,0 +1,66 @@ | |||
| # elicitation | |||
There was a problem hiding this comment.
for this whole story we need to make this clear that this is the legacy appraoch, maybe even renaming to to lecay_elicitation with an explanation of how mrtr is the new way even tho not yet implemented, so some TODO(maxisbey)'s would be good
same with the others for deprecated or legacy behaviour
Adds a story-style examples suite: one self-contained folder per protocol feature, each with
server.py(MCPServer),server_lowlevel.py(lowlevelServer),client.py, andREADME.md. Mirrors the typescript-sdkv2-2026-07-28examples layout.Motivation and Context
The current
examples/directory has four parallel layouts, every client example still uses the v1ClientSessionAPI, and onlyeverything-serverruns in CI. We want examples that:MCPServerand lowlevelServervariants where the feature allowsWhat's in this PR
examples/stories/: tools, prompts, resources, lifespan, dual_era, custom_version, streaming, elicitation, sampling, stickynotes, custom_methods, schema_validators, middleware, parallel_calls, roots, pagination, error_handling, client_session, serve_one, stateless_legacy, json_response, legacy_routing, starlette_mount, sse_polling, standalone_get, reconnect, bearer_auth, oauth, oauth_client_credentialstests/examples/): pytest-driven, parametrizes each story across (server variant × transport × era) viamanifest.toml— 145 legs in ~1.4s, all in-memory or ASGI-bridged (no subprocesses, no sleeps). Plus a 3-leg subprocess smoke test gated onMCP_EXAMPLES_SMOKE=1to verify the__main__paths work over real stdio/uvicorn._harness.py(client-side, stable) and_hosting.py(server-side; isolates the HTTP entry calls that are still being reshaped on this branch so one edit propagates when that lands),_shared/auth.py(in-process AS for the auth stories)examples/pyproject.tomlworkspace member;from stories.tools.server import build_serverworks in scripts,-m, and pytestexamples/clients/,examples/servers/*,examples/mcpserver/andexamples/snippets/directories are left in place for nowHow Has This Been Tested?
./scripts/test: 2831 passed, 100% coverage, strict-no-cover clean (net −5 pragmas insrc/— stories now exercise previously-uncovered branches)pytest tests/examples/: 145 passed, 4 xfail (progress notifications dropped over JSON-mode HTTP on the modern protocol path — known, documented in the affected READMEs), 3 skipped (smoke without env var)MCP_EXAMPLES_SMOKE=1 pytest tests/examples/test_stories_smoke.py: 3 passed (real subprocess stdio + uvicorn)Breaking Changes
None.
Types of changes
Checklist
Additional context
Intentional behavior changes
src/mcp/server/{elicitation,lowlevel/server,mcpserver/server}.py: removed 5# pragma: no covermarkers on branches the new stories exercise. No logic change.Known caveats / follow-ups
_hosting.pyisolates those calls so the stories themselves won't churn when that lands.roots,sampling, and the logging portion ofstreamingare deprecated as of the 2026-07-28 spec (SEP-2577); their READMEs say so and point at the migration path.Client(stdio_params)overload, noauth=passthrough onClient(url),MCPServerhas no public middleware hook). Tracked separately; the stories work around them in the harness, not in the user-facing files.examples/{clients,servers,mcpserver}/left in place for now; consolidating intostories/is a follow-up.AI Disclaimer