@@ -11,11 +11,8 @@ it has one golden rule: **extensions are off by default**.
1111
1212Pass instances at construction:
1313
14- ``` python
15- from mcp.server.apps import Apps
16- from mcp.server.mcpserver import MCPServer
17-
18- mcp = MCPServer(" demo" , extensions = [Apps()])
14+ ``` python title="server.py"
15+ -- 8 < -- " docs_src/extensions/tutorial001.py"
1916```
2017
2118Done. The server now advertises ` io.modelcontextprotocol/ui ` under
@@ -39,8 +36,7 @@ Subclass `Extension` and override only what you need. Every method has a default
3936### The identifier
4037
4138``` python
42- class Stamps (Extension ):
43- identifier = " com.example/stamps"
39+ -- 8 < -- " docs_src/extensions/tutorial002.py"
4440```
4541
4642The identifier is a ` vendor-prefix/name ` string following the spec's ` _meta ` key
@@ -60,8 +56,8 @@ specified by the MCP project itself.
6056
6157The smallest useful extension is one tool and a settings map:
6258
63- ``` python title="server.py" hl_lines="16 18-19 21-22 25 "
64- -- 8 < -- " docs_src/extensions/tutorial001 .py"
59+ ``` python title="server.py" hl_lines="17 19-20 22-23 26 "
60+ -- 8 < -- " docs_src/extensions/tutorial003 .py"
6561```
6662
6763* ` tools() ` returns ` ToolBinding ` s. The server registers each one exactly as if you
@@ -72,27 +68,19 @@ The smallest useful extension is one tool and a settings map:
7268* The extension never receives the server. It declares contributions as data;
7369 ` MCPServer ` consumes them. There is no ` self.server ` to mutate.
7470
75- #### Try it
71+ And ` main() ` is the proof, an in-memory client straight against ` mcp ` :
7672
77- ``` python
78- from mcp import Client
79-
80-
81- async def main () -> None :
82- async with Client(mcp) as client:
83- print (client.server_capabilities.extensions)
84- # {'com.example/stamps': {'sealed': True}}
85- result = await client.call_tool(" stamp" , {" text" : " hello" })
86- # [stamped] hello
73+ ``` python title="server.py" hl_lines="29-34"
74+ -- 8 < -- " docs_src/extensions/tutorial003.py"
8775```
8876
8977### Serving your own methods
9078
9179An extension can register ** new request methods** : its own verbs, served next to the
9280spec's:
9381
94- ``` python title="server.py" hl_lines="14-20 24 33-41 "
95- -- 8 < -- " docs_src/extensions/tutorial002 .py"
82+ ``` python title="server.py" hl_lines="15-21 30 39-47 "
83+ -- 8 < -- " docs_src/extensions/tutorial004 .py"
9684```
9785
9886* ` SearchParams ` subclasses ` RequestParams ` , so the 2026 ` _meta ` envelope parses
@@ -116,18 +104,31 @@ runtime:
116104* An empty ` protocol_versions ` set raises too: a method that can never be served
117105 is a bug, not a configuration.
118106
119- !!! tip
120- Calling a vendor method from the client goes through ` client.session.send_request(...) `
121- today; ` Client ` only grows first-class methods for spec verbs. The
122- ` custom_methods ` story in ` examples/stories/ ` shows the full round trip.
107+ ### The client side
108+
109+ The same file's ` main() ` is the whole client story, both halves of it:
110+
111+ ``` python title="server.py" hl_lines="53-57"
112+ -- 8 < -- " docs_src/extensions/tutorial004.py"
113+ ```
114+
115+ * ` Client(..., extensions={EXTENSION_ID: {}}) ` declares the extension. That map
116+ becomes ` ClientCapabilities.extensions ` : on a 2026-07-28 connection it travels in
117+ the per-request ` _meta ` envelope, so the server sees it on ** every** request; on
118+ a legacy connection it rides the ` initialize ` handshake. Server code doesn't care
119+ which: ` require_client_extension(ctx, ...) ` and
120+ ` ctx.session.check_client_capability(...) ` read the right source on both paths.
121+ * Vendor methods drop one layer to ` client.session.send_request(...) ` ; ` Client `
122+ only grows first-class methods for spec verbs. The ` cast ` is there because
123+ ` send_request ` is typed against the spec's closed request union.
123124
124125### Intercepting ` tools/call `
125126
126127The one interceptive hook. Override ` intercept_tool_call ` to observe, short-circuit,
127128or veto a tool call:
128129
129130``` python title="server.py" hl_lines="18-25"
130- -- 8 < -- " docs_src/extensions/tutorial003 .py"
131+ -- 8 < -- " docs_src/extensions/tutorial005 .py"
131132```
132133
133134* ` params ` is the validated ` CallToolRequestParams ` : you get ` params.name ` and
@@ -143,23 +144,6 @@ or veto a tool call:
143144The hook wraps ` tools/call ` and nothing else. For every-message concerns, use
144145[ Middleware] ( middleware.md ) . That is what it is for.
145146
146- ## The client side
147-
148- A client declares the extensions it supports the same way the server does:
149-
150- ``` python
151- from mcp import Client
152-
153- async with Client(target, extensions = {" com.example/search" : {}}) as client:
154- ...
155- ```
156-
157- That map becomes ` ClientCapabilities.extensions ` . On a 2026-07-28 connection it
158- travels in the per-request ` _meta ` envelope, so the server sees it on ** every**
159- request; on a legacy connection it rides the ` initialize ` handshake. Server code
160- doesn't care which: ` require_client_extension(ctx, ...) ` and
161- ` ctx.session.check_client_capability(...) ` read the right source on both paths.
162-
163147## What an extension cannot do
164148
165149The contribution surface is ** closed** on purpose: settings, tools, resources,
0 commit comments