Skip to content
Merged
2 changes: 2 additions & 0 deletions docs/client/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ When a client connects it declares its `capabilities`, the mirror image of the s
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
| none of them | `{}` |

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.

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

The server reads the declaration back with `ctx.session.check_client_capability(...)`. Add a tool that does:
Expand Down
13 changes: 13 additions & 0 deletions docs/handlers/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,25 @@ 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.
* 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.
* 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.
3 changes: 3 additions & 0 deletions docs/handlers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**.
Expand Down
2 changes: 1 addition & 1 deletion docs/handlers/multi-round-trip.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
46 changes: 46 additions & 0 deletions docs/handlers/sampling-and-roots.md
Original file line number Diff line number Diff line change
@@ -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)**.
18 changes: 18 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,24 @@ and raises `RuntimeError` if the resource requests input.

The internal layers (`ToolManager.call_tool`, `Tool.run`, `Prompt.render`, `ResourceTemplate.create_resource`, etc.) now require `context` as a positional argument.

### Resolver-routed requests require the client capability on every protocol version

A v1 server could send elicitation, sampling, and roots requests to clients
that never declared the matching capability; only tools-bearing sampling was
checked. In v2 the `Resolve(...)` markers (`Elicit`, `Sample`, `ListRoots`)
enforce the spec's egress rule: an undeclared capability (form-mode `elicitation`,
`sampling`, or `roots`, plus `sampling.tools` when the request carries `tools`
or `tool_choice`) fails the call with a `-32021`
`MISSING_REQUIRED_CLIENT_CAPABILITY` JSON-RPC error instead of sending a
request the client cannot handle. This applies on 2025-11-25 sessions with a
live back-channel too; a session with no back-channel keeps failing with its
no-back-channel error. To migrate, declare the capability: the SDK client
declares `elicitation`, `sampling`, and `roots` when the matching callback is
set, and `sampling.tools` needs an explicit
`Client(sampling_capabilities=SamplingCapability(tools=...))`. Direct
`ctx.elicit()` and `ctx.session.*` calls outside resolvers keep their previous
behavior, including the pre-existing tools check on `create_message`.

### `MCPError` raised from an `@mcp.tool()` handler now surfaces as a JSON-RPC error

Raising `MCPError` (or any subclass) inside an `@mcp.tool()` handler now
Expand Down
26 changes: 26 additions & 0 deletions docs_src/dependencies/tutorial004.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Annotated

from mcp_types import CreateMessageResult, SamplingMessage, TextContent

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve, Sample

mcp = MCPServer("Bookshop")


def suggest_title(genre: str) -> Sample:
prompt = f"Suggest one {genre} book title. Answer with the title only."
return Sample(
[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
max_tokens=50,
)


@mcp.tool()
async def recommend_book(
genre: str,
suggestion: Annotated[CreateMessageResult, Resolve(suggest_title)],
) -> str:
"""Recommend a book in the given genre."""
title = suggestion.content.text if suggestion.content.type == "text" else "the classics"
return f"Today's {genre} pick: {title}"
Empty file.
22 changes: 22 additions & 0 deletions docs_src/sampling_and_roots/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import Annotated

from mcp_types import CreateMessageResult, SamplingMessage, TextContent

from mcp.server import MCPServer
from mcp.server.mcpserver import Resolve, Sample

mcp = MCPServer("Bookshop")


def draft_blurb(title: str) -> Sample:
prompt = f"Write a one-sentence blurb for the book {title!r}."
return Sample(
[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
max_tokens=60,
)


@mcp.tool()
async def blurb(title: str, draft: Annotated[CreateMessageResult, Resolve(draft_blurb)]) -> str:
"""Draft a blurb for a book."""
return draft.content.text if draft.content.type == "text" else "No blurb."
20 changes: 20 additions & 0 deletions docs_src/sampling_and_roots/tutorial002.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import Annotated

from mcp_types import ListRootsResult

from mcp.server import MCPServer
from mcp.server.mcpserver import ListRoots, Resolve

mcp = MCPServer("Bookshop")


def workspace_roots() -> ListRoots:
return ListRoots()


@mcp.tool()
async def catalog_folder(roots: Annotated[ListRootsResult, Resolve(workspace_roots)]) -> str:
"""Pick the folder the catalog export should go to."""
if not roots.roots:
return "No workspace folders shared."
return str(roots.roots[0].uri)
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ nav:
- Lifespan: handlers/lifespan.md
- Elicitation: handlers/elicitation.md
- Multi-round-trip requests: handlers/multi-round-trip.md
- Sampling and roots: handlers/sampling-and-roots.md
- Progress: handlers/progress.md
- Logging: handlers/logging.md
- Subscriptions: handlers/subscriptions.md
Expand Down
4 changes: 4 additions & 0 deletions src/mcp/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ async def main():
sampling_callback: SamplingFnT | None = None
"""Callback for handling sampling requests."""

sampling_capabilities: types.SamplingCapability | None = None
"""Sampling sub-capabilities (e.g. tools) declared alongside `sampling_callback`; no effect without it."""

list_roots_callback: ListRootsFnT | None = None
"""Callback for handling list roots requests."""

Expand Down Expand Up @@ -418,6 +421,7 @@ async def _build_session(self, exit_stack: AsyncExitStack) -> ClientSession:
dispatcher=dispatcher,
read_timeout_seconds=self.read_timeout_seconds,
sampling_callback=self.sampling_callback,
sampling_capabilities=self.sampling_capabilities,
list_roots_callback=self.list_roots_callback,
logging_callback=self.logging_callback,
message_handler=message_handler,
Expand Down
4 changes: 4 additions & 0 deletions src/mcp/server/mcpserver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
DeclinedElicitation,
Elicit,
ElicitationResult,
ListRoots,
Resolve,
Sample,
)
from .resources import DEFAULT_RESOURCE_SECURITY, ResourceSecurity
from .server import MCPServer, require_client_extension
Expand All @@ -33,6 +35,8 @@
"Icon",
"Resolve",
"Elicit",
"Sample",
"ListRoots",
"ElicitationResult",
"AcceptedElicitation",
"DeclinedElicitation",
Expand Down
Loading
Loading