Extend resolver DI to sampling and roots requests (#3049)
This commit is contained in:
parent
53117cb3a9
commit
d287c9868f
4 changed files with 1134 additions and 200 deletions
|
|
@ -134,6 +134,18 @@ That's the right default for a precondition: no answer, no order. When declining
|
|||
to bind to. A question built from such volatile data makes every recorded answer look stale,
|
||||
so the server re-asks it on every round until the client's round limit ends the call.
|
||||
|
||||
## Ask the client, not the user
|
||||
|
||||
Elicitation is one of the three questions a resolver can ask, and the multi-round-trip flow allows no others. 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 `tool_choice`) or `ListRootsResult`:
|
||||
|
||||
```python title="server.py" hl_lines="11-16 22"
|
||||
--8<-- "docs_src/dependencies/tutorial004.py"
|
||||
```
|
||||
|
||||
* 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**. An undeclared capability refuses the call with a `-32021` protocol error (`sampling`, `roots`, form-mode `elicitation`; `sampling.tools` when the request carries `tools` or `tool_choice`).
|
||||
* 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.
|
||||
* 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; servers that don't should integrate with an LLM provider directly. `include_context` values other than `"none"` are themselves deprecated; avoid them.
|
||||
|
||||
## Recap
|
||||
|
||||
* `Annotated[T, Resolve(fn)]` on a tool parameter: the SDK runs `fn` and injects its return value.
|
||||
|
|
@ -141,5 +153,6 @@ That's the right default for a precondition: no answer, no order. When declining
|
|||
* 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.
|
||||
* Bad graphs fail at registration with `InvalidSignature`, not mid-call.
|
||||
* Return `Elicit(message, Model)` to ask the user, only when you have to. Unwrapped annotations abort on decline; `ElicitationResult[T]` lets the tool branch.
|
||||
* Return `Sample(...)` or `ListRoots()` to ask the client for an LLM completion or the roots list; the plain result is injected.
|
||||
|
||||
The state your server builds once at startup, and how a handler reaches it, is the **[Lifespan](lifespan.md)** page.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ What it can do while it runs:
|
|||
* Ask the user for more input with **[Elicitation](elicitation.md)**, and
|
||||
**[Multi-round-trip requests](multi-round-trip.md)**, the 2026-07-28
|
||||
pattern that carries it.
|
||||
* Ask the client for an LLM completion or its workspace folders with
|
||||
**[Sampling and roots](sampling-and-roots.md)**, deprecated but still
|
||||
served.
|
||||
* Report **[Progress](progress.md)** on something slow.
|
||||
* Write logs (to standard error, for whoever operates the server) with
|
||||
**[Logging](logging.md)**.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ That's the whole protocol. Every leg is an ordinary request from the client to t
|
|||
|
||||
## The server side
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```python title="server.py" hl_lines="44-47"
|
||||
--8<-- "docs_src/mrtr/tutorial001.py"
|
||||
|
|
|
|||
46
docs/handlers/sampling-and-roots.md
Normal file
46
docs/handlers/sampling-and-roots.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Sampling and roots
|
||||
|
||||
A handler can ask the connected client for two more things: a completion from the client's own model (**sampling**), and the client's workspace folders (**roots**).
|
||||
|
||||
Both still work, on every protocol version the SDK speaks. But read the warning before you design around them:
|
||||
|
||||
!!! warning "Deprecated by the 2026-07-28 specification"
|
||||
Sampling and roots are deprecated as of `2026-07-28` ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2577)). They remain fully functional and stay in the specification for at least twelve months before becoming eligible for removal, but new implementations should not build on them. The suggested migrations: integrate directly with your LLM provider's API instead of sampling, and pass directories via tool parameters, resource URIs, or server configuration instead of roots. The SDK-wide list is in **[Deprecated features](../deprecated.md)**.
|
||||
|
||||
## Sampling: borrow the client's model
|
||||
|
||||
A resolver returns `Sample(...)` and the tool receives the completion, through the same dependency mechanism that runs `Elicit` in **[Dependencies](dependencies.md)**:
|
||||
|
||||
```python title="server.py" hl_lines="11-16 20"
|
||||
--8<-- "docs_src/sampling_and_roots/tutorial001.py"
|
||||
```
|
||||
|
||||
* `Sample(messages, max_tokens=...)` mirrors the `sampling/createMessage` parameters. The injected value is the client's `CreateMessageResult`; pass `tools` or `tool_choice` and it becomes a `CreateMessageResultWithTools` instead.
|
||||
* The client must have declared the `sampling` capability (`sampling.tools` if you pass `tools` or `tool_choice`). If it didn't, the call fails with a `-32021` protocol error instead of sending a request the client cannot handle. A pre-2026 session with no back-channel fails with its usual no-back-channel error, since there is nothing to send on.
|
||||
* At `2026-07-28` the request is delivered inside the multi-round-trip flow (**[Multi-round-trip requests](multi-round-trip.md)**); on `2025-11-25` it is a standalone request to the client. The code is the same either way, but mind the multi-round-trip rule: the request must render identically across retry rounds, so build it only from the tool's arguments and other stable data.
|
||||
* Leave `include_context` alone: values other than `"none"` are themselves deprecated (SEP-2596) and need a capability almost no client declares.
|
||||
|
||||
## Roots: where should this go?
|
||||
|
||||
Roots are the folders the client says the server may operate on. They are informational guidance, not an access-control mechanism. A resolver returns `ListRoots()`:
|
||||
|
||||
```python title="server.py" hl_lines="11-12 16"
|
||||
--8<-- "docs_src/sampling_and_roots/tutorial002.py"
|
||||
```
|
||||
|
||||
* The injected `ListRootsResult` carries a list of `Root`s: a `file://` URI and an optional display name.
|
||||
* The gate is the same as for sampling: without a declared `roots` capability the call fails with `-32021` instead of sending the request.
|
||||
|
||||
On the other side of the wire, the client answers both requests with the callbacks it already has: `sampling_callback` and `list_roots_callback`, covered in **[Client callbacks](../client/callbacks.md)**.
|
||||
|
||||
## On 2025-era connections
|
||||
|
||||
`ctx.session.create_message(...)` and `ctx.session.list_roots()` still exist for code that drives the session directly. They only work where a back-channel exists (2025-era, non-stateless connections), and calling them raises a deprecation warning. The resolver markers above are the supported form: they pick the delivery from the negotiated version and don't warn.
|
||||
|
||||
## Recap
|
||||
|
||||
* Return `Sample(...)` or `ListRoots()` from a resolver; the tool receives the `CreateMessageResult` or `ListRootsResult` like any other dependency.
|
||||
* The client must declare the matching capability, or the call fails with `-32021` instead of a request being sent.
|
||||
* Both features are deprecated at `2026-07-28`: fully functional for now, wrong for new designs. Prefer provider APIs over sampling and explicit parameters over roots.
|
||||
|
||||
Reporting how far along a slow tool is: **[Progress](progress.md)**.
|
||||
Loading…
Reference in a new issue