Server-initiated sampling and elicitation are unreachable on FastMCP 4
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:
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
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 sideSwapping 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
Source: pydantic/pydantic-ai