Skip to content

Commit 05b16bc

Browse files
committed
Pin explicit anchors on every prose heading
Heading ids are now written into the docs source instead of derived from the heading text, so links stay stable across rewording and across the translated sites, and scripts/docs/check_anchors.py keeps it that way. The ids match what the current site already renders. Also settles the Streamable HTTP capitalisation the corpus used inconsistently.
1 parent 6d3a840 commit 05b16bc

50 files changed

Lines changed: 513 additions & 513 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/advanced/apps.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# MCP Apps
1+
# MCP Apps {#mcp-apps}
22

33
An **MCP App** is a tool with a face: alongside its data, the tool points at an HTML
44
document the host renders as an interactive surface.
@@ -18,7 +18,7 @@ The SDK ships this as the built-in `Apps` extension (`io.modelcontextprotocol/ui
1818
If [Extensions](extensions.md) are new to you, skim that page first. One minute,
1919
then come back.
2020

21-
## A clock with a face
21+
## A clock with a face {#a-clock-with-a-face}
2222

2323
```python title="server.py" hl_lines="19 22 30 32"
2424
--8<-- "docs_src/apps/tutorial001.py"
@@ -41,7 +41,7 @@ apps, use the official [`@modelcontextprotocol/ext-apps`](https://github.com/mod
4141
browser SDK inside your HTML. It gives you `ontoolresult`, `callServerTool`,
4242
`getHostContext`, and `onhostcontextchanged` instead of raw message events.
4343

44-
## Graceful degradation
44+
## Graceful degradation {#graceful-degradation}
4545

4646
Not every client renders apps. The spec is blunt about what that means for you:
4747

@@ -66,7 +66,7 @@ client half of the negotiation, and the rich answer comes back.
6666
fallback text is useless, the tool is useless to every text-only client and to
6767
the model itself. Write the sentence.
6868

69-
## Locking the iframe down
69+
## Locking the iframe down {#locking-the-iframe-down}
7070

7171
The resource side carries the security metadata: what the iframe may load, which
7272
browser permissions it wants, how it would like to be framed:
@@ -102,7 +102,7 @@ may refuse. Feature-detect in your JS rather than assuming a grant.
102102
metadata has no slot for them, and hosts ignore them there. The SDK makes the
103103
mistake unrepresentable: `@apps.tool()` simply has no `csp` parameter.
104104

105-
### Visibility
105+
### Visibility {#visibility}
106106

107107
`visibility=["app"]` on a tool says "this exists for the iframe, not the model":
108108

@@ -113,7 +113,7 @@ may refuse. Feature-detect in your JS rather than assuming a grant.
113113
Filtering is the **host's** job. Your server lists app-only tools in `tools/list`
114114
like any other; the host hides them from the model. Don't filter server-side.
115115

116-
## The rules the SDK enforces
116+
## The rules the SDK enforces {#the-rules-the-sdk-enforces}
117117

118118
All of these fail at startup, not in production:
119119

@@ -130,7 +130,7 @@ All of these fail at startup, not in production:
130130
Neither the TypeScript ext-apps SDK nor FastMCP catches any of these today; we'd
131131
rather you find out before a host does.
132132

133-
## Beyond inline HTML
133+
## Beyond inline HTML {#beyond-inline-html}
134134

135135
`add_html_resource` covers the common case: a string of HTML. For anything else,
136136
HTML on disk or generated content, build the resource yourself and hand it over:
@@ -149,7 +149,7 @@ under any other MIME type is one no host will render.
149149
`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`.
150150
The nested `ui` object is the spec shape; the flat key is on its way out.
151151

152-
## See it run
152+
## See it run {#see-it-run}
153153

154154
The `apps` story in `examples/stories/` is this page as a runnable pair: a server
155155
with a UI-bound clock tool and a client that negotiates Apps, reads the tool's

docs/advanced/extensions.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Extensions
1+
# Extensions {#extensions}
22

33
An **extension** is an opt-in bundle of MCP behaviour behind one identifier.
44

@@ -8,7 +8,7 @@ notifications. Each side advertises under its own `capabilities.extensions`, and
88
changes for anyone who didn't ask for it. That is the contract ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), and
99
it has one golden rule: **extensions are off by default**.
1010

11-
## Using an extension
11+
## Using an extension {#using-an-extension}
1212

1313
Pass instances at construction:
1414

@@ -30,11 +30,11 @@ The capability map rides `server/discover`, which is a **2026-07-28** path. A le
3030
the extension. Design for that: an extension *augments* a server, it must not be the
3131
only way the server is usable.
3232

33-
## Writing your own
33+
## Writing your own {#writing-your-own}
3434

3535
Subclass `Extension` and override only what you need. Every method has a default.
3636

37-
### The identifier
37+
### The identifier {#the-identifier}
3838

3939
```python
4040
--8<-- "docs_src/extensions/tutorial002.py"
@@ -53,7 +53,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
5353
Use a domain you control as the prefix. `io.modelcontextprotocol/*` is for extensions
5454
specified by the MCP project itself.
5555

56-
### Contributing tools
56+
### Contributing tools {#contributing-tools}
5757

5858
The smallest useful extension is one tool and a settings map:
5959

@@ -75,7 +75,7 @@ And `main()` is the proof, an in-memory client straight against `mcp`:
7575
--8<-- "docs_src/extensions/tutorial003.py"
7676
```
7777

78-
### Serving your own methods
78+
### Serving your own methods {#serving-your-own-methods}
7979

8080
An extension can register **new request methods**: its own verbs, served next to the
8181
spec's:
@@ -105,7 +105,7 @@ runtime:
105105
* An empty `protocol_versions` set raises too: a method that can never be served
106106
is a bug, not a configuration.
107107

108-
### The client side
108+
### The client side {#the-client-side}
109109

110110
The same file's `main()` is the whole client story, both halves of it:
111111

@@ -123,7 +123,7 @@ The same file's `main()` is the whole client story, both halves of it:
123123
only grows first-class methods for spec verbs. `send_request` accepts any
124124
`Request` subclass, so the vendor request passes as-is.
125125

126-
### Intercepting `tools/call`
126+
### Intercepting `tools/call` {#intercepting-toolscall}
127127

128128
The one interceptive hook. Override `intercept_tool_call` to observe, short-circuit,
129129
or veto a tool call:
@@ -152,7 +152,7 @@ or veto a tool call:
152152
The hook wraps `tools/call` and nothing else. For every-message concerns, use
153153
[Middleware](middleware.md). That is what it is for.
154154

155-
## Using a client extension
155+
## Using a client extension {#using-a-client-extension}
156156

157157
A **client extension** is the same contract from the consuming side: a bundle of
158158
client-side behaviour behind one identifier. Pass instances to
@@ -183,7 +183,7 @@ from mcp.client import advertise
183183
client = Client(mcp, extensions=[advertise("com.example/search")])
184184
```
185185

186-
## Writing a client extension
186+
## Writing a client extension {#writing-a-client-extension}
187187

188188
Subclass `ClientExtension` and override only what you need. Three contribution
189189
kinds, each with a default: `settings()`, `claims()`, and `notifications()`.
@@ -223,7 +223,7 @@ would reject. And when you want the claimed shape yourself instead of the resolv
223223
call `client.session.call_tool(..., allow_claimed=True)`; without that flag, a
224224
claimed shape reaching a session-tier caller raises `UnexpectedClaimedResult`.
225225

226-
### Extension verbs
226+
### Extension verbs {#extension-verbs}
227227

228228
An extension's own request methods need no client-side registration. A vendor request
229229
type subclasses `mcp.types.Request` and goes through `client.session.send_request`,
@@ -238,7 +238,7 @@ this for their verbs), the request type declares `name_param`:
238238
The session mirrors `params["jobId"]` into `Mcp-Name` on every send path, and a
239239
missing value fails loudly rather than silently omitting a required header.
240240

241-
## What an extension cannot do
241+
## What an extension cannot do {#what-an-extension-cannot-do}
242242

243243
The contribution surface is **closed** on purpose. On the server: settings, tools,
244244
resources, methods, one `tools/call` interceptor. On the client: settings, result

docs/advanced/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Advanced
1+
# Advanced {#advanced}
22

33
Everything an ordinary server or client needs has a topical home in the sections above.
44
This section is the escape hatches you reach for when `MCPServer`'s convenience

docs/advanced/low-level-server.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# The low-level Server
1+
# The low-level Server {#the-low-level-server}
22

33
`@mcp.tool()` is a layer. Underneath it is a second server class, `Server`, that speaks raw MCP: you hand it the protocol objects and it puts them on the wire, unchanged.
44

@@ -10,7 +10,7 @@
1010

1111
For everything else, stay on `MCPServer`.
1212

13-
## The same tool, by hand
13+
## The same tool, by hand {#the-same-tool-by-hand}
1414

1515
This is the `search_books` tool that **[Tools](../servers/tools.md)** writes in nine lines of `@mcp.tool()`, with the sugar removed:
1616

@@ -29,7 +29,7 @@ Three things changed, and they are the whole low-level API:
2929
!!! info
3030
If you've used FastAPI, you already know this relationship. `MCPServer` is the decorators-and-type-hints layer; `Server` is the Starlette underneath. They are not rivals: `MCPServer` constructs a `Server` and registers handlers exactly like these on it.
3131

32-
### Try it
32+
### Try it {#try-it}
3333

3434
There is no Inspector for this one: `mcp dev` and `mcp run` only accept an `MCPServer`. The in-memory `Client` doesn't care; it takes a low-level `Server` exactly like it takes an `MCPServer`:
3535

@@ -59,7 +59,7 @@ The same text the `@mcp.tool()` version produced. Two honest differences:
5959
* `result.structured_content` is `None`. The high-level server wraps a `-> str` into `{"result": ...}` for you; here nobody builds what you didn't build.
6060
* `list_tools` returns the schema **you** typed, character for character. The high-level version had `"title": "Query"` on every property and a `"title": "search_booksArguments"` at the root: Pydantic artifacts. Down here, if it's on the wire, you put it there.
6161

62-
## Nothing is checked for you
62+
## Nothing is checked for you {#nothing-is-checked-for-you}
6363

6464
`MCPServer` rejects a bad argument before your function ever runs, validating the call against the schema it generated (**[Tools](../servers/tools.md)**).
6565

@@ -76,7 +76,7 @@ The same text the `@mcp.tool()` version produced. Two honest differences:
7676

7777
That generalises. An exception raised from a low-level handler is **always** a protocol error, never an `is_error=True` tool result. If you want the model to read the failure and recover, validate `params.arguments` yourself and return `CallToolResult(content=[TextContent(...)], is_error=True)`. The two kinds of failure are the subject of **[Handling errors](../servers/handling-errors.md)**.
7878

79-
## Two tools, one handler
79+
## Two tools, one handler {#two-tools-one-handler}
8080

8181
`on_call_tool` is the single entry point for every tool on the server. You route on `params.name`:
8282

@@ -87,7 +87,7 @@ That generalises. An exception raised from a low-level handler is **always** a p
8787
* `list_tools` advertises both. `call_tool` dispatches on the name.
8888
* The `else` branch matters: `Server` will happily forward a `tools/call` for a name you never listed straight into your handler. Raising there turns the call into the same `-32603` as above.
8989

90-
## Structured output, by hand
90+
## Structured output, by hand {#structured-output-by-hand}
9191

9292
Declare `output_schema` on the `Tool` and put `structured_content` on the result. Both are yours:
9393

@@ -111,7 +111,7 @@ The `_meta` block is the server's identity stamp: the SDK adds it to every 2026-
111111

112112
The server never compares the two fields. This SDK's `Client` does: return `structured_content` that doesn't satisfy the `output_schema` you declared and `call_tool` raises a `RuntimeError` that starts with `Invalid structured content returned by tool search_books` and goes on to quote the `jsonschema` failure. Promising a schema is cheap; keeping it is on you. The whole ladder of return types and schemas is in **[Structured Output](../servers/structured-output.md)**.
113113

114-
## `_meta`: for the application, not the model
114+
## `_meta`: for the application, not the model {#\_meta-for-the-application-not-the-model}
115115

116116
`content` is the part of the answer the model reads. `structured_content` is the same answer as typed data. `_meta` is the third channel: data that rides along with the result for the **client application**, without being part of the answer at all.
117117

@@ -128,7 +128,7 @@ Use it for record IDs, trace IDs, anything your UI needs and your prompt doesn't
128128
`_meta` is a convention between you and the client application, not a guarantee about what reaches
129129
the model. The host decides what it renders. Never put a secret in any part of a tool result.
130130

131-
## Capabilities follow your handlers
131+
## Capabilities follow your handlers {#capabilities-follow-your-handlers}
132132

133133
A `Server` advertises exactly the method families you gave it handlers for. The `Bookshop` above passes `on_list_tools` and `on_call_tool` and nothing else, so a client connecting to it sees:
134134

@@ -140,7 +140,7 @@ No `resources`, no `prompts`: there is nothing to back them. Pass `on_list_promp
140140

141141
`MCPServer` always advertises tools, resources and prompts, whether you registered any or not, because its managers always exist. Down here the declaration *is* the constructor call.
142142

143-
## The lifespan generic
143+
## The lifespan generic {#the-lifespan-generic}
144144

145145
`Server` is generic in the type its lifespan yields. Annotate it once and the object is typed everywhere it surfaces:
146146

@@ -154,7 +154,7 @@ No `resources`, no `prompts`: there is nothing to back them. Pass `on_list_promp
154154

155155
Without a `lifespan=`, `ctx.lifespan_context` is an empty `dict`.
156156

157-
## A method of your own
157+
## A method of your own {#a-method-of-your-own}
158158

159159
The constructor covers the methods MCP defines. `add_request_handler` covers everything else:
160160

@@ -180,7 +180,7 @@ The handshake belongs to the runner. `server/discover`, `ping`, and every other
180180
!!! tip
181181
`Server.middleware`, mentioned in that error, wraps **every** inbound message, including `initialize`. If what you want is to observe or rewrite traffic rather than answer a new method, start at **[Middleware](middleware.md)**.
182182

183-
## The other handlers
183+
## The other handlers {#the-other-handlers}
184184

185185
Each of these is one idea you now have the vocabulary for; each has its own page.
186186

@@ -189,7 +189,7 @@ Each of these is one idea you now have the vocabulary for; each has its own page
189189
* `on_subscriptions_listen` serves the 2026-07-28 `subscriptions/listen` stream. Pass a `ListenHandler` built over a `SubscriptionBus` and publish events to the bus from your other handlers; see **[Subscriptions](../handlers/subscriptions.md)** for the full composition.
190190
* `server.streamable_http_app()` returns the same Starlette app `MCPServer`'s does; deploy it the way **[Running your server](../run/index.md)** deploys any other ASGI app. There is no `server.run(transport=...)` down here: `server.run(read_stream, write_stream, server.create_initialization_options())` drives one connection over a pair of streams, and that one line is the whole story.
191191

192-
## Recap
192+
## Recap {#recap}
193193

194194
* The low-level `Server` takes its handlers as `on_*` **constructor parameters**; every handler is `async (ctx, params) -> result`.
195195
* You write the `input_schema` dict and you build the `CallToolResult`. Nothing is derived, wrapped, or validated for you.

docs/advanced/middleware.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Middleware
1+
# Middleware {#middleware}
22

33
A **middleware** is one async function that wraps every message your server receives.
44

@@ -14,7 +14,7 @@ You write it as `async (ctx, call_next)` and append it to `server.middleware`. T
1414
below uses the low-level `Server`; if `Server(name, on_call_tool=...)` is new to you, read
1515
**[The low-level Server](low-level-server.md)** first.
1616

17-
## A timing middleware
17+
## A timing middleware {#a-timing-middleware}
1818

1919
One server, one tool, one middleware that logs how long each message took:
2020

@@ -31,7 +31,7 @@ One server, one tool, one middleware that logs how long each message took:
3131
* `server.middleware.append(...)` registers it. The list runs outermost-first, so
3232
`middleware[0]` is the one closest to the wire.
3333

34-
### Try it
34+
### Try it {#try-it}
3535

3636
Connect a client, list the tools, call one. Your log has **three** lines:
3737

@@ -53,7 +53,7 @@ That is the point. Middleware wraps **every** inbound message:
5353
* Even a method the server has no handler for: `call_next` raises the
5454
`MCPError(-32601, "Method not found")` *through* your middleware on its way to the client.
5555

56-
## What you can do inside one
56+
## What you can do inside one {#what-you-can-do-inside-one}
5757

5858
In increasing order of how much you should hesitate:
5959

@@ -88,7 +88,7 @@ In increasing order of how much you should hesitate:
8888
an elicitation) while handling `initialize` therefore **deadlocks the connection**: the
8989
response you are waiting for can never be read. Fire-and-forget notifications are fine.
9090

91-
## The one middleware that ships on by default
91+
## The one middleware that ships on by default {#the-one-middleware-that-ships-on-by-default}
9292

9393
The SDK ships exactly one middleware, and it is already on your server's list: the one that
9494
emits an OpenTelemetry span for every message. You don't append it, and most of the time you
@@ -101,7 +101,7 @@ don't think about it. It is a no-op until you install an exporter, and it has it
101101
the decoded message instead of the raw HTTP request. The two compose: Starlette middleware
102102
on `streamable_http_app()` sees HTTP; this sees MCP.
103103

104-
## Recap
104+
## Recap {#recap}
105105

106106
* A middleware is `async (ctx, call_next) -> result`, passed as `MCPServer(middleware=[...])` (or
107107
appended to `mcp.middleware`), and appended to `server.middleware` on the low-level `Server`.

docs/advanced/pagination.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Pagination
1+
# Pagination {#pagination}
22

33
Most servers never need this.
44

@@ -8,7 +8,7 @@ Pagination is for the server whose resource list is really a database: thousands
88

99
`@mcp.resource()` has no hook for any of that. To page, you write the list handler yourself, on the **[low-level Server](low-level-server.md)**.
1010

11-
## A server that pages
11+
## A server that pages {#a-server-that-pages}
1212

1313
```python title="server.py" hl_lines="12 15-16"
1414
--8<-- "docs_src/pagination/tutorial001.py"
@@ -24,7 +24,7 @@ Pagination is for the server whose resource list is really a database: thousands
2424
one-line resources can afford a page of 500; a list of fat prompt templates cannot.
2525
The client has no say in it, and that is by design.
2626

27-
### Try it
27+
### Try it {#try-it}
2828

2929
`Client(server)` connects to a low-level `Server` in memory exactly as it connects to an `MCPServer`.
3030

@@ -34,7 +34,7 @@ Hand it back with `list_resources(cursor="10")` and the first resource is `book-
3434

3535
The tenth page comes back with `next_cursor` set to `None`. Done.
3636

37-
## The client loop
37+
## The client loop {#the-client-loop}
3838

3939
Every `list_*` method on `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) takes a `cursor=` keyword. Draining a paged list is one `while True`:
4040

@@ -50,7 +50,7 @@ Run its `main()` and it prints `100 resources`: ten pages of ten, stitched toget
5050

5151
This is the same loop **[The Client](../client/index.md)** shows for every `list_*` verb, and it costs nothing against a server that doesn't page: `next_cursor` is `None` on the first response and the loop runs once.
5252

53-
## The three rules
53+
## The three rules {#the-three-rules}
5454

5555
**Cursors are opaque.** A client must never parse, build, or guess one. The only legal source of a cursor is the previous page's `next_cursor`, verbatim.
5656

@@ -69,7 +69,7 @@ This is the same loop **[The Client](../client/index.md)** shows for every `list
6969

7070
A cursor you didn't get from the server is a bug, not a feature request.
7171

72-
## Recap
72+
## Recap {#recap}
7373

7474
* `MCPServer` returns everything in one page. Pagination is opt-in, and you opt in on the low-level `Server`.
7575
* `on_list_resources` (and `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) receives `PaginatedRequestParams | None`; `params.cursor` is `None` for the first page.

0 commit comments

Comments
 (0)