@@ -202,19 +202,11 @@ class _FoldedExtensions:
202202
203203
204204def _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