22
33An ** extension** is an opt-in bundle of MCP behaviour behind one identifier.
44
5- It can contribute tools, resources, and new request methods, and it can wrap ` tools/call ` .
6- The server advertises it under ` capabilities.extensions ` , the client opts in the same way,
7- and nothing changes for anyone who didn't ask for it. That is the contract ([ SEP-2133] ( https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133 ) ), and
5+ On a server it can contribute tools, resources, and new request methods, and it can wrap
6+ ` tools/call ` . On a client it can claim extra ` tools/call ` result shapes and observe vendor
7+ notifications. Each side advertises under its own ` capabilities.extensions ` , and nothing
8+ changes for anyone who didn't ask for it. That is the contract ([ SEP-2133] ( https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133 ) ), and
89it has one golden rule: ** extensions are off by default** .
910
1011## Using an extension
@@ -79,7 +80,7 @@ And `main()` is the proof, an in-memory client straight against `mcp`:
7980An extension can register ** new request methods** : its own verbs, served next to the
8081spec's:
8182
82- ``` python title="server.py" hl_lines="15-21 30 39-47 "
83+ ``` python title="server.py" hl_lines="16-22 31 40-48 "
8384-- 8 < -- " docs_src/extensions/tutorial004.py"
8485```
8586
@@ -108,19 +109,19 @@ runtime:
108109
109110The same file's ` main() ` is the whole client story, both halves of it:
110111
111- ``` python title="server.py" hl_lines="53-57 "
112+ ``` python title="server.py" hl_lines="54-58 "
112113-- 8 < -- " docs_src/extensions/tutorial004.py"
113114```
114115
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
116+ * ` Client(..., extensions=[advertise( EXTENSION_ID)]) ` declares the extension. The
117+ declarations become ` ClientCapabilities.extensions ` : on a 2026-07-28 connection
118+ the map travels in the per-request ` _meta ` envelope, so the server sees it on
119+ ** every ** request; on a legacy connection it rides the ` initialize ` handshake.
120+ Server code doesn't care which: ` require_client_extension(ctx, ...) ` and
120121 ` ctx.session.check_client_capability(...) ` read the right source on both paths.
121122* 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 .
123+ only grows first-class methods for spec verbs. ` send_request ` accepts any
124+ ` Request ` subclass, so the vendor request passes as-is .
124125
125126### Intercepting ` tools/call `
126127
@@ -144,15 +145,104 @@ or veto a tool call:
144145The hook wraps ` tools/call ` and nothing else. For every-message concerns, use
145146[ Middleware] ( middleware.md ) . That is what it is for.
146147
148+ ## Using a client extension
149+
150+ A ** client extension** is the same contract from the consuming side: a bundle of
151+ client-side behaviour behind one identifier. Pass instances to
152+ ` Client(extensions=[...]) ` and call tools normally:
153+
154+ ``` python title="client.py" hl_lines="67-69"
155+ -- 8 < -- " docs_src/extensions/tutorial006.py"
156+ ```
157+
158+ ` call_tool("buy", ...) ` returns a plain ` CallToolResult ` , like every other call. What
159+ the extension changed: the server may now answer ` buy ` with a ` receipt ` ** result
160+ shape** instead of a final result, and ` Receipts ` finishes it (here by redeeming the
161+ receipt with a follow-up call) before ` call_tool ` returns. Nothing about the call
162+ site moves.
163+
164+ Drop the extension and none of this exists: the server's gate refuses a client
165+ that did not declare it (error -32021), and a claimed shape from a server that
166+ skips the gate fails validation, exactly as the spec requires for an
167+ unrecognized ` resultType ` . Off by default, on both ends of the wire.
168+
169+ To advertise an identifier with ** no** client-side behaviour (the server gates on
170+ the capability, the client does nothing, as in the search client above), use
171+ ` advertise() ` :
172+
173+ ``` python
174+ from mcp.client import advertise
175+
176+ client = Client(mcp, extensions = [advertise(" com.example/search" )])
177+ ```
178+
179+ ## Writing a client extension
180+
181+ Subclass ` ClientExtension ` and override only what you need. Three contribution
182+ kinds, each with a default: ` settings() ` , ` claims() ` , and ` notifications() ` .
183+
184+ ``` python title="client.py" hl_lines="18-19 44-45 47-48"
185+ -- 8 < -- " docs_src/extensions/tutorial006.py"
186+ ```
187+
188+ * The identifier follows the same grammar as the server's, validated when the class
189+ is defined.
190+ * ` claims() ` returns ` ResultClaim ` s: a wire tag, the model that parses it, and the
191+ resolver that finishes it. The model must pin the tag with
192+ ` result_type: Literal["receipt"] ` and must not subclass the verb's core result
193+ types; both are enforced when the claim is constructed. Vendor fields like
194+ ` receipt_token ` ride the wire as-is: a substituted shape reaches the client
195+ verbatim.
196+ * The resolver receives the parsed model and a ` ClaimContext ` ; ` ctx.session ` is the
197+ same public handle as ` client.session ` , so follow-ups are ordinary session calls.
198+ It returns the verb's normal ` CallToolResult ` .
199+ * ` settings() ` is the value advertised at ` ClientCapabilities.extensions[identifier] ` ,
200+ read once at ` Client ` construction.
201+
202+ ` notifications() ` declares vendor server notifications to observe:
203+
204+ ``` python
205+ def notifications (self ) -> Sequence[NotificationBinding[Any]]:
206+ return [NotificationBinding(method = " notifications/receipts" , params_type = ReceiptEvent, handler = self .on_receipt)]
207+ ```
208+
209+ The handler receives validated params one at a time, in dispatch order. It observes; it cannot veto
210+ or reply.
211+
212+ Two quiet rules. Claims are active on 2026-07-28 connections only, and the capability
213+ ad follows them: on a legacy connection the claims dissolve and the identifier drops
214+ out of the ad with them, so the client never advertises an extension whose shapes it
215+ would reject. And when you want the claimed shape yourself instead of the resolver,
216+ call ` client.session.call_tool(..., allow_claimed=True) ` ; without that flag, a
217+ claimed shape reaching a session-tier caller raises ` UnexpectedClaimedResult ` .
218+
219+ ### Extension verbs
220+
221+ An extension's own request methods need no client-side registration. A vendor request
222+ type subclasses ` mcp_types.Request ` and goes through ` client.session.send_request ` ,
223+ as in [ Serving your own methods] ( #serving-your-own-methods ) . One addition: when a
224+ params key must ride the ` Mcp-Name ` header (extension specs such as tasks require
225+ this for their verbs), the request type declares ` name_param ` :
226+
227+ ``` python title="client.py" hl_lines="23-26 47-48"
228+ -- 8 < -- " docs_src/extensions/tutorial007.py"
229+ ```
230+
231+ The session mirrors ` params["jobId"] ` into ` Mcp-Name ` on every send path, and a
232+ missing value fails loudly rather than silently omitting a required header.
233+
147234## What an extension cannot do
148235
149- The contribution surface is ** closed** on purpose: settings, tools, resources,
150- methods, one ` tools/call ` interceptor. An extension cannot:
236+ The contribution surface is ** closed** on purpose. On the server: settings, tools,
237+ resources, methods, one ` tools/call ` interceptor. On the client: settings, result
238+ claims, notification bindings. An extension cannot:
151239
152- * ** Reach into the server.** It declares data; it holds no server reference.
153- * ** Replace core behaviour.** Spec methods are rejected at construction, and
154- ` initialize ` is reserved by the runner outright.
155- * ** Register late.** After ` MCPServer(...) ` returns, the extension set is what it is.
240+ * ** Reach into the host.** It declares data; it holds no server or client reference.
241+ * ** Replace core behaviour.** Spec methods and core result tags are rejected at
242+ construction (` initialize ` is reserved by the runner outright); a notification
243+ binding shadowed by core vocabulary goes quiet with a warning instead.
244+ * ** Register late.** After ` MCPServer(...) ` or ` Client(...) ` returns, the extension
245+ set is what it is.
156246
157247If you are fighting these walls, you are not writing an extension. You are writing
158248a fork. The walls are the feature: a user reading ` extensions=[Apps(), Stamps()] `
0 commit comments