Skip to content

Commit 8c62f61

Browse files
committed
docs: add a Sampling and roots page under Inside your handler
A short page for the two deprecated ask-the-client features: the Sample/ListRoots resolver way with tested snippets, the capability gate, and a warning box carrying the SEP-2577 deprecation scope (functional for at least twelve months before eligibility for removal, with the spec's suggested migrations). Also reworks dash-heavy sentences in this branch's earlier doc additions into plainer structure.
1 parent bc3b145 commit 8c62f61

11 files changed

Lines changed: 163 additions & 9 deletions

File tree

docs/client/callbacks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ When a client connects it declares its `capabilities`, the mirror image of the s
7878
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
7979
| none of them | `{}` |
8080

81-
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.
81+
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 they can send them.
8282

8383
`logging_callback` and `message_handler` are not in the table. They handle notifications, and notifications need no capability.
8484

docs/handlers/dependencies.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,15 @@ That's the right default for a precondition: no answer, no order. When declining
136136

137137
## Ask the client, not the user
138138

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`:
139+
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 `ListRootsResult`:
140140

141141
```python title="server.py" hl_lines="11-16 22"
142142
--8<-- "docs_src/dependencies/tutorial004.py"
143143
```
144144

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.
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**. 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; servers that don't should integrate with an LLM provider directly. `include_context` values other than `"none"` are themselves deprecated; avoid them.
148148

149149
## Recap
150150

@@ -153,6 +153,6 @@ Elicitation is one of three questions a resolver can ask - the closed set the mu
153153
* 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.
154154
* Bad graphs fail at registration with `InvalidSignature`, not mid-call.
155155
* 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.
156+
* Return `Sample(...)` or `ListRoots()` to ask the client for an LLM completion or the roots list; the plain result is injected.
157157

158158
The state your server builds once at startup, and how a handler reaches it, is the **[Lifespan](lifespan.md)** page.

docs/handlers/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ What it can do while it runs:
1818
* Ask the user for more input with **[Elicitation](elicitation.md)**, and
1919
**[Multi-round-trip requests](multi-round-trip.md)**, the 2026-07-28
2020
pattern that carries it.
21+
* Ask the client for an LLM completion or its workspace folders with
22+
**[Sampling and roots](sampling-and-roots.md)**, deprecated but still
23+
served.
2124
* Report **[Progress](progress.md)** on something slow.
2225
* Write logs (to standard error, for whoever operates the server) with
2326
**[Logging](logging.md)**.

docs/handlers/multi-round-trip.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ That's the whole protocol. Every leg is an ordinary request from the client to t
1919

2020
## The server side
2121

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:
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:
2323

2424
```python title="server.py" hl_lines="44-47"
2525
--8<-- "docs_src/mrtr/tutorial001.py"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Sampling and roots
2+
3+
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**).
4+
5+
Both still work, on every protocol version the SDK speaks. But read the warning before you design around them:
6+
7+
!!! warning "Deprecated by the 2026-07-28 specification"
8+
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)**.
9+
10+
## Sampling: borrow the client's model
11+
12+
A resolver returns `Sample(...)` and the tool receives the completion, through the same dependency mechanism that runs `Elicit` in **[Dependencies](dependencies.md)**:
13+
14+
```python title="server.py" hl_lines="11-16 20"
15+
--8<-- "docs_src/sampling_and_roots/tutorial001.py"
16+
```
17+
18+
* `Sample(messages, max_tokens=...)` mirrors the `sampling/createMessage` parameters. The injected value is the client's `CreateMessageResult`; pass `tools=[...]` and it becomes a `CreateMessageResultWithTools` instead.
19+
* The client must have declared the `sampling` capability (`sampling.tools` if you pass tools). If it didn't, the call fails with a `-32021` protocol error before anything is sent.
20+
* 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.
21+
* Leave `include_context` alone: values other than `"none"` are themselves deprecated (SEP-2596) and need a capability almost no client declares.
22+
23+
## Roots: where should this go?
24+
25+
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()`:
26+
27+
```python title="server.py" hl_lines="11-12 16"
28+
--8<-- "docs_src/sampling_and_roots/tutorial002.py"
29+
```
30+
31+
* The injected `ListRootsResult` carries a list of `Root`s: a `file://` URI and an optional display name.
32+
* The gate is the same as for sampling: without a declared `roots` capability the call fails with `-32021` before a request is sent.
33+
34+
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)**.
35+
36+
## On 2025-era connections
37+
38+
`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.
39+
40+
## Recap
41+
42+
* Return `Sample(...)` or `ListRoots()` from a resolver; the tool receives the `CreateMessageResult` or `ListRootsResult` like any other dependency.
43+
* The client must declare the matching capability, or the call fails with `-32021` before a request is sent.
44+
* 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.
45+
46+
Reporting how far along a slow tool is: **[Progress](progress.md)**.

docs/migration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ A v1 server could call `ctx.elicit()`, `create_message()`, or `list_roots()`
4646
against any client; nothing checked what the client had declared. In v2 the
4747
`Resolve(...)` markers (`Elicit`, `Sample`, `ListRoots`) enforce the spec's
4848
egress rule on both transports: if the client never declared the matching
49-
capability (`elicitation`, `sampling`plus `sampling.tools` when the request
50-
carries tools — or `roots`), the call fails with a `-32021`
49+
capability (`elicitation`, `sampling`, or `roots`, plus `sampling.tools` when
50+
the request carries tools), the call fails with a `-32021`
5151
`MISSING_REQUIRED_CLIENT_CAPABILITY` JSON-RPC error instead of sending a
5252
request the client cannot handle. This applies on 2025-11-25 sessions too, so a
5353
client that answered elicitations without declaring the capability now sees the

docs_src/sampling_and_roots/__init__.py

Whitespace-only changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Annotated
2+
3+
from mcp_types import CreateMessageResult, SamplingMessage, TextContent
4+
5+
from mcp.server import MCPServer
6+
from mcp.server.mcpserver import Resolve, Sample
7+
8+
mcp = MCPServer("Bookshop")
9+
10+
11+
def draft_blurb(title: str) -> Sample:
12+
prompt = f"Write a one-sentence blurb for the book {title!r}."
13+
return Sample(
14+
[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
15+
max_tokens=60,
16+
)
17+
18+
19+
@mcp.tool()
20+
async def blurb(title: str, draft: Annotated[CreateMessageResult, Resolve(draft_blurb)]) -> str:
21+
"""Draft a blurb for a book."""
22+
return draft.content.text if draft.content.type == "text" else "No blurb."
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from typing import Annotated
2+
3+
from mcp_types import ListRootsResult
4+
5+
from mcp.server import MCPServer
6+
from mcp.server.mcpserver import ListRoots, Resolve
7+
8+
mcp = MCPServer("Bookshop")
9+
10+
11+
def workspace_roots() -> ListRoots:
12+
return ListRoots()
13+
14+
15+
@mcp.tool()
16+
async def catalog_folder(roots: Annotated[ListRootsResult, Resolve(workspace_roots)]) -> str:
17+
"""Pick the folder the catalog export should go to."""
18+
if not roots.roots:
19+
return "No workspace folders shared."
20+
return str(roots.roots[0].uri)

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ nav:
3636
- Lifespan: handlers/lifespan.md
3737
- Elicitation: handlers/elicitation.md
3838
- Multi-round-trip requests: handlers/multi-round-trip.md
39+
- Sampling and roots: handlers/sampling-and-roots.md
3940
- Progress: handlers/progress.md
4041
- Logging: handlers/logging.md
4142
- Subscriptions: handlers/subscriptions.md

0 commit comments

Comments
 (0)