You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Resolvers can now return Sample(...) or ListRoots() in addition to
Elicit: on 2026-07-28 sessions the request batches into the
multi-round-trip InputRequiredResult flow, on 2025-11-25 it goes over
the standalone back-channel request. One rendering produces the
identical wire request on both transports, and marker-routed legacy
sends bypass the deprecated session wrappers so no SEP-2577 warning
fires for the compatibility path.
Sampling and roots results are persisted in request_state like
elicited answers (the client pays for an LLM call once per tool call,
not once per round), pinned to the exact rendered request. Because the
response union cannot always discriminate the two sampling result
shapes, an answer is validated against the marker's expected model
rather than trusting the union member.
The elicitation-only capability check generalizes to a per-kind gate
applied before sending on either transport: sampling, roots, and
elicitation - including sampling.tools when the request carries tools,
reported in full in the -32021 requiredCapabilities payload. This also
gates the previously unchecked 2025 elicitation leg (documented in the
migration guide).
Client gains sampling_capabilities so sampling sub-capabilities like
tools support can be declared alongside sampling_callback.
Sampling sub-capabilities are the one refinement: pass `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` alongside `sampling_callback` when your sampler handles the `tools` / `tool_choice` parameters - servers must see `sampling.tools` declared before sending them.
82
+
81
83
`logging_callback` and `message_handler` are not in the table. They handle notifications, and notifications need no capability.
82
84
83
85
The server reads the declaration back with `ctx.session.check_client_capability(...)`. Add a tool that does:
Copy file name to clipboardExpand all lines: docs/handlers/dependencies.md
+13Lines changed: 13 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -134,12 +134,25 @@ That's the right default for a precondition: no answer, no order. When declining
134
134
to bind to. A question built from such volatile data makes every recorded answer look stale,
135
135
so the server re-asks it on every round until the client's round limit ends the call.
136
136
137
+
## Ask the client, not the user
138
+
139
+
Elicitation is one of three questions a resolver can ask - the closed set the multi-round-trip flow allows. The other two go to the **client** rather than the user: return `Sample(...)` to run an LLM call through the client (a `sampling/createMessage` request), or `ListRoots()` to fetch the client's current roots. Neither has an accept/decline outcome - the consumer annotates the result type directly, `CreateMessageResult` (`CreateMessageResultWithTools` when the request carries tools) or `ListRootsResult`:
140
+
141
+
```python title="server.py" hl_lines="11-16 22"
142
+
--8<--"docs_src/dependencies/tutorial004.py"
143
+
```
144
+
145
+
* The framework routes these exactly like `Elicit`: inside the multi-round-trip `tools/call` on **2026-07-28**, over the standalone server->client request on **2025-11-25** - and on either transport it refuses with a `-32021` protocol error when the client never declared the matching capability (`sampling`, `roots`, `elicitation`; `sampling.tools` when the request carries tools).
146
+
* Everything the info box above says about questions applies unchanged: a `Sample` request is matched to its recorded result by its exact rendering, so build it deterministically from the tool's arguments and earlier answers - the client then pays for the LLM call once per tool call, not once per round. The recorded result rides `request_state` for the rest of the call, so a very large completion makes every remaining round-trip heavier.
147
+
* The standalone sampling and roots *features* are deprecated at 2026-07-28 (SEP-2577) - new servers that need the client's model ask through this carrier instead, and servers that don't should integrate with an LLM provider directly. `include_context` values other than `"none"` are themselves deprecated; avoid them.
148
+
137
149
## Recap
138
150
139
151
*`Annotated[T, Resolve(fn)]` on a tool parameter: the SDK runs `fn` and injects its return value.
140
152
* A resolved parameter is invisible to the model and cannot be supplied by a client. Values the model must not invent - prices, identities, permissions - belong here.
141
153
* A resolver's parameters are resolved the same way: the `Context`, another `Resolve(...)`, or a tool argument by name. The graph runs each resolver at most once per round, however many consumers it has; each question is asked exactly once, and any resolver may run again when a call resumes after a question.
142
154
* Bad graphs fail at registration with `InvalidSignature`, not mid-call.
143
155
* Return `Elicit(message, Model)` to ask the user, only when you have to. Unwrapped annotations abort on decline; `ElicitationResult[T]` lets the tool branch.
156
+
* Return `Sample(...)` or `ListRoots()` to ask the client - an LLM completion or the roots list, injected as the plain result.
144
157
145
158
The state your server builds once at startup, and how a handler reaches it, is the **[Lifespan](lifespan.md)** page.
Copy file name to clipboardExpand all lines: docs/handlers/multi-round-trip.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -19,7 +19,7 @@ That's the whole protocol. Every leg is an ordinary request from the client to t
19
19
20
20
## The server side
21
21
22
-
On `@mcp.tool()` you rarely build this by hand: declare a dependency that asks the user and the SDK returns the `InputRequiredResult` for you - that form is the **[Dependencies](dependencies.md)** page. The two forms don't mix: a call has one `input_responses`/`request_state` channel, so a tool that uses `Resolve(...)` parameters cannot also return `InputRequiredResult` from its body. A declared `InputRequiredResult` return is rejected at registration (`InvalidSignature`), and an undeclared one fails the call at runtime. The manual form is the **low-level**`Server`, whose `on_call_tool` handler is allowed to return either result type:
22
+
On `@mcp.tool()` you rarely build this by hand: declare a dependency that asks the user (`Elicit`), samples the client's LLM (`Sample`), or lists its roots (`ListRoots`) and the SDK returns the `InputRequiredResult` for you - that form is the **[Dependencies](dependencies.md)** page. The two forms don't mix: a call has one `input_responses`/`request_state` channel, so a tool that uses `Resolve(...)` parameters cannot also return `InputRequiredResult` from its body. A declared `InputRequiredResult` return is rejected at registration (`InvalidSignature`), and an undeclared one fails the call at runtime. The manual form is the **low-level**`Server`, whose `on_call_tool` handler is allowed to return either result type:
0 commit comments