Rebuild the docs around tested examples; shrink README.v2.md to a pitch#2978
Conversation
There was a problem hiding this comment.
4 issues found across 198 files
Partial review: This PR has more than 50 files, so cubic reviewed the highest-priority files first. During the trial, paid plans get a higher file limit.
You can try an ultrareview to bypass the file limit, comment @cubic-dev-ai ultrareview. Learn more.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
1 issue found across 33 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| - src/mcp | ||
| - docs_src |
There was a problem hiding this comment.
Shouldn't this just be src? I don't think docs_src is necessary.
There was a problem hiding this comment.
It's load-bearing for mkdocs serve, because the pages don't contain their code blocks any more: every example is a file under docs_src/ pulled in with --8<--, and snippets resolve from the repo root, outside mkdocs' default watch set. With the entry:
INFO - Watching paths for changes: 'docs', 'mkdocs.yml', 'src/mcp', 'docs_src'
and without it docs_src is absent, so editing an example while serve is running doesn't rebuild the page that includes it (same reason src/mcp is in the list, for mkdocstrings). It only affects serve. Happy to drop it if you'd rather, but it's the difference between live-reload working and not for the files a docs author actually edits.
README.v2.md was 2,516 lines and had drifted badly from main: several of its snippets crash today (`json_response` in the MCPServer constructor, `ctx.mcp_server.settings.host`), it never once constructs the high-level `Client`, and it pastes `# pyright: ignore[reportDeprecated]` into reader-facing code to keep teaching deprecated APIs. The mkdocs site was three "Under Construction" stubs untouched for nine months plus a landing page whose only example doesn't run. The root cause was structural, not editorial: nothing forced a doc code block to stay true. This puts that force in place and rewrites the three pages that set the shape, leaving the rest of the tutorial to follow. docs_src/ holds every example as a complete, importable module, so pyright strict and ruff already cover it. Pages include them with pymdownx.snippets `--8<--` (already installed, previously unused), so nothing is ever pasted into markdown; `check_paths: true` plus the existing `strict: true` fail the build on a renamed example. tests/docs_src/ imports each module and drives it through the in-memory `Client(mcp)` -- the assertions ARE the page's claims -- and a per-module filterwarnings mark re-arms the deprecation warning the SDK silences for itself, so an example cannot quietly teach a deprecated API. A small shape test adds: every example imports, no `_`-private mcp import, no retired API, no example a page doesn't show, no include pointing at nothing. All branch-coverage clean with zero pragmas. No new dependencies. The README quickstart, docs/index.md, and its test all read the same file. README.v2.md drops to ~125 lines: pitch, install, one server, one client, a pointer to the site. Pages: index.md and installation.md rewritten (installation was missing five real dependencies, anyio among them); a new Tutorial section with its intro, Tools, and Testing (the old docs/testing.md, kept and expanded -- it now answers the open TODO on what `raise_exceptions=True` does). The concepts/authorization/low-level-server stubs are removed. Examples standardize on `from mcp.server import MCPServer`, the path `mcp/server/__init__.py` exports. Hardening of the existing machinery while here: - shared.yml gains a `docs` job running `mkdocs build --strict` on PRs. Until now that only ran post-merge in deploy-docs.yml, which is how a broken landing-page example survived nine months. - deploy-docs.yml's `paths:` trigger now includes `docs_src/**`. - update_readme_snippets.py: a missing snippet source file is now fatal instead of a warning that lets `--check` pass with exit 0. - tests/test_examples.py also ruff-lints the new docs pages' fences. examples/snippets/ is left in place; most of it becomes unreferenced by the shorter README and is a follow-up removal (it is a uv workspace member, so deleting it touches uv.lock).
The previous commit built the rails (docs_src/ + `--8<--` includes + tests/docs_src/ driving every example through the in-memory Client) and proved them on three pages. This is the rest of the book on those rails. New chapters: - Tutorial: first-steps, structured-output, resources, prompts, context, handling-errors, lifespan, media, completions, elicitation, progress, logging - Running your server: index (stdio/HTTP/CLI/settings), asgi - The Client: index, callbacks, transports, protocol-versions - Advanced: multi-round-trip, low-level-server, pagination, middleware, authorization, oauth-clients, session-groups, deprecated - nav lists all of them; deploy-docs and the fence-lint test cover the new directories Every page is built from complete example files in docs_src/<chapter>/ included with `--8<--`, plus a test module in tests/docs_src/ that drives each example through `Client(mcp)` in memory and asserts what the prose claims (633 tests, 100% branch coverage). tests/docs_src/test_shape.py enforces the global invariants: no orphan example, no include without a file, no private `mcp.*` import, no deprecated or retired API in anything a reader copies. Corrections to the existing pages that fell out of writing the rest: - testing.md: `raise_exceptions=True` does not surface a failing tool's traceback (a tool exception is always an `is_error` result); on the default in-memory connection it stops a non-tool failure being sanitised to "Internal server error". Say so, scoped to that connection. - first-steps.md: the three primitive capabilities are not derived from what you register — an empty MCPServer declares the identical three. Only optional capabilities follow registration; the page and its test now prove it. - mkdocs.yml: nav titles match each page's H1.
… docs tests cover The lowest-direct CI cells failed on tests/docs_src/test_asgi.py reaching into starlette's `Middleware` internals — `.kwargs` does not exist on the oldest supported starlette. The test now drives the app over `httpx.ASGITransport` and asserts what the page promises a browser: the preflight allows GET/POST/DELETE and a cross-origin response exposes `Mcp-Session-Id`. The locked cells failed at `strict-no-cover`: the docs tests execute 21 lines marked `# pragma: no cover` — the `Image`/`Audio` helper paths, the elicitation cancel arm, custom Starlette routes on the low-level app, `Context.request_context` outside a request, and the token_verifier-without-auth `ValueError`. Those markers are no longer true, so they are removed; the full suite still reports 100% under branch coverage.
Review feedback on the new docs: - Em-dashes, arrows, ellipses and the rest of the typographic non-ASCII are gone from every page and example (457 em-dashes); each site is rewritten as the punctuation the sentence wanted. A new test in tests/docs_src/test_shape.py pins the rule for the book's own files. One of these characters was also a real bug: pytest-examples pipes fence source to ruff in the platform encoding, so a U+2026 inside two media.md fences failed ruff on every Windows CI cell. That failure class is now impossible. - The recurring "### Check it" section heading becomes "### Try it". - A few sentences that narrated the documentation instead of the SDK are now plain statements of fact. - The README, index and installation pages pin the newest real pre-release (2.0.0a2) instead of a 2.0.0aN placeholder nobody can install.
- The plain-ASCII check in tests/docs_src/test_shape.py now covers the test modules in tests/docs_src/ as well as the pages and examples, and their docstrings and comments are rewritten to satisfy it. The check's own table of banned characters is spelled as escapes so the file passes the check it implements. - The invalid-`mode=` hint in Client.__post_init__ was the one user-facing string in src/mcp with a typographic dash, and docs/client/protocol-versions.md quotes that error verbatim. It now uses a semicolon; the two tests that pin the message are updated. - testing.md's note on pytest and inline-snapshot no longer speaks in the first person, and two test docstrings now state the invariant they pin.
Each review finding was verified against the SDK before changing anything. - The ASGI chapter taught deployments that do not work as written. With no `transport_security=`, `streamable_http_app()` auto-enables DNS-rebinding protection that accepts only localhost Host/Origin headers, so anything served behind a real hostname was answered `421 Invalid Host header` (and the CORS example's own browser origin `403 Invalid Origin header`) before MCP ran. The browser example also granted none of the `Mcp-*` request headers, so a browser's preflight blocked every request after the first. The page gains a "Localhost only, until you say otherwise" section, the CORS example now carries `TransportSecuritySettings(...)` and `allow_headers`, and two new tests pin the rejection statuses and the documented browser scenario end to end (the latter fails against the old example). - progress.md implied the in-memory timing (callbacks complete before `call_tool` returns) holds on every transport. It is guaranteed by construction in memory; on a wire dispatcher callbacks run as their own tasks and can outlive the call. The info box now says so and a new test pins the wire behaviour. The "Try it" sentence a reviewer challenged is correct as written for the connection it narrates, so it is unchanged. - tools.md's `ToolAnnotations` example paired `idempotent_hint=True` with `read_only_hint=True`, which the spec defines as meaningful only for non-read-only tools; it now shows `open_world_hint=False` and the prose explains the rule. Its `hl_lines` was also off by one, the only misaligned range out of all 70 in the book. - elicitation: the booking example re-validates the date the user accepts by sending it back through the tool, and the "Try it" names which of the page's two servers each step runs against. - installation.md leads with the pinned install command (the unpinned one installs v1.x), adds `mcp-types` to "What gets installed", and reuses the pydantic bullet for what pydantic does now that the protocol types live in `mcp-types`. The README's install command is pinned the same way. - The three tutorial closing pointers that did not follow the nav order (media, progress, logging) now hand off to the next chapter, and testing.md gains the missing final hand-off; all 31 closers were checked. - testing.md no longer calls inline-snapshot optional while showing a test that imports it.
…rogress test Two review nits on the previous commit: - docs/index.md's install tabs were the last unpinned `mcp[cli]` commands in the book. They now pin 2.0.0a2 like installation.md and the README, and the warning box below them explains the pin instead of asking the reader to add one. - The new test in tests/docs_src/test_progress.py waited for the gated callbacks with a sleep poll, which AGENTS.md tells contributors not to do, and used a 10s fail_after where 5 is the standard. The callback now sets an anyio.Event when the second value lands and the test waits on that, under fail_after(5).
2.0.0a3 went out today; the book's install commands pinned a2. Every page tells the reader to use the newest alpha, so they now pin a3.
Three JSON blocks introduced as the schema the SDK sends (two in tools.md, one in context.md) omitted the root "title" key the SDK emits, and one also dropped "type": "object". They now match the output byte for byte, the tutorial002 schema is pinned by a full snapshot like its siblings, and a survey of every schema block in the book found no other trimmed quote.
| The Inspector built that form (a required integer field for `a`, another for `b`) from your type hints. So will Claude, and every other MCP host. | ||
|
|
||
| Now go to **Resources** and read `greeting://World`: | ||
|
|
||
| ```text | ||
| Hello, World! | ||
| ``` |
There was a problem hiding this comment.
🟡 The 'Try it' step says to go to Resources and read greeting://World, but the example server registers greeting as a templated resource (@mcp.resource("greeting://{name}")), so the Inspector's Resources list is empty and the entry only appears under Resource Templates, where the reader has to supply name=World first. Consider mirroring the wording the very next chapter (docs/tutorial/first-steps.md) already uses for the identical resource, e.g. 'find greeting under Resource Templates, give it the name World, and read it'.
Extended reasoning...
What the page says vs. what the server does. docs/index.md line 73 instructs: "Now go to Resources and read greeting://World", followed by the expected output Hello, World!. The example the page includes (docs_src/index/tutorial001.py) registers the resource as a template: @mcp.resource("greeting://{name}"). A templated resource is never listed as a concrete resource, so in the Inspector the Resources list for this server is empty; greeting shows up only in the Resource Templates section, where the reader must type World into the name field before anything can be read. There is no listed entry called greeting://World to click on, so the sentence cannot be followed exactly as written.
It contradicts the very next chapter. docs/tutorial/first-steps.md (line 65) walks through the identical resource on the identical server shape and goes out of its way to explain this exact behaviour: "The Resources list is empty. greeting is under Resource Templates, because greeting://{name} has a parameter: there is no single resource to list until someone supplies a name. Give it World and read it." So the landing page promises an experience that the next page explicitly says will not happen. A first-time reader who hits the empty list on the landing page gets their first moment of "the docs are wrong" on the very first page of a docs rewrite whose stated premise is that every claim is exact and test-backed.
Why the existing tests don't catch it. tests/docs_src/test_index.py exercises tutorial001.py through the in-memory Client (read_resource("greeting://World") works fine programmatically, since resources/read matches templates). The discrepancy is purely about what the Inspector's UI shows and what the prose tells the reader to click, which no test can see.
Step-by-step. (1) The reader pastes server.py from the landing page and runs uv run mcp dev server.py. (2) They follow line 73, open the Resources tab, and look for greeting://World in the resources list. (3) The list is empty — @mcp.resource("greeting://{name}") produces a resource template, which the Inspector lists separately under Resource Templates. (4) They have to notice that section themselves, click greeting, type World into the name field, and read it — a step the page never mentions, but which first-steps.md spells out for the same resource one click later.
On the counter-argument. One reviewer noted that "Resources" is the name of the Inspector tab, and that tab also contains the Resource Templates section — so the reader does land in the right place and will figure it out within seconds. That is fair, and it is why this is a nit rather than a blocker: the friction is small and self-resolving. But the instruction still elides the one step (supplying name) that makes the read possible, and it still tells a different story than the next chapter tells about the same server. The first-steps wording exists precisely because the author judged this worth explaining; the landing page should not contradict it.
How to fix. One phrase: e.g. "Now go to Resources, find greeting under Resource Templates, give it the name World, and read it:" — or any wording that matches first-steps.md. Prose-only, no code or test changes.
The #2978 docs rebuild introduced docs/tutorial/resources.md as the beginner narrative. This PR's RFC 6570 syntax reference, ResourceSecurity configuration, safe_join usage, and lowlevel examples move to docs/advanced/uri-templates.md alongside the other advanced pages. Adds a forward link from the tutorial.
The #2978 docs rebuild introduced docs/tutorial/resources.md as the beginner narrative. This PR's RFC 6570 syntax reference, ResourceSecurity configuration, safe_join usage, and lowlevel examples move to docs/advanced/uri-templates.md alongside the other advanced pages. Adds a forward link from the tutorial.
The #2978 docs rebuild introduced docs/tutorial/resources.md as the beginner narrative. This PR's RFC 6570 syntax reference, ResourceSecurity configuration, safe_join usage, and lowlevel examples move to docs/advanced/uri-templates.md alongside the other advanced pages. Adds a forward link from the tutorial.
The #2978 docs rebuild introduced docs/tutorial/resources.md as the beginner narrative. This PR's RFC 6570 syntax reference, ResourceSecurity configuration, safe_join usage, and lowlevel examples move to docs/advanced/uri-templates.md alongside the other advanced pages. Adds a forward link from the tutorial.
What this is
A ground-up rewrite of the SDK's documentation as a tutorial, replacing most of the 2,500-line
README.v2.mdwith adocs/tree where every code example is a complete file that the test suite imports and runs.No new dependencies and no new infrastructure: the pieces (
pymdownx.snippets,pytest-examples, the in-memoryClient, mkdocs strict mode) were already in the repo. This PR makes them load-bearing.How a chapter works
So a page can say "call it with
limit=100and the call fails before your function runs" and there is a test named after that sentence proving it. When the SDK changes behaviour, the docs fail in CI instead of drifting.The book
mcp.run(), stdio/Streamable HTTP, the CLI, settings; ASGI mounting and deploymentServer, pagination, middleware, authorization, OAuth clients, session groups, deprecated featuresREADME.v2.mdshrinks to a short intro (badges, one server, one client) that points at the docs;docs/concepts.md,docs/authorization.md,docs/low-level-server.mdanddocs/testing.mdare replaced by chapters.Validation (all of it runs in CI)
tests/docs_src/: 649 tests drive every example through the in-memoryClient, at 100% branch coverage.tests/docs_src/test_shape.py: book-wide invariants. Everydocs_srcfile is included by a page, every--8<--target exists, no privatemcpimport, no deprecated or retired API in anything a reader copies, plain-ASCII punctuation, every example imports.tests/test_examples.py(pytest-examples) lints every inline fence on every page.docsjob runsmkdocs build --stricton PRs, so a broken include or nav entry is a red check.docs_src/is type-checked by pyright and linted/formatted by ruff like any other source.Notes for reviewers
advanced/deprecated.md, and used by zero examples; a per-module warning filter intests/docs_src/turns an accidental use into a hard test failure.src/mcpchanges ride along: 21# pragma: no covermarkers on lines the new tests execute are removed (strict-no-coverrequires it), and theClient(mode=...)error hint now uses a semicolon instead of a U+2014 dash, becausedocs/client/protocol-versions.mdquotes that error verbatim and the docs use plain-ASCII punctuation throughout.if __name__ == "__main__"block, documentMCPServer(website_url=...).AI Disclaimer