Skip to content

Rebuild the docs around tested examples; shrink README.v2.md to a pitch#2978

Merged
maxisbey merged 9 commits into
mainfrom
docs-rework
Jun 26, 2026
Merged

Rebuild the docs around tested examples; shrink README.v2.md to a pitch#2978
maxisbey merged 9 commits into
mainfrom
docs-rework

Conversation

@maxisbey

@maxisbey maxisbey commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

What this is

A ground-up rewrite of the SDK's documentation as a tutorial, replacing most of the 2,500-line README.v2.md with a docs/ 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-memory Client, mkdocs strict mode) were already in the repo. This PR makes them load-bearing.

How a chapter works

docs_src/tools/tutorial004.py      # a complete, runnable server: the example IS a file
docs/tutorial/tools.md             # the page includes it:  --8<-- "docs_src/tools/tutorial004.py"
tests/docs_src/test_tools.py       # drives it through Client(mcp) in memory and asserts
                                   # exactly what the page's prose claims

So a page can say "call it with limit=100 and 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

  • Tutorial - User Guide (14): first steps, tools, structured output, resources, prompts, the Context, handling errors, lifespan, media, completions, elicitation, progress, logging, testing
  • Running your server (2): mcp.run(), stdio/Streamable HTTP, the CLI, settings; ASGI mounting and deployment
  • The Client (4): the client object, callbacks, client transports, protocol versions
  • Advanced (8): multi-round-trip requests, the low-level Server, pagination, middleware, authorization, OAuth clients, session groups, deprecated features
  • README.v2.md shrinks to a short intro (badges, one server, one client) that points at the docs; docs/concepts.md, docs/authorization.md, docs/low-level-server.md and docs/testing.md are replaced by chapters.

Validation (all of it runs in CI)

  • tests/docs_src/: 649 tests drive every example through the in-memory Client, at 100% branch coverage.
  • tests/docs_src/test_shape.py: book-wide invariants. Every docs_src file is included by a page, every --8<-- target exists, no private mcp import, 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.
  • A new docs job runs mkdocs build --strict on 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

  • The deprecated 2026-07-28 surfaces (sampling push, roots, protocol logging) are documented in exactly one place, advanced/deprecated.md, and used by zero examples; a per-module warning filter in tests/docs_src/ turns an accidental use into a hard test failure.
  • Two small src/mcp changes ride along: 21 # pragma: no cover markers on lines the new tests execute are removed (strict-no-cover requires it), and the Client(mode=...) error hint now uses a semicolon instead of a U+2014 dash, because docs/client/protocol-versions.md quotes that error verbatim and the docs use plain-ASCII punctuation throughout.
  • Deliberate follow-ups, not in this PR: turn the bold cross-references into links, give the client-side examples an if __name__ == "__main__" block, document MCPServer(website_url=...).

AI Disclaimer

@maxisbey maxisbey marked this pull request as ready for review June 26, 2026 05:31

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread docs/installation.md Outdated
Comment thread docs_src/elicitation/tutorial001.py
Comment thread docs/tutorial/progress.md
Comment thread docs_src/asgi/tutorial005.py
Comment thread docs/run/asgi.md Outdated
Comment thread docs/installation.md
Comment thread docs/tutorial/logging.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread docs/tutorial/testing.md Outdated
Comment thread docs/tutorial/elicitation.md Outdated
Comment thread tests/docs_src/test_progress.py Outdated
Comment thread docs/index.md
Comment thread docs/tutorial/tools.md
Comment thread mkdocs.yml
Comment on lines 139 to +140
- src/mcp
- docs_src

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't this just be src? I don't think docs_src is necessary.

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.

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.

AI Disclaimer

maxisbey added 9 commits June 26, 2026 10:08
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.
Comment thread docs/index.md
Comment on lines +71 to 77
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!
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

@maxisbey maxisbey merged commit 411a6d3 into main Jun 26, 2026
33 checks passed
@maxisbey maxisbey deleted the docs-rework branch June 26, 2026 10:49
maxisbey added a commit that referenced this pull request Jun 26, 2026
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.
maxisbey added a commit that referenced this pull request Jun 26, 2026
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.
maxisbey added a commit that referenced this pull request Jun 26, 2026
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.
maxisbey added a commit that referenced this pull request Jun 26, 2026
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.
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.

2 participants