44resolver `fn` before the tool body, instead of from the LLM-supplied arguments.
55Resolvers form a DAG: a resolver may declare its own `Resolve(...)` dependencies,
66take tool arguments by name, and take the `Context`. A resolver may return a
7- request marker - `Elicit[T]` to ask the user, `Sample` to run an LLM call on the
8- client, or `ListRoots` to fetch the client's roots - and the framework runs the
9- request and injects the response. These are the three request kinds the
10- multi-round-trip flow allows.
11-
12- The framework picks the transport from the negotiated protocol. At >= 2026-07-28
13- it returns an `InputRequiredResult` carrying the batched requests and resumes
14- when the client retries with `input_responses`/`request_state` (independent
15- resolvers are asked in one round; a resolver depending on another's answer is
16- asked in a later round). At <= 2025-11-25 it issues the standalone server-to-client
17- request (`elicitation/create`, `sampling/createMessage`, `roots/list`) mid-call.
18- Only *asked* outcomes are carried in `request_state` across rounds (so the user
19- is asked - and the client's LLM is sampled - once per question). Resolver
7+ request marker (`Elicit[T]` to ask the user, `Sample` to sample the client's
8+ LLM, `ListRoots` to fetch its roots); the framework injects the response.
9+
10+ The transport follows the negotiated protocol: >= 2026-07-28 batches the requests
11+ into an `InputRequiredResult` and resumes when the client retries with
12+ `input_responses`/`request_state`; <= 2025-11-25 sends each standalone server-to-client
13+ request mid-call. Only *asked* outcomes ride `request_state`, so each question is asked once. Resolver
2014bodies may re-run on every round; a recorded outcome is consulted only when the
2115body asks its question again, so a resolver's own computation always wins over
2216anything the client echoes back in `request_state`.
2822- `Annotated[ElicitationResult[T], Resolve(fn)]` (or a specific member) -> the
2923 full outcome; the consumer branches on accept/decline/cancel.
3024
31- `Sample` and `ListRoots` have no decline arm (a client refuses by erroring), so
32- their consumers annotate the result directly: `CreateMessageResult` (or
33- `CreateMessageResultWithTools` when tools are given) and `ListRootsResult`.
25+ `Sample` and `ListRoots` have no decline arm; their consumers annotate the result type directly.
3426"""
3527
3628from __future__ import annotations
@@ -127,22 +119,12 @@ def __init__(self, message: str, schema: type[T]) -> None:
127119
128120
129121class Sample :
130- """A resolver's request to sample the client's LLM.
131-
132- Returned from a resolver to have the client run an LLM call; the framework
133- injects the `CreateMessageResult` (a `CreateMessageResultWithTools` when
134- `tools` are given). Requires the client to declare the `sampling` capability
135- (plus `sampling.tools` when tools are given). Mirrors the parameters of
136- `sampling/createMessage`.
137-
138- On >= 2026-07-28 the rendered request must be identical across retry rounds
139- (the recorded result is pinned to it) - derive it only from tool arguments
140- and stable data, never timestamps or random values. The sampled result rides
141- the `request_state` envelope on every remaining round, so very large
142- completions inflate the rest of the exchange.
143-
144- Note: `include_context` values other than "none" are deprecated in the draft
145- specification and should be avoided.
122+ """A resolver's request to sample the client's LLM via `sampling/createMessage`.
123+
124+ The framework injects a `CreateMessageResult` (`CreateMessageResultWithTools` when `tools` are
125+ given); requires the `sampling` capability (`sampling.tools` when tools are given). On
126+ >= 2026-07-28 the request must render identically across retry rounds, and the sampled result
127+ rides `request_state` on every later round. `include_context` other than "none" is deprecated in the draft spec.
146128 """
147129
148130 def __init__ (
@@ -175,16 +157,11 @@ def __init__(
175157
176158
177159class ListRoots :
178- """A resolver's request for the client's current roots.
179-
180- Returned from a resolver to fetch the client's roots list; the framework
181- injects the `ListRootsResult`. Requires the client to declare the `roots`
182- capability.
183- """
160+ """A resolver's request for the client's roots via `roots/list`; the framework injects the `ListRootsResult`."""
184161
185162
186163_Marker = Elicit [Any ] | Sample | ListRoots
187- """The request kinds a resolver may return - the closed set the multi-round-trip flow allows ."""
164+ """The request markers a resolver may return."""
188165
189166
190167class _ParamPlan :
@@ -308,18 +285,14 @@ def _check_elicit_return(return_annotation: Any, name: str) -> None:
308285 """Validate the request-marker arms of a resolver's return annotation.
309286
310287 Raises:
311- InvalidSignature: If the annotation has more than one marker arm
312- (`Elicit[...]`, `Sample`, `ListRoots`); a resolver asks one
313- question - a second arm means it should be split.
288+ InvalidSignature: If the annotation has more than one marker arm.
314289 """
315- # A bare marker type is itself a candidate; a union contributes its members.
316290 candidates = get_args (return_annotation ) if _is_union (return_annotation ) else (return_annotation ,)
317291 # Typing dedupes equal union members, so two arms here are genuinely distinct.
318292 arms : list [Any ] = [
319293 c
320294 for c in candidates
321- # The `get_origin(c) is None` guard keeps 3.10 safe: there `dict[str, Any]`
322- # passes `isinstance(c, type)` and would crash `issubclass`.
295+ # Origin guard for 3.10: `dict[str, Any]` passes `isinstance(c, type)` there and would crash `issubclass`.
323296 if get_origin (c ) is Elicit
324297 or (get_origin (c ) is None and isinstance (c , type ) and issubclass (c , Elicit | Sample | ListRoots ))
325298 ]
@@ -597,10 +570,8 @@ async def _resolve(fn: Callable[..., Any], res: _Resolution) -> ElicitationResul
597570async def _fulfil (marker : _Marker , key : str , res : _Resolution ) -> ElicitationResult [Any ]:
598571 """Turn a resolver's request marker into an outcome via the negotiated transport."""
599572 if not res .input_required :
600- # Gate only when the handshake's declaration is visible. A session with no
601- # client info (e.g. stateless HTTP) has no back-channel either, and the send
602- # path reports that truthfully; on >= 2026-07-28 absence means not declared,
603- # because capabilities arrive per-request there.
573+ # Gate only when the handshake's declaration is visible: a session with no
574+ # client info (e.g. stateless HTTP) has no back-channel, and the send path reports that.
604575 if res .context .client_capabilities is not None :
605576 _require_capability (res .context , marker , key )
606577 if isinstance (marker , Elicit ):
@@ -633,9 +604,7 @@ async def _fulfil(marker: _Marker, key: str, res: _Resolution) -> ElicitationRes
633604 res .pending [key ] = request
634605 raise _Pending
635606 if not isinstance (marker , Elicit ):
636- # The response union cannot always discriminate the two sampling result shapes
637- # (a no-tool-use answer to a tools request parses as the plain one), so validate
638- # the wire data against the marker's expected model instead of the union member.
607+ # A no-tool-use answer to a tools request parses as the plain result; validate against the marker's model.
639608 wire = answer .model_dump (mode = "json" , by_alias = True , exclude_none = True )
640609 try :
641610 result = _result_type (marker ).model_validate (wire )
@@ -672,7 +641,6 @@ def _unwrap(outcome: ElicitationResult[Any], name: str) -> Any:
672641
673642
674643def _is_marker (value : Any ) -> TypeGuard [_Marker ]:
675- """Runtime narrow of a resolver's return value to a request marker."""
676644 return isinstance (value , Elicit | Sample | ListRoots )
677645
678646
@@ -695,12 +663,9 @@ def _uses_input_required(protocol_version: str | None) -> bool:
695663
696664
697665def _require_capability (context : Context [Any , Any ], marker : _Marker , key : str ) -> None :
698- """Assert the client declared the capability `marker`'s request needs before sending it .
666+ """Assert the client declared the capability `marker`'s request needs.
699667
700- The spec forbids sending a client a request it has not declared a capability
701- for; the same predicate gates both transports. A bare `elicitation: {}`
702- declaration (the only shape before modes existed) counts as form support; an
703- explicit url-only declaration does not.
668+ A bare `elicitation: {}` (the only shape before modes existed) counts as form support; url-only does not.
704669
705670 Raises:
706671 MCPError: With code `MISSING_REQUIRED_CLIENT_CAPABILITY` and a
@@ -817,9 +782,7 @@ def _outcome_from_state(entry: _StateEntry, marker: _Marker) -> ElicitationResul
817782 """Rebuild an outcome from a decoded `request_state` entry.
818783
819784 Raises:
820- ValidationError: If the entry does not fit the live marker - accepted
821- data failing the expected shape, or a decline/cancel recorded for a
822- kind that has no such outcome (its `data` is `None`).
785+ ValidationError: If the entry does not fit the live marker.
823786 """
824787 if isinstance (marker , Elicit ):
825788 if entry .action == "decline" :
0 commit comments