Server-initiated sampling and elicitation are unreachable on FastMCP 4

Author: DouweMCreated Sep 18, 2026Updated Sep 18, 2026
Labelsbug

Description

MCPToolset(..., sampling_model=...) and elicitation_handler= don't work on FastMCP 4, which is what a fresh pip install "pydantic-ai-slim[mcp]" resolves to today. Both are server-initiated requests, and FastMCP 4's default modern session has no back-channel for them:

NoBackChannelError: Cannot send 'sampling/createMessage': this transport context has no back-channel
for server-initiated requests.

That's structural, not a bug in the SDK — mcp/server/connection.py installs _NoChannelOutbound deliberately: "Connection.from_envelope installs this so the modern single-exchange path never needs a mode flag — the channel itself says no." A legacy session still has the back-channel.

Sampling therefore still works on FastMCP 4, but only through a pre-built client that negotiates legacy mode:

python
client = Client(transport, mode='legacy', sampling_handler=sampling_handler)
agent = Agent(model, toolsets=[MCPToolset(client)])

And that path can't use our own shortcuts: MCPToolset raises Cannot pass 'sampling_model' alongside a pre-built fastmcp.Client, and exposes no way to ask for a legacy session when it builds the client itself. So on the default install, the nicest sampling API we document is unreachable, and users must hand-build a client and hand-write a handler.

log_level has the same shape of problem — MCPToolset.log_level's own docstring already notes a modern session "warns and leaves it unapplied."

Possible direction: let MCPToolset negotiate the session mode when it builds its own client (e.g. a protocol_mode= argument, or implying legacy when sampling_model / elicitation_handler / log_level is set), so the documented shortcuts keep working on FastMCP 4.

Minimal, Reproducible Example

python
import asyncio, sys
from fastmcp.client.transports import StdioTransport
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPToolset
from pydantic_ai.models.test import TestModel

# server.py defines a tool calling MCPSamplingModel(session=ctx.session)
toolset = MCPToolset(
    StdioTransport(command=sys.executable, args=['server.py']),
    sampling_model=TestModel(custom_output_text='hi'),
)
agent = Agent(TestModel(call_tools=['poet']), toolsets=[toolset])
asyncio.run(agent.run('go'))  # tool fails; NoBackChannelError on the server side

Swapping the toolset for MCPToolset(Client(transport, mode='legacy', sampling_handler=...)) succeeds.

Why it wasn't caught

The sampling and elicitation client examples in docs/mcp/client.md are test="skip", and tests/models/test_mcp_sampling.py drives a mocked session rather than a real FastMCP 4 server, so no test exercises server-initiated sampling against a live modern session.

Python, Pydantic AI & LLM client version

  • Pydantic AI: main
  • fastmcp-slim 4.0.3, mcp 2.0.0