Skip to content

Commit 8157846

Browse files
committed
Tighten comments and docstrings across the extension surface
Comments state only non-inferable constraints, one line where possible; docstrings follow Google format with a single-sentence summary and tightened Raises sections; development narration and restated-code comments are gone. Prose sticks to ASCII.
1 parent 15ff3c2 commit 8157846

22 files changed

Lines changed: 250 additions & 611 deletions

docs/advanced/extensions.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -157,16 +157,16 @@ client-side behaviour behind one identifier. Pass instances to
157157

158158
`call_tool("buy", ...)` returns a plain `CallToolResult`, like every other call. What
159159
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
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
162162
site moves.
163163

164164
Drop the extension and none of this exists: a `receipt` shape arriving at a client
165165
that didn't declare it fails validation, exactly as the spec requires for an
166166
unrecognized `resultType`. Off by default, on both ends of the wire.
167167

168-
To advertise an identifier with **no** client-side behaviour the server gates on
169-
the capability, the client does nothing, as in the search client above use
168+
To advertise an identifier with **no** client-side behaviour (the server gates on
169+
the capability, the client does nothing, as in the search client above), use
170170
`advertise()`:
171171

172172
```python
@@ -189,7 +189,7 @@ kinds, each with a default: `settings()`, `claims()`, and `notifications()`.
189189
* `claims()` returns `ResultClaim`s: a wire tag, the model that parses it, and the
190190
resolver that finishes it. The model must pin the tag with
191191
`result_type: Literal["receipt"]` and must not subclass the verb's core result
192-
typesboth enforced when the claim is constructed. Vendor fields like
192+
types; both are enforced when the claim is constructed. Vendor fields like
193193
`receipt_token` ride the wire as-is: a substituted shape reaches the client
194194
verbatim.
195195
* The resolver receives the parsed model and a `ClaimContext`; `ctx.session` is the
@@ -210,11 +210,10 @@ or reply.
210210

211211
Two quiet rules. Claims are active on 2026-07-28 connections only, and the capability
212212
ad follows them: on a legacy connection the claims dissolve and the identifier drops
213-
out of the ad in the same breath, so the client never advertises an extension whose
214-
shapes it would reject. And when you want the claimed shape yourself instead of the
215-
resolver, call `client.session.call_tool(..., allow_claimed=True)` — the escape hatch
216-
`UnexpectedClaimedResult` names when a claimed shape reaches a session-tier caller
217-
that didn't opt in.
213+
out of the ad with them, so the client never advertises an extension whose shapes it
214+
would reject. And when you want the claimed shape yourself instead of the resolver,
215+
call `client.session.call_tool(..., allow_claimed=True)`; without that flag, a
216+
claimed shape reaching a session-tier caller raises `UnexpectedClaimedResult`.
218217

219218
### Extension verbs
220219

@@ -233,8 +232,8 @@ missing value fails loudly rather than silently omitting a required header.
233232

234233
## What an extension cannot do
235234

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
235+
The contribution surface is **closed** on purpose. On the server: settings, tools,
236+
resources, methods, one `tools/call` interceptor. On the client: settings, result
238237
claims, notification bindings. An extension cannot:
239238

240239
* **Reach into the host.** It declares data; it holds no server or client reference.

docs/migration.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -483,9 +483,9 @@ default and never alter behaviour unless registered. (The low-level
483483

484484
Changed in the v2 pre-releases: earlier alphas took
485485
`Client(extensions={identifier: settings})`, an advertisement-only dict.
486-
Extensions now contribute behaviour claims and notification handlers not
487-
just an ad, and a sequence of declaration objects is the shape that can carry
488-
that. An ad-only entry becomes an `advertise()` call:
486+
Extensions now contribute behaviour (claims and notification handlers), not
487+
just an ad, so the argument is a sequence of declaration objects. An ad-only
488+
entry becomes an `advertise()` call:
489489

490490
**Before (v2 alphas):**
491491

@@ -501,10 +501,10 @@ from mcp.client import advertise
501501
client = Client(server, extensions=[advertise("com.example/ui", {"mimeTypes": [...]})])
502502
```
503503

504-
`advertise()` is only for identifiers with no client-side behaviour.
505-
For a behavioural extension e.g. tasks, once its extension shipsconstruct
506-
that extension's object instead; advertising an identifier you do not
507-
implement asserts wire support you don't have.
504+
`advertise()` is only for identifiers with no client-side behaviour. For a
505+
behavioural extension (e.g. tasks, once its extension ships), construct that
506+
extension's object instead; advertising an identifier you do not implement
507+
asserts wire support you don't have.
508508

509509
### `McpError` renamed to `MCPError`
510510

examples/stories/custom_methods/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ uv run python -m stories.custom_methods.client --http
2929
collide with a future spec method.
3030
- `client.py` `client.session.send_request(...)``Client` only exposes spec
3131
verbs, so vendor methods go through the underlying `ClientSession`.
32-
`send_request` accepts any `types.Request` subclass, so the vendor request
33-
passes as-is, no cast.
32+
`send_request` accepts any `types.Request` subclass.
3433

3534
## Caveats
3635

examples/stories/custom_methods/client.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ async def main(target: Target, *, mode: str = "auto") -> None:
2727
# `Client` only exposes spec-defined verbs, so vendor methods have to drop one
2828
# layer to `client.session` today — there is no `Client`-level API for them
2929
# yet, and whether `.session` stays public is undecided. `send_request`
30-
# accepts any `Request` subclass; the unknown method skips the per-spec
31-
# result-validation registry.
30+
# accepts any `Request` subclass.
3231
request = SearchRequest(params=SearchParams(query="mcp", limit=3))
3332
result = await client.session.send_request(request, SearchResult)
3433
assert result.items == ["mcp-0", "mcp-1", "mcp-2"], result

src/mcp-types/mcp_types/_types.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,10 @@ class Request(MCPModel, Generic[RequestParamsT, MethodT]):
129129
params: RequestParamsT
130130

131131
name_param: ClassVar[str | None] = None
132-
"""Wire-params key mirrored into the `Mcp-Name` header on sends (SEP-2243
133-
family; SEP-2663 requires it for tasks/*). The request type declares; the
134-
host emits. Subclasses override by bare assignment (`name_param = "taskId"`)
135-
— re-annotating as `ClassVar[str]` trips pyright's ClassVar invariance."""
132+
"""Wire-params key mirrored into the `Mcp-Name` header on sends; SEP-2663 requires it for tasks/*.
133+
134+
Subclasses override by bare assignment: re-annotating as `ClassVar` trips pyright's invariance check.
135+
"""
136136

137137

138138
class PaginatedRequest(Request[PaginatedRequestParams | None, MethodT], Generic[MethodT]):

src/mcp/client/client.py

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -202,19 +202,11 @@ class _FoldedExtensions:
202202

203203

204204
def _fold_extensions(extensions: Sequence[ClientExtension] | None) -> _FoldedExtensions:
205-
"""Validate extension instances and fold their contributions, once, at `Client` construction.
206-
207-
Mirrors the server's consumption-time posture (`MCPServer._apply_extension`): a
208-
per-instance identifier is validated here because no class attribute existed to
209-
validate at definition time. `settings()` is read exactly once per extension and
210-
the returned dict is held by reference. Duplicate resultType claims and
211-
duplicate notification methods are rejected here, where both owning extensions
212-
can be named — the session's own duplicate checks know only methods and tags.
213-
"""
205+
"""Fold extension contributions at construction, naming both owners on duplicate tags or methods."""
214206
if isinstance(extensions, Mapping):
215207
raise TypeError(
216-
"extensions= takes a sequence of ClientExtension instances; the mapping form was "
217-
"replaced use advertise(identifier, settings) for advertise-only entries"
208+
"extensions= takes a sequence of ClientExtension instances. The mapping form was "
209+
"replaced: use advertise(identifier, settings) for advertise-only entries"
218210
)
219211
if not extensions:
220212
return _FoldedExtensions(ad=None, claims=None, bindings=None, by_model={})
@@ -247,8 +239,7 @@ def _fold_extensions(extensions: Sequence[ClientExtension] | None) -> _FoldedExt
247239
)
248240
raise ValueError(f"{both} resultType {tag!r}; a wire tag can have only one resolver")
249241
claim_owners[tag] = identifier
250-
# One model, one tag: the model's result_type Literal is pinned to exactly
251-
# this tag at claim construction, so the type-keyed index cannot collide.
242+
# Each model pins its result_type Literal to one tag, so this index cannot collide.
252243
by_model[claim.model] = claim
253244
if extension_claims:
254245
claims[identifier] = extension_claims
@@ -349,13 +340,9 @@ async def main():
349340
extensions: Sequence[ClientExtension] | None = None
350341
"""Opt-in client extensions (SEP-2133).
351342
352-
Each instance contributes its capability ad (advertised under
353-
`ClientCapabilities.extensions`), its result claims (extra `tools/call` result
354-
shapes that `call_tool` resolves transparently through the claim's resolver),
355-
and its notification bindings. For an ad-only entry — an identifier plus
356-
settings, no client-side behaviour — use `mcp.client.advertise(identifier,
357-
settings)`. Each extension's `settings()` is read once, at construction; the
358-
returned dict is held by reference."""
343+
Each instance contributes its capability ad, its result claims (resolved
344+
transparently by `call_tool`), and its notification bindings. For an
345+
ad-only entry use `mcp.client.advertise(identifier, settings)`."""
359346

360347
cache: CacheConfig | Literal[False] | None = None
361348
"""Client-side response caching for the SEP-2549 cacheable methods (2026-07-28).
@@ -701,11 +688,9 @@ async def call_tool(
701688
persist `request_state` across process restarts — use
702689
`client.session.call_tool(..., allow_input_required=True)`.
703690
704-
If the server returns a result shape claimed by one of this client's
705-
`extensions`, the owning claim's resolver finishes the call and its
706-
`CallToolResult` is returned — the claimed shape never surfaces here.
707-
Resolver exceptions propagate as-is; the extension owns its error
708-
vocabulary. To receive the claimed shape yourself, use
691+
Result shapes claimed by this client's `extensions` are finished by the
692+
owning claim's resolver, whose `CallToolResult` is returned; resolver
693+
exceptions propagate as-is. To receive the claimed shape yourself, use
709694
`client.session.call_tool(..., allow_claimed=True)`.
710695
711696
Args:
@@ -736,26 +721,21 @@ async def retry(r: InputResponses | None, s: str | None) -> CallToolResult | Inp
736721
request_state=s,
737722
meta=meta,
738723
allow_input_required=True,
739-
# The driver's retry leg must also admit claimed shapes — the spec
740-
# resolves multi-round-trip input before a claimed result, so a claim
741-
# may terminate any round, not just the first.
724+
# Input rounds resolve before a claimed result, so a claim may end any round.
742725
allow_claimed=True,
743726
)
744727

745728
result = await self._drive_input_required(await retry(input_responses, request_state), retry)
746729
if isinstance(result, CallToolResult):
747730
return result
748-
# Only claimed shapes escape the parse (`_drive_input_required` never returns an
749-
# `InputRequiredResult`), so the lookup is total; a KeyError here is an SDK bug.
731+
# Only claimed shapes reach this point, so the lookup is total.
750732
claim = self._folded_extensions.by_model[type(result)]
751733
final = await claim.resolve(
752734
result,
753735
ClaimContext(session=self.session, tool_name=name, read_timeout_seconds=read_timeout_seconds),
754736
)
755737
if not final.is_error:
756-
# The resolver's product gets the same output-schema revalidation as the
757-
# direct path (`ClientSession.call_tool`'s own guard); isError results
758-
# must not raise, also matching the direct path.
738+
# Match the direct path: revalidate the output schema, but never for isError results.
759739
await self.session.validate_tool_result(name, final)
760740
return final
761741

0 commit comments

Comments
 (0)