You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Copy file name to clipboardExpand all lines: docs/advanced/extensions.md
+12-12Lines changed: 12 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,4 +1,4 @@
1
-
# Extensions
1
+
# Extensions {#extensions}
2
2
3
3
An **extension** is an opt-in bundle of MCP behaviour behind one identifier.
4
4
@@ -8,7 +8,7 @@ notifications. Each side advertises under its own `capabilities.extensions`, and
8
8
changes for anyone who didn't ask for it. That is the contract ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), and
9
9
it has one golden rule: **extensions are off by default**.
10
10
11
-
## Using an extension
11
+
## Using an extension {#using-an-extension}
12
12
13
13
Pass instances at construction:
14
14
@@ -30,11 +30,11 @@ The capability map rides `server/discover`, which is a **2026-07-28** path. A le
30
30
the extension. Design for that: an extension *augments* a server, it must not be the
31
31
only way the server is usable.
32
32
33
-
## Writing your own
33
+
## Writing your own {#writing-your-own}
34
34
35
35
Subclass `Extension` and override only what you need. Every method has a default.
36
36
37
-
### The identifier
37
+
### The identifier {#the-identifier}
38
38
39
39
```python
40
40
--8<--"docs_src/extensions/tutorial002.py"
@@ -53,7 +53,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string
53
53
Use a domain you control as the prefix. `io.modelcontextprotocol/*` is for extensions
54
54
specified by the MCP project itself.
55
55
56
-
### Contributing tools
56
+
### Contributing tools {#contributing-tools}
57
57
58
58
The smallest useful extension is one tool and a settings map:
59
59
@@ -75,7 +75,7 @@ And `main()` is the proof, an in-memory client straight against `mcp`:
75
75
--8<--"docs_src/extensions/tutorial003.py"
76
76
```
77
77
78
-
### Serving your own methods
78
+
### Serving your own methods {#serving-your-own-methods}
79
79
80
80
An extension can register **new request methods**: its own verbs, served next to the
81
81
spec's:
@@ -105,7 +105,7 @@ runtime:
105
105
* An empty `protocol_versions` set raises too: a method that can never be served
106
106
is a bug, not a configuration.
107
107
108
-
### The client side
108
+
### The client side {#the-client-side}
109
109
110
110
The same file's `main()` is the whole client story, both halves of it:
111
111
@@ -123,7 +123,7 @@ The same file's `main()` is the whole client story, both halves of it:
123
123
only grows first-class methods for spec verbs. `send_request` accepts any
124
124
`Request` subclass, so the vendor request passes as-is.
Copy file name to clipboardExpand all lines: docs/advanced/low-level-server.md
+12-12Lines changed: 12 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,4 +1,4 @@
1
-
# The low-level Server
1
+
# The low-level Server {#the-low-level-server}
2
2
3
3
`@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.
4
4
@@ -10,7 +10,7 @@
10
10
11
11
For everything else, stay on `MCPServer`.
12
12
13
-
## The same tool, by hand
13
+
## The same tool, by hand {#the-same-tool-by-hand}
14
14
15
15
This is the `search_books` tool that **[Tools](../servers/tools.md)** writes in nine lines of `@mcp.tool()`, with the sugar removed:
16
16
@@ -29,7 +29,7 @@ Three things changed, and they are the whole low-level API:
29
29
!!! info
30
30
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.
31
31
32
-
### Try it
32
+
### Try it {#try-it}
33
33
34
34
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`:
35
35
@@ -59,7 +59,7 @@ The same text the `@mcp.tool()` version produced. Two honest differences:
59
59
*`result.structured_content` is `None`. The high-level server wraps a `-> str` into `{"result": ...}` for you; here nobody builds what you didn't build.
60
60
*`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.
61
61
62
-
## Nothing is checked for you
62
+
## Nothing is checked for you {#nothing-is-checked-for-you}
63
63
64
64
`MCPServer` rejects a bad argument before your function ever runs, validating the call against the schema it generated (**[Tools](../servers/tools.md)**).
65
65
@@ -76,7 +76,7 @@ The same text the `@mcp.tool()` version produced. Two honest differences:
76
76
77
77
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)**.
78
78
79
-
## Two tools, one handler
79
+
## Two tools, one handler {#two-tools-one-handler}
80
80
81
81
`on_call_tool` is the single entry point for every tool on the server. You route on `params.name`:
82
82
@@ -87,7 +87,7 @@ That generalises. An exception raised from a low-level handler is **always** a p
87
87
*`list_tools` advertises both. `call_tool` dispatches on the name.
88
88
* 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.
89
89
90
-
## Structured output, by hand
90
+
## Structured output, by hand {#structured-output-by-hand}
91
91
92
92
Declare `output_schema` on the `Tool` and put `structured_content` on the result. Both are yours:
93
93
@@ -111,7 +111,7 @@ The `_meta` block is the server's identity stamp: the SDK adds it to every 2026-
111
111
112
112
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)**.
113
113
114
-
## `_meta`: for the application, not the model
114
+
## `_meta`: for the application, not the model {#\_meta-for-the-application-not-the-model}
115
115
116
116
`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.
117
117
@@ -128,7 +128,7 @@ Use it for record IDs, trace IDs, anything your UI needs and your prompt doesn't
128
128
`_meta` is a convention between you and the client application, not a guarantee about what reaches
129
129
the model. The host decides what it renders. Never put a secret in any part of a tool result.
130
130
131
-
## Capabilities follow your handlers
131
+
## Capabilities follow your handlers {#capabilities-follow-your-handlers}
132
132
133
133
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:
134
134
@@ -140,7 +140,7 @@ No `resources`, no `prompts`: there is nothing to back them. Pass `on_list_promp
140
140
141
141
`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.
142
142
143
-
## The lifespan generic
143
+
## The lifespan generic {#the-lifespan-generic}
144
144
145
145
`Server` is generic in the type its lifespan yields. Annotate it once and the object is typed everywhere it surfaces:
146
146
@@ -154,7 +154,7 @@ No `resources`, no `prompts`: there is nothing to back them. Pass `on_list_promp
154
154
155
155
Without a `lifespan=`, `ctx.lifespan_context` is an empty `dict`.
156
156
157
-
## A method of your own
157
+
## A method of your own {#a-method-of-your-own}
158
158
159
159
The constructor covers the methods MCP defines. `add_request_handler` covers everything else:
160
160
@@ -180,7 +180,7 @@ The handshake belongs to the runner. `server/discover`, `ping`, and every other
180
180
!!! tip
181
181
`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)**.
182
182
183
-
## The other handlers
183
+
## The other handlers {#the-other-handlers}
184
184
185
185
Each of these is one idea you now have the vocabulary for; each has its own page.
186
186
@@ -189,7 +189,7 @@ Each of these is one idea you now have the vocabulary for; each has its own page
189
189
*`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.
190
190
*`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.
191
191
192
-
## Recap
192
+
## Recap {#recap}
193
193
194
194
* The low-level `Server` takes its handlers as `on_*`**constructor parameters**; every handler is `async (ctx, params) -> result`.
195
195
* You write the `input_schema` dict and you build the `CallToolResult`. Nothing is derived, wrapped, or validated for you.
Copy file name to clipboardExpand all lines: docs/advanced/pagination.md
+6-6Lines changed: 6 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,4 +1,4 @@
1
-
# Pagination
1
+
# Pagination {#pagination}
2
2
3
3
Most servers never need this.
4
4
@@ -8,7 +8,7 @@ Pagination is for the server whose resource list is really a database: thousands
8
8
9
9
`@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)**.
10
10
11
-
## A server that pages
11
+
## A server that pages {#a-server-that-pages}
12
12
13
13
```python title="server.py" hl_lines="12 15-16"
14
14
--8<--"docs_src/pagination/tutorial001.py"
@@ -24,7 +24,7 @@ Pagination is for the server whose resource list is really a database: thousands
24
24
one-line resources can afford a page of 500; a list of fat prompt templates cannot.
25
25
The client has no say in it, and that is by design.
26
26
27
-
### Try it
27
+
### Try it {#try-it}
28
28
29
29
`Client(server)` connects to a low-level `Server` in memory exactly as it connects to an `MCPServer`.
30
30
@@ -34,7 +34,7 @@ Hand it back with `list_resources(cursor="10")` and the first resource is `book-
34
34
35
35
The tenth page comes back with `next_cursor` set to `None`. Done.
36
36
37
-
## The client loop
37
+
## The client loop {#the-client-loop}
38
38
39
39
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`:
40
40
@@ -50,7 +50,7 @@ Run its `main()` and it prints `100 resources`: ten pages of ten, stitched toget
50
50
51
51
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.
52
52
53
-
## The three rules
53
+
## The three rules {#the-three-rules}
54
54
55
55
**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.
56
56
@@ -69,7 +69,7 @@ This is the same loop **[The Client](../client/index.md)** shows for every `list
69
69
70
70
A cursor you didn't get from the server is a bug, not a feature request.
71
71
72
-
## Recap
72
+
## Recap {#recap}
73
73
74
74
*`MCPServer` returns everything in one page. Pagination is opt-in, and you opt in on the low-level `Server`.
75
75
*`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