Skip to content

docs: restructure into topical sections and add the four most-asked-for pages#3044

Merged
maxisbey merged 15 commits into
mainfrom
docs-restructure
Jul 1, 2026
Merged

docs: restructure into topical sections and add the four most-asked-for pages#3044
maxisbey merged 15 commits into
mainfrom
docs-restructure

Conversation

@maxisbey

@maxisbey maxisbey commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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)

  • "Running your server", the section a deploying user needs most, had exactly one child, and it was titled "ASGI". That page actually held the DNS-rebinding 421 answer, mounting into an existing FastAPI/Starlette app, and CORS; Guide: Resolving "421 Invalid Host Header" (DNS Rebinding Protection) #1798 is literally a user filing an issue asking for a "Guide" to content that already existed on it. It becomes "Add to an existing app", and the section grows Authorization, OpenTelemetry, and two of the new pages.
  • "Advanced" held 15 pages, only 2 of which were honestly advanced. Every misfiled page moves to a topical home; the 5 that remain are genuine escape hatches behind a real, clickable index.
  • The one page that explains the two protocol eras was the 4th child of "The Client", where no server author looks. It and "Deprecated features" are top-level now.

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 of docs_src/ moves: the --8<-- snippet includes are repo-root-relative.

The new left panel

MCP Python SDK
Get started            install -> first server -> connect it to a real host -> 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    Add to an existing app / Deploy & scale
                       / Authorization / OpenTelemetry / Serving legacy clients
Clients                Callbacks / Transports / OAuth / Identity assertion
                       / Multiple servers / Caching
Protocol versions
Deprecated features
Advanced               The low-level Server / Pagination / Middleware
                       / Extensions / MCP Apps
Troubleshooting
Migration Guide
API Reference

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 str and 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 legacy ctx.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.

  • 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, which every exception escaping async with Client(...) arrives inside.
  • Deploy & scale: the DNS-rebinding Host allowlist, the most-reported deploy failure, moves here from "Add to an existing app"; its config example was the doc set's one untested inline snippet and is now a tested file. Then the two things "more than one worker" changes: requestState across 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 shared SubscriptionBus. Both are proved by tests that run two server instances in memory, the wrong way and the right way.
  • Connect to a real host: one tested server file, one short section per host. mcp install supports 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.
  • Serving legacy clients: one 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=True is 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, one Resolve tool, and a legacy and a modern client served concurrently in memory.

How Has This Been Tested?

  • mkdocs build --strict is 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.
  • The full docs test suite passes: 973 tests (from 859), including 44 new tests behind the four new pages and the CI lint of every new page's fenced code. ./scripts/test (coverage, branch mode, fail_under = 100) passes. All pre-commit hooks pass on every commit.
  • Beyond CI: every factual claim on the new pages was checked against 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

  • 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

  • The mkdocs-material floor is raised to >=9.7.0 because navigation.path (the breadcrumb feature, already enabled in mkdocs.yml) shipped in the 9.7.0 community edition; the previous >=9.5.45 floor let a fresh resolve silently drop a configured feature.
  • A nav title and a page H1 may differ, deliberately: the sidebar names the reader's goal ("Multiple servers", "Images, audio & icons"), the page keeps its honest technical name ("Session groups", "Media").
  • Docs are written v2-first: the modern (2026-07-28) way is presented unqualified, and legacy behaviour appears as short asides on the pages where it genuinely matters, plus one page each for the era model (Protocol versions), the operational posture (Serving legacy clients), and the removed features (Deprecated features). Interleaving v1-to-v2 migration notes throughout the pages is deliberately out of scope for this PR; docs/migration.md remains the one migration page.

AI Disclaimer

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3044.mcp-python-docs.pages.dev
Deployment https://68a997f7.mcp-python-docs.pages.dev
Commit 59e7a59
Triggered by @maxisbey
Updated 2026-07-01 19:43:37 UTC

@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 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

Comment thread docs/get-started/testing.md Outdated
Comment on lines 101 to 103
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.

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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.
Fix with cubic

Comment thread docs/get-started/testing.md Outdated
Comment thread mkdocs.yml

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

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

Comment thread docs/servers/media.md Outdated
Comment thread docs/client/oauth-clients.md Outdated
Comment thread mkdocs.yml Outdated
Comment thread README.md Outdated
@maxisbey maxisbey changed the title docs: restructure the navigation into topical sections docs: restructure into topical sections and add the four most-asked-for pages Jul 1, 2026
maxisbey added 9 commits July 1, 2026 18:07
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.

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

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:

@cubic-dev-ai cubic-dev-ai Bot Jul 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Fix with cubic

Comment thread docs/servers/handling-errors.md Outdated
Comment thread docs/handlers/dependencies.md Outdated
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.
@maxisbey maxisbey force-pushed the docs-restructure branch from 5267648 to 01128d6 Compare July 1, 2026 18:15
"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.
Comment thread tests/docs_src/test_troubleshooting.py Outdated
Comment thread docs/run/asgi.md
Comment thread RELEASE.md
maxisbey added 4 commits July 1, 2026 18:40
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.
Comment thread docs/get-started/index.md

## 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**:

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.

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

  1. docs/client/oauth-clients.md — reworded by this same PR — says: "Most examples in these docs you can check with an in-memory Client(server). Not this: the whole point of the flow is an HTTP 401, and there is no HTTP between an in-memory client and its server." Its examples (docs_src/oauth_clients/tutorial001-002.py) are exercised by tests/docs_src/test_oauth_clients.py without ever opening an in-memory client.
  2. Several examples added by this PR are exercised over httpx.ASGITransport / streamable_http_client rather 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), and docs_src/troubleshooting/tutorial003/004/005/008.py (test_troubleshooting.py).
  3. docs/deprecated.md has no docs_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.

@maxisbey maxisbey enabled auto-merge (squash) July 1, 2026 20:06
@maxisbey maxisbey merged commit 220d362 into main Jul 1, 2026
39 checks passed
@maxisbey maxisbey deleted the docs-restructure branch July 1, 2026 20:06
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