docs: restructure into topical sections and add the four most-asked-for pages#3044
Conversation
📚 Documentation preview
|
There was a problem hiding this comment.
1 issue found across 26 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/tutorial/testing.md">
<violation number="1" location="docs/tutorial/testing.md:101">
P3: Overstates test guarantee: not every docs example is exercised through this Client path. Scope the claim to this page/tutorial example to avoid misleading readers.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| That one line is also why these docs can promise you that their examples work: every | ||
| example file is exercised by the SDK's own test suite through exactly this client. You're using the | ||
| same tool the SDK uses on itself. |
There was a problem hiding this comment.
P3: Overstates test guarantee: not every docs example is exercised through this Client path. Scope the claim to this page/tutorial example to avoid misleading readers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/tutorial/testing.md, line 101:
<comment>Overstates test guarantee: not every docs example is exercised through this Client path. Scope the claim to this page/tutorial example to avoid misleading readers.</comment>
<file context>
@@ -98,9 +98,9 @@ Leave it on in tests. It has no meaning in production code.
failure inside the server task instead of in your test.
-That one line is also why the rest of this tutorial can promise you that its examples work: every
+That one line is also why these docs can promise you that their examples work: every
example file is exercised by the SDK's own test suite through exactly this client. You're using the
same tool the SDK uses on itself.
</file context>
| That one line is also why these docs can promise you that their examples work: every | |
| example file is exercised by the SDK's own test suite through exactly this client. You're using the | |
| same tool the SDK uses on itself. | |
| That one line is also why this page can promise this example works: this | |
| example file is exercised by the SDK's own test suite through exactly this client. You're using the | |
| same tool the SDK uses on itself. |
There was a problem hiding this comment.
2 issues found across 23 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/servers/media.md">
<violation number="1" location="docs/servers/media.md:33">
P3: This wording introduces a sentence fragment and makes the docs harder to understand. Restore the missing connector so the relationship to `TextContent` is explicit.</violation>
</file>
<file name="docs/client/oauth-clients.md">
<violation number="1" location="docs/client/oauth-clients.md:90">
P3: “Every other example in these docs” is too broad: identity assertion and other HTTP-focused examples also cannot be checked with an in-memory client. Narrow the wording to non-auth/introductory examples.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
mkdocs.yml has enabled `navigation.path` (the breadcrumb trail) since the docs landed, but it is a mkdocs-material 9.7.0 feature: the current `>=9.5.45` floor lets a fresh resolve land on a 9.5/9.6 release that silently drops it. Raise the floor to what the config actually needs. There is currently no redirect machinery in these docs at all, so any page rename 404s every existing inbound link, including the ones in the published llms.txt. Add mkdocs-redirects so renames can carry a redirect map. It is inert until a `redirect_maps` is configured; nothing in this branch renames a page.
The docs render as one flat 15-chapter "Tutorial - User Guide" plus a
15-item "Advanced" grab-bag (whose sidebar heading is a dead label: it has
no index page), and the section a deploying user needs most, "Running your
server", has exactly one child, titled "ASGI".
Regroup the same 40 pages into sections a reader would actually scan for:
Get started install -> first server -> test it
Servers one page per thing a server exposes; Tools first
Inside your handler the Context, dependencies, and everything a
running handler can do
Running your server now also owns Authorization and OpenTelemetry
Clients now also owns OAuth, identity assertion,
connecting to multiple servers, and the cache
Advanced only the genuine escape hatches (5, was 15),
with a real, clickable index page
"Protocol versions" and "Deprecated features" become their own top-level
entries. The first is the one page that squarely explains the two protocol
eras and was buried as the last child of "The Client", where a server
author never looks. The second is the SEP-2577 retirement table, filed
dead last in "Advanced".
Not a single file moves. MkDocs nav sections, nav titles, and file paths
are three independent things, so this is an mkdocs.yml edit plus three
new ~200-word section index pages (docs/servers/, docs/handlers/,
docs/advanced/). Every existing URL, every `--8<--` include, and every
docs_src test is untouched, and Material's breadcrumbs follow the nav,
not the directory.
docs/tutorial/index.md is rewritten from a "how these docs are built"
meta page into the Get started doorway; its tested-examples promise is
kept. tests/test_examples.py gains the two new docs directories so their
fenced code blocks stay lint-covered, and AGENTS.md's description of how
the docs are organised is updated to match.
The pages chain into each other with end-of-page "Next: ..." hand-offs. Under the regrouped nav some of those pointed backward, or across a section boundary, or at a page by a title it no longer carries. Fix each one to hand off to the page that actually follows it in its section, or to the next section where it is the last page. Also: - "ASGI" (the page H1 and every link to it) becomes "Add to an existing app". The old title named a Python interface standard; the page's content is mounting into an existing Starlette/FastAPI app, the DNS-rebinding 421, and CORS -- none of which anyone finds under the word "ASGI". (#1798 is literally a user asking for a "Guide" to content that already exists on that page.) - The landing page and README stop calling the docs "the tutorial"; the section they named is now "Get started" and the body of the docs is a reference, not a course. - Three sentences that said "this tutorial" now say "these docs"; there is no longer a tutorial for them to be in. - A pre-existing factual error on completions.md is fixed while its closing line is retargeted: completions apply to prompt arguments and resource-template parameters, never to a tool's, but the sentence said "Suggestions help before a tool runs."
The nav regroup put pages from docs/tutorial/ under "Servers", pages from docs/advanced/ under "Clients", and so on -- the sidebar was right but the on-disk layout and every URL still described the old grouping. Move each of the 28 affected pages so its directory matches its section, under one rule: the directory follows the section and the filename stem never changes. docs/tutorial/ is gone. Every one of the 28 old URLs gets an entry in mkdocs-redirects' redirect_maps, so nothing 404s; under `strict: true` a stale redirect target is itself a build failure, and the rendered redirect stubs were spot-checked. Every relative inter-page link is recomputed for the pages' new locations, and the strict build (which fails on any broken link, nav path, or anchor) validates all of them. The other things that reference the moved paths move in lockstep: - The tests/docs_src/ module docstrings, two example READMEs, and RELEASE.md all name docs pages by path. - tests/test_examples.py's find_examples() directory list is rewritten for the new layout, and gains the two pages that are now at the docs root (protocol-versions.md, deprecated.md) and would otherwise silently lose the inline-code-block lint coverage. None of docs_src/ moves: the `--8<--` snippet includes are repo-root- relative, so the 120 tested example files and their tests are untouched. The "Inside your handler" section index also drops an over-broad claim while it is being moved into: Elicitation and Multi-round-trip requests are not Context verbs (Resolve is an annotated parameter and MRTR is a return value; only the legacy `ctx.elicit` path touches Context), so the Context bullet now names only the progress and change-notification verbs it actually carries.
The pages were written for a linear read-through, so many refer to the
reader's history on ANOTHER page -- "In Tools you returned a str and the
result came back twice", "the input schema you met in Tools", "the same
one you use in Testing", "So far every request has gone one way", "you
already know". Most people arrive at reference docs from a search engine
and read one page; for them those sentences are false and read as steps
in a walkthrough they are not on.
An audit of all 42 pages found 24 such sentences on 16 pages (26 pages
were already clean). Each is rewritten to carry the SAME facts and,
almost always, the SAME cross-link, with only the false claim about the
reader's history removed:
In [Tools] you returned a str and the result came back twice ...
-> A tool that returns a plain str produces the result twice ...
the TextContent you met in [Tools]
-> the TextContent a plain str result becomes ([Tools])
You saw this in [Tools] with Field(le=50).
-> [Tools] shows the same rejection with a Field(le=50) constraint.
Cross-REFERENCES are deliberately untouched: routing the reader
elsewhere for MORE ("the full addressing syntax is on [URI templates]")
is what good reference docs do. Only a sentence that DEPENDS on another
page to make sense is the bug. Every rewritten factual claim was
re-verified against the SDK source, not against the docs.
Also:
- The word "chapter" becomes "page" everywhere (27 sites): a book has
chapters, a reference has pages.
- The landing page's "Where to go next" gains the two audience routes it
was missing: someone building a CLIENT, and someone adding MCP to an
app they already run. The README routes both; the docs did not.
- migration.md's Tasks note is corrected. It said Tasks "are expected to
return as a separate MCP extension in a future release"; the
2026-07-28 revision reintroduces them as SEP-2663
(io.modelcontextprotocol/tasks), redesigned around polling. This SDK
does not implement the extension yet, and the note now says so.
Ten pages closed with a 'Next: ...' / '... is next' hand-off to the page that used to follow them in the retired linear read-through. The pointer and the link are worth keeping -- the word 'Next' is not: it tells a reader who arrived at one page from a search engine that they are on a course, which is exactly the tutorial framing this series of changes removes. Each hand-off keeps its full sentence and its link and loses only the sequencing word: Next: telling connected clients that something changed ... with [Subscriptions]. -> Telling connected clients that something changed ... is [Subscriptions]. The one 'Next:' inside Get started (first-steps.md) is deliberately untouched: that section IS a guided sequence, and there the word is correct.
Three unrelated small fixes on existing pages.
Era wording. Five sentences described a legacy mechanism as a universal
truth. At protocol revision 2026-07-28 there is no initialize handshake
(the client sends one server/discover probe), so 'the server's half of
the handshake', 'advertised it during the handshake', 'icons arrive
during the handshake', and -- best of all -- 'the [server/discover]
result is cacheable' being explained as 'the handshake result' were each
wrong for a modern connection. Each is reworded to the era-neutral truth
(capabilities are declared to every connecting client, however it
connected). Pages ABOUT the handshake (Protocol versions, Session
groups, the migration guide, the two OAuth 'handshakes' that are not
MCP's) are deliberately untouched.
Auth router. authorization.md and oauth-clients.md are the two halves
of one flow and the most-confused pair in the docs. oauth-clients.md
already corrects a wrong-lander in its opening lines ('This page is the
client side. Making your own server demand a token is Authorization.');
authorization.md only did so in its final line. It now has the mirror
sentence up top, so someone who clicked the wrong one finds out in
sentence three, not paragraph forty.
CIMD. The 2026-07-28 revision deprecates OAuth Dynamic Client
Registration in favor of Client ID Metadata Documents, and the SDK
already implements the client side (client_metadata_url= on
OAuthClientProvider) -- but the page never said so: it presented
dynamic registration as THE registration mechanism and named
client_metadata_url once, in passing, with no explanation. A short
section now covers what CIMD is, the one argument that enables it, the
exact condition under which it is used, that the fallback to dynamic
registration is silent, and the construction-time ValueError on a bad
URL. Deliberately prose-only: the SDK's own authorization server cannot
advertise CIMD support, so no runnable docs example can honestly
demonstrate the selection.
The page taught ctx.elicit() first and unqualified, with the resolver second (under a heading about WHEN it runs, 'Ask before the tool runs') and the protocol-era constraint disclosed only in an info box inside the client section, 115 lines down. That is backwards. ctx.elicit() and ctx.elicit_url() are requests from the server to the client, a channel that only exists for a client on a legacy connection (spec 2025-11-25 and earlier); a Resolve-annotated parameter is deliberately era-portable -- the SDK sends elicitation/create on a legacy connection and a multi-round-trip result on a modern one, and the handler never knows the difference. The page now opens by naming both ways to ask and saying which one to reach for; 'Ask with a resolver' is the first section and states the portability out loud; and the two ctx verbs' section carries the legacy-connection warning at its top instead of a hundred lines later. The 'Try it' walkthrough named its server as 'the first one on this page', which the reorder would have silently falsified; it now names it. No example changes: the resolver example the page now leads with was already there and already CI-tested. It was just filed second.
Troubleshooting, Deploy & scale, Connect to a real host, and Serving legacy clients. Each answers a question the issue tracker shows users asking repeatedly and the docs never answering, and each follows this doc set's rule that every code block is an executable file under docs_src/, included by the page and exercised by the test suite. 44 new tests; the docs suite goes from 859 to 973. Troubleshooting (top level). Every heading is the exact text of an error the SDK produces -- the CLIENT-side text where that is what a user actually sees -- followed by what it means and the one-move fix, and every quoted error is reproduced by a test. It opens with the one that wraps all the others: anyio's "ExceptionGroup: unhandled errors in a TaskGroup", which every exception escaping `async with Client(...)` arrives inside, so the first thing the page teaches is to read the last line of the paste. Deploy & scale (Running your server). The DNS-rebinding Host allowlist -- the most-reported deployment failure by a wide margin -- moves here from "Add to an existing app", which keeps a short warning and a pointer: it is a deploy gate, not an add-to-my-app concern, and its config example is now a tested file where before it was the doc set's one untested inline snippet. Then the two things "more than one worker" actually changes. A multi-round-trip retry that lands on a different worker fails with the frozen -32602 "Invalid or expired requestState" because the default sealing key is os.urandom(32) per process; the fix is RequestStateSecurity(keys=[...]) shared across instances AND the same server name on every instance, because the name is the seal's default audience claim -- the half nobody finds. Change notifications cross replicas only through a shared SubscriptionBus, a two-method Protocol you implement over your own pub/sub. Both are proved by tests that run two server instances in memory, the wrong way and the right way. Connect to a real host (Get started). A host needs one thing from you: the command that starts your server over stdio. One tested server file, then one short section per host. `mcp install` supports exactly one host -- Claude Desktop -- so the page shows the exact JSON it writes and where it writes it, and gives Claude Code, Cursor, and VS Code their one config block each. Serving legacy clients (Running your server). The streamable_http_app() you already deploy serves both protocol eras, routed per request on the version header; there is nothing to configure and no era knob. What a legacy client costs you is a session -- so more than one worker means sticky routing, since sessions live in an in-process dict -- and the one knob, stateless_http=True, is legacy-leg-only and trades away both server-to-client channels on that leg. The page's central example is one server with one Resolve-based tool serving a legacy and a modern client concurrently, entirely in memory, which is the whole pitch in one test. The pages that should route to the new ones now do: the landing page, the Get started sequence and its index, the Running your server index, the Testing hand-off, and Handling errors.
There was a problem hiding this comment.
3 issues found across 52 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/tutorial/testing.md">
<violation number="1" location="docs/tutorial/testing.md:101">
P3: Overstates test guarantee: not every docs example is exercised through this Client path. Scope the claim to this page/tutorial example to avoid misleading readers.</violation>
</file>
<file name="docs_src/deploy/tutorial003.py">
<violation number="1" location="docs_src/deploy/tutorial003.py:20">
P2: The refund flow can be completed without a prior confirmation round-trip if a caller sends `input_responses` on the initial request. Checking only `ctx.input_responses` allows bypassing the intended human-confirmation gate; gating on `ctx.request_state` preserves the two-step flow.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| @mcp.tool() | ||
| async def refund(amount: int, ctx: Context) -> str | InputRequiredResult: | ||
| """Refund an amount, once a human has confirmed it.""" | ||
| if ctx.input_responses is None: |
There was a problem hiding this comment.
P2: The refund flow can be completed without a prior confirmation round-trip if a caller sends input_responses on the initial request. Checking only ctx.input_responses allows bypassing the intended human-confirmation gate; gating on ctx.request_state preserves the two-step flow.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs_src/deploy/tutorial003.py, line 20:
<comment>The refund flow can be completed without a prior confirmation round-trip if a caller sends `input_responses` on the initial request. Checking only `ctx.input_responses` allows bypassing the intended human-confirmation gate; gating on `ctx.request_state` preserves the two-step flow.</comment>
<file context>
@@ -0,0 +1,27 @@
+ @mcp.tool()
+ async def refund(amount: int, ctx: Context) -> str | InputRequiredResult:
+ """Refund an amount, once a human has confirmed it."""
+ if ctx.input_responses is None:
+ return InputRequiredResult(input_requests={"ok": CONFIRM}, request_state=f"refund:{amount}")
+ answer = (ctx.input_responses or {}).get("ok")
</file context>
Rebasing onto main picked up #3040, which hardened the era semantics: a server-initiated request on a 2026-07-28 connection is now refused by the SERVER, as MCPError: Cannot send 'elicitation/create': this transport context has no back-channel for server-initiated requests. instead of reaching the client and being rejected there as "Method not found". Two of this branch's docs tests failed on the rebase -- which is the point of testing every example -- and the troubleshooting page keyed its biggest elicitation entry on the old string. So the two entries merge into the one that owns the surviving string. "Method not found" is now short and generic (an era mismatch: a method one protocol revision has and the other does not), and says explicitly that ctx.elicit() at 2026-07-28 no longer produces it. The "Cannot send 'elicitation/create' ..." entry becomes the single home for "your handler reached back and nothing can carry it", with its two real triggers -- any 2026-07-28 connection, and a legacy connection on a stateless_http=True server -- both shown from tested examples, and the one fix (a resolver). The tests pin the new behaviour, the same way #3040 itself updated tests/docs_src/test_client_callbacks.py.
5267648 to
01128d6
Compare
"Connect to a real host" showed a server file ending in mcp.run() and never said what that call does, which is the one fact the whole page stands on: with no arguments it is a STDIO server -- it blocks, reads protocol messages on stdin, writes them on stdout, and never opens a port. That is why every host on the page is configured with a command rather than an address. Running your server explains this, but a reader lands on this page from the Get started sequence (or a search) without having read that one. The same page now also answers the two questions a reader has next, in one note: this is the LOCAL story (to serve people who don't have your file, you hand out a URL -- Running your server, then Deploy & scale), and a host is just an application with an MCP client inside, so your own Python can play that part (Client transports launches the same file with stdio_client). Client transports points back the other way.
CI requires 100% branch coverage over tests/ as well as src/, and the new docs test modules left four statements and a handful of branch arcs uncovered. The statements were real gaps, each closed by making the test stronger rather than weaker: - test_deploy: the shared-key-different-name test now also lands the retry back on the instance that minted the token and asserts it completes. That is the half of the story the page tells, and it is exactly what the sibling default-key test already proved. - test_troubleshooting: two decorated functions whose bodies can never run (one's decoration is what raises; the other is the duplicate that gets dropped) now have docstring-only bodies, and the connection-fails case enters the client explicitly with __aenter__() (the shape tests/client/test_client.py already uses) instead of an `async with` whose body is unreachable by design. The rest were not real: every remaining flagged line executes (zero missed statements); coverage.py misattributes arcs around nested `async with` bodies on newer Pythons, worst on 3.14, which is exactly the case AGENTS.md documents for `# pragma: no branch` (branch arcs only; ~180 existing uses across src/ and tests/). Six of those, one of them on a straight-line test that raises nothing at all. ./scripts/test (the CI-equivalent gate) now reports 100.00% and strict-no-cover passes.
By maintainer request: no em-dashes or other typographic non-ASCII in the PR's prose, and each removal must restructure the sentence rather than substitute a character. The PR's added lines carried 124 of them, all in the four new pages, the three new section indexes, and the sentences this PR rewrote on existing pages. Each one was rewritten by reading the sentence it was in. A paired aside became parentheses or its own sentence; a dash before an elaboration became a colon or a new sentence; a dash gluing on a reason or a consequence became "because" or "so" or a full stop; the "title -- definition" bullets on the section indexes gained real verbs. No meaning, link, emphasis, or code changed, and pre-existing prose on pages this PR merely edits is untouched. Every line the PR adds is now pure ASCII; the commit was gated on grepping the whole added-line diff for non-ASCII and finding nothing.
A review pass left 13 comments; nine needed action and each fix is a word or a line. - testing.md over-claimed that every example runs "through exactly this client". Every example file IS exercised by the suite, but two of the 42 docs test modules never construct a Client (the OAuth examples cannot be driven that way), so it now says "almost all of them". - oauth-clients.md's "Every other example in these docs you can check with an in-memory Client(server)" had the same shape (the identity assertion example cannot be either); now "Most examples". - handling-errors.md's new hand-off promised Troubleshooting covers "every error the SDK produces". The Troubleshooting page scopes itself the honest way round, so the hand-off now matches it. - dependencies.md's closing hand-off read "State your server builds once at startup ... is the Lifespan", which garden-paths as an imperative; it gained its determiner and its head noun. - asgi.md's tip said "the next section is what host= actually controls", but this PR moved that explanation to Deploy & scale; the tip now points there. - media.md gains a relative pronoun: "the TextContent that a plain str result becomes". - README's two get-started links still pointed at the old /v2/tutorial/ URL, which this PR turns into a redirect stub; both now point at /v2/get-started/. - docs/hooks/llms_txt.py's docstring example path named the removed tutorial/ directory; it now names the moved page's real path. - RELEASE.md's list of version-pin locations gains the new docs/get-started/real-host.md (which pins the version seven times), and examples/README.md no longer claims the simple-auth pair is linked from docs/advanced/ (this PR moved every page that links it). Two other comments were already addressed by earlier commits on this branch (the chapter-to-page sweep and the branch-coverage fix), and two were declined with evidence. The claimed "sentence fragment" in media.md is a complete sentence (a contact relative clause) read one wrapped line at a time. And gating the deploy example's refund on request_state instead of input_responses would not add a human-confirmation guarantee: input_responses is a wire parameter only the MCP client can send, and that same client authors the elicitation answer on the honest second round; the SDK's own Client documents seeding input_responses on the first call.
Review call by the maintainer. The docs site's URLs are days old (the /v2/ book shipped just before 2.0.0b1) and v2 is a beta, so there is nothing meaningful to keep alive at the 28 moved pages' old URLs. Remove the mkdocs-redirects plugin, its dependency, and the 28-entry redirect map. Anyone holding a days-old deep link lands on the themed 404 page, which carries the full navigation. The mkdocs-material floor bump that arrived in the same dependency commit stays: navigation.path is enabled in mkdocs.yml and genuinely requires 9.7.0.
|
|
||
| ## You will not be guessing | ||
|
|
||
| Every example in these docs is a complete file under [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) in the SDK's own repository, and every one of them is exercised by the SDK's test suite through an **in-memory client**: |
There was a problem hiding this comment.
🟡 docs/get-started/index.md line 22 still carries the un-hedged claim that "every one" of the docs examples "is exercised by the SDK's test suite through an in-memory client", but this PR broadens that sentence's scope from the tutorial to the whole doc set, which now includes examples that are explicitly not checked that way (the OAuth clients page says so itself, and the new deploy/legacy-clients/troubleshooting examples run over httpx.ASGITransport). The same sentence in docs/get-started/testing.md was already hedged to "almost all of them through exactly this client" in response to review, so hedging index.md the same way (or scoping it to the Get started section) restores consistency.
Extended reasoning...
What the bug is. The new docs/get-started/index.md (line 22, in the "You will not be guessing" section) says: "Every example in these docs is a complete file under docs_src/ ... and every one of them is exercised by the SDK's test suite through an in-memory client". When this sentence lived on docs/tutorial/index.md its scope was the tutorial only, where the claim held. This PR renames the section to "Get started" and keeps the sentence's "these docs" wording while the doc set it now describes has grown to include pages whose examples are explicitly not checked with an in-memory Client(server).
The specific evidence.
docs/client/oauth-clients.md— reworded by this same PR — says: "Most examples in these docs you can check with an in-memoryClient(server). Not this: the whole point of the flow is an HTTP401, and there is no HTTP between an in-memory client and its server." Its examples (docs_src/oauth_clients/tutorial001-002.py) are exercised bytests/docs_src/test_oauth_clients.pywithout ever opening an in-memory client.- Several examples added by this PR are exercised over
httpx.ASGITransport/streamable_http_clientrather than the in-memory client:docs_src/deploy/tutorial001.py(tests/docs_src/test_deploy.py::test_the_allowlisted_app_serves_its_hostname_and_still_rejects_others),docs_src/legacy_clients/tutorial002.py(test_legacy_clients.py::test_stateless_http_kills_the_legacy_back_channel_and_only_the_legacy_one), anddocs_src/troubleshooting/tutorial003/004/005/008.py(test_troubleshooting.py). docs/deprecated.mdhas nodocs_src/example at all, by design (tests/docs_src/test_deprecated.py's module docstring says so).
Why this is an inconsistency the PR itself introduced. The identical sentence in docs/get-started/testing.md was flagged during review (the cubic comment on the old docs/tutorial/testing.md line 101) and this PR fixed it: it now reads "every example file is exercised by the SDK's own test suite, almost all of them through exactly this client". The index page carries the same broadened claim without the hedge, so the doc set now says three different things about the same guarantee: index.md says "every one", testing.md says "almost all", and oauth-clients.md says "most ... not this". The earlier review comment addressed only testing.md, so this location is not a duplicate of an already-resolved thread.
Step-by-step proof. (1) Read docs/get-started/index.md line 22: "every one of them is exercised by the SDK's test suite through an in-memory client". (2) Open tests/docs_src/test_oauth_clients.py: it constructs providers and asserts on TokenStorage/inspect.signature properties; there is no Client(...) context manager anywhere in the file, so docs_src/oauth_clients/* is a docs example not exercised through an in-memory client. (3) Open tests/docs_src/test_deploy.py::test_the_allowlisted_app_serves_its_hostname_and_still_rejects_others: docs_src/deploy/tutorial001.py's app is driven with httpx.AsyncClient(transport=httpx.ASGITransport(app=...)), again not the in-memory Client(server) path. So the universal claim is false for at least these examples, while the sibling page testing.md (fixed in this PR) and oauth-clients.md (also fixed in this PR) already state the accurate, hedged version.
Impact. Prose accuracy only — nothing functional breaks, no test fails, and the substance of the promise (examples are complete files, exercised in CI) remains true. But it is exactly the overstatement the author already agreed to fix on testing.md, so leaving the index page un-hedged undoes the point of that fix and contradicts the OAuth page a few clicks away.
How to fix. Hedge the sentence the same way testing.md was hedged — e.g. "...and almost all of them are exercised by the SDK's test suite through an in-memory client" — or scope it to the Get started section ("Every example in this section..."), which is where the claim still holds verbatim.
Regroup the 40 docs pages from one flat 15-chapter tutorial plus a 15-item "Advanced" grab-bag into topical sections a reader would scan for, move each page's file into the directory of its new section, reword every page to stand on its own, and add the four pages users were filing issues instead of finding: Troubleshooting, Deploy & scale, Connect to a real host, and Serving legacy clients.
Motivation and Context
The current docs are excellent page-by-page but render as one continuous book, and the four questions the issue tracker sees most have no page. Three kinds of change, in commits that each stand alone:
1. Structure (the nav, then the files)
The regroup and the file moves are separate commits on purpose: the regroup moves zero files (nav sections, nav titles, and file paths are three independent things), and a purely mechanical commit then moves each of the 28 affected pages so its directory matches its section, under one rule (the directory follows the section; the filename stem never changes).
docs/tutorial/is gone, and the old URLs are deliberately not redirected: the docs site is days old and v2 is a beta, so there is nothing meaningful to keep alive. None ofdocs_src/moves: the--8<--snippet includes are repo-root-relative.The new left panel
2. Wording: every page stands on its own
The pages were written for a linear read-through, so many referred to the reader's history on another page ("In Tools you returned a
strand the result came back twice...", "the input schema you met in Tools", "you already know"). Most people arrive at reference docs from a search engine and read one page; for them those sentences are false. An audit of all 42 pages found 24 such sentences on 16 pages; each is rewritten to carry the same facts and (almost always) the same cross-link with only the false claim about the reader's history removed. Cross-references are deliberately untouched: routing the reader elsewhere for more is what good reference docs do. Only a sentence that depends on another page to make sense was the bug. Also: "chapter" becomes "page" everywhere, the page-bottom "Next: ..." course framing is dropped outside the (deliberately sequential) Get started section, five sentences that stated a legacy-only mechanism as a universal truth are made era-accurate, and the elicitation page now leads with the era-portable resolver instead of the legacyctx.elicit()verb.3. Four new pages
Every code block on every new page is an executable file under
docs_src/, included by the page and exercised by the test suite, which grows from 859 to 973 tests.ExceptionGroup, which every exception escapingasync with Client(...)arrives inside.requestStateacross workers (shared keys and the same server name, because the name is the seal's default audience claim), and change notifications across replicas through a sharedSubscriptionBus. Both are proved by tests that run two server instances in memory, the wrong way and the right way.mcp installsupports exactly one host (Claude Desktop), so the page shows the exact JSON it writes and where, and gives Claude Code, Cursor, and VS Code their one config block each.streamable_http_app()serves both protocol eras with nothing to configure; what a legacy client costs you is a session (so multiple workers need sticky routing);stateless_http=Trueis legacy-leg-only and trades away the server-to-client channels. Its central example is the dual-era pitch as a passing test: one server, oneResolvetool, and a legacy and a modern client served concurrently in memory.How Has This Been Tested?
mkdocs build --strictis clean, and under strict mode a missing nav path, a page absent from the nav, a broken inter-page link or anchor, and a bad--8<--include are each a build failure../scripts/test(coverage, branch mode,fail_under = 100) passes. All pre-commit hooks pass on every commit.src/mcp/(not against other docs), every verbatim error and log string was grep-verified against the SDK's literals, and the rendered site was spot-checked (no literal--8<--survives into the HTML; the new pages render).Breaking Changes
No code or API changes. The moved pages' old URLs are not redirected, deliberately: the docs site is days old and v2 is pre-release, and the themed 404 page carries the full navigation.
Types of changes
Checklist
Additional context
mkdocs-materialfloor is raised to>=9.7.0becausenavigation.path(the breadcrumb feature, already enabled inmkdocs.yml) shipped in the 9.7.0 community edition; the previous>=9.5.45floor let a fresh resolve silently drop a configured feature.docs/migration.mdremains the one migration page.AI Disclaimer