Tighten comments and docstrings in the resolver marker additions · modelcontextprotocol/python-sdk@1a12981 · GitHub
Skip to content

Commit 1a12981

Browse files
committed
Tighten comments and docstrings in the resolver marker additions
1 parent f24a39f commit 1a12981

3 files changed

Lines changed: 31 additions & 82 deletions

File tree

src/mcp/client/client.py

Lines changed: 1 addition & 4 deletions

src/mcp/server/mcpserver/resolve.py

Lines changed: 24 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,13 @@
44
resolver `fn` before the tool body, instead of from the LLM-supplied arguments.
55
Resolvers form a DAG: a resolver may declare its own `Resolve(...)` dependencies,
66
take 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
2014
bodies may re-run on every round; a recorded outcome is consulted only when the
2115
body asks its question again, so a resolver's own computation always wins over
2216
anything the client echoes back in `request_state`.
@@ -28,9 +22,7 @@
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

3628
from __future__ import annotations
@@ -127,22 +119,12 @@ def __init__(self, message: str, schema: type[T]) -> None:
127119

128120

129121
class 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

177159
class 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

190167
class _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
597570
async 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

674643
def _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

697665
def _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":

tests/server/mcpserver/test_resolve.py

Lines changed: 6 additions & 17 deletions

0 commit comments

Comments
 (0)