Skip to content

Add story-style examples suite (29 stories + harness + CI)#2957

Draft
maxisbey wants to merge 7 commits into
s3-client-modern-pathfrom
examples-story-suite
Draft

Add story-style examples suite (29 stories + harness + CI)#2957
maxisbey wants to merge 7 commits into
s3-client-modern-pathfrom
examples-story-suite

Conversation

@maxisbey

@maxisbey maxisbey commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Adds a story-style examples suite: one self-contained folder per protocol feature, each with server.py (MCPServer), server_lowlevel.py (lowlevel Server), client.py, and README.md. Mirrors the typescript-sdk v2-2026-07-28 examples layout.

Motivation and Context

The current examples/ directory has four parallel layouts, every client example still uses the v1 ClientSession API, and only everything-server runs in CI. We want examples that:

  • answer "how do I do X?" with one small, focused folder a user can read top to bottom
  • run as part of the test suite so any public-API change is immediately visible against real usage
  • cover both 2025-handshake and 2026-stateless eras, with both MCPServer and lowlevel Server variants where the feature allows

What's in this PR

  • 29 runnable stories under 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_credentials
  • 7 README-only stubs for features not yet landed: caching, mrtr, subscriptions, tasks, apps, skills, events
  • Test harness (tests/examples/): pytest-driven, parametrizes each story across (server variant × transport × era) via manifest.toml — 145 legs in ~1.4s, all in-memory or ASGI-bridged (no subprocesses, no sleeps). Plus a 3-leg subprocess smoke test gated on MCP_EXAMPLES_SMOKE=1 to verify the __main__ paths work over real stdio/uvicorn.
  • Scaffolding: _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)
  • Packaging: single examples/pyproject.toml workspace member; from stories.tools.server import build_server works in scripts, -m, and pytest
  • CI: stories run in the existing test matrix; smoke leg runs on one ubuntu/3.12/locked cell
  • The existing examples/clients/, examples/servers/*, examples/mcpserver/ and examples/snippets/ directories are left in place for now

How Has This Been Tested?

  • ./scripts/test: 2831 passed, 100% coverage, strict-no-cover clean (net −5 pragmas in src/ — 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)
  • pyright, ruff, pre-commit: clean

Breaking Changes

None.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Intentional behavior changes

  • src/mcp/server/{elicitation,lowlevel/server,mcpserver/server}.py: removed 5 # pragma: no cover markers on branches the new stories exercise. No logic change.

Known caveats / follow-ups

  • The server HTTP entry surface is mid-refactor on this branch; _hosting.py isolates those calls so the stories themselves won't churn when that lands.
  • roots, sampling, and the logging portion of streaming are deprecated as of the 2026-07-28 spec (SEP-2577); their READMEs say so and point at the migration path.
  • Several SDK ergonomic gaps were found while writing these (e.g. no Client(stdio_params) overload, no auth= passthrough on Client(url), MCPServer has no public middleware hook). Tracked separately; the stories work around them in the harness, not in the user-facing files.
  • Old examples/{clients,servers,mcpserver}/ left in place for now; consolidating into stories/ is a follow-up.

AI Disclaimer

maxisbey added 7 commits June 23, 2026 21:12
- 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prolly worth a comment saying that this handles both old and new automatially or something without requiring config

@@ -0,0 +1,66 @@
# elicitation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant