Sampling
MCP 2026-07-28 removed sampling as a capability-driven push request. The
client now fulfils sampling/createMessage
input requests
inside its own MRTR round-trip loop, so the caller of call_tool still sees
a single call.
The whole kind is deprecated on arrival: Client::map_sampling carries
#[deprecated] and needs #[allow(deprecated)]. And the #[sampling]
attribute macro is not available in the default build — it belongs to the
legacy push model. Wire the handler with an explicit map_sampling.
In MCP, the client is responsible for executing LLM sampling requests initiated by servers. Unlike traditional architectures, the client:
- Owns model access and API keys
- Applies local policies (cost, privacy, rate limits)
- Mediates all interaction with language models
Servers never communicate with LLMs directly - they only request sampling.
Important mental model
- Server requests sampling
- Client executes sampling
- Client decides:
- which model to use
- whether tools are supported
- how prompts are handled
- Client returns structured results back to the server
Client Configuration
Sampling support must be explicitly enabled on the client:
let mut client = Client::new()
.with_options(|opt| opt
.with_sampling(|s| s.with_tools()));
- with_sampling() enables sampling support
- with_tools() allows tool calls during sampling
Registering a handler is what makes the client declare
clientCapabilities.sampling on every request; a server may only ask for a
kind the client declared, and asking an undeclared client is reported rather
than left to stall the round-trip.
Sampling Handler
Register the handler with
Client::map_sampling.
It receives a CreateMessageRequestParams and returns a
CreateMessageResult.
use neva::prelude::*;
use neva::types::sampling::{CreateMessageRequestParams, CreateMessageResult};
async fn complete(params: CreateMessageRequestParams) -> CreateMessageResult {
// Client-side sampling logic goes here
CreateMessageResult::assistant()
.with_model("o3-mini")
.with_content("...")
.end_turn()
}
#[tokio::main]
async fn main() -> Result<(), Error> {
let mut client = Client::new()
.with_options(|opt| opt.with_default_http());
// Deprecated on arrival, like the whole sampling kind.
#[allow(deprecated)]
client.map_sampling(complete);
client.connect().await?;
// The MRTR round-trips happen inside this one call.
let result = client.call_tool("summarize_report", [("topic", "EMEA")]).await?;
client.disconnect().await
}
The handler is invoked once per round in which the server calls Context::sample().
legacy-specSampling is a server→client push request, and the
#[sampling]
attribute macro registers the handler for you. See
Legacy spec.
Inspecting Sampling Requests
The incoming CreateMessageRequestParams contains:
- Prompt messages
- System prompt
- Model preferences
- Tools metadata
- Previous tool results (for multi-step sampling)
Access text prompts
let prompts: Vec<&TextContent> = params.text().collect();
This includes all user and assistant text messages accumulated so far.
Detecting tool usage requests
The client can check whether the server allows or expects tool usage via
tool_choice:
if params.tool_choice.is_some_and(|c| !c.is_none()) {
// Model is allowed or required to call tools
}
This allows the client to decide whether to produce tool calls or final text.
Tool Use
If tools are enabled, the client may respond with a tool invocation request instead of plain text.
CreateMessageResult::assistant()
.with_model("gpt-5")
.use_tools([
("get_weather", ("city", "London"))
])
- Tool execution is always performed by the server
- The client only returns intent to call tools
- Tool arguments must match the tool schema
Handling Tool Results
After the server executes tools, it will issue a follow-up sampling request containing tool results.
These results are available via:
let results: Vec<&ToolResult> = params.results().collect();
At this stage, the client should typically:
- Interpret tool outputs
- Generate a final assistant response
- End the sampling turn
Producing Final Responses
To return a normal assistant message and end the sampling loop:
CreateMessageResult::assistant()
.with_model("gpt-5")
.with_content("Final response text")
.end_turn()
Calling end_turn() signals to the server that sampling is complete.
When to Customize Client Sampling
Consider custom sampling logic when:
- You need to integrate proprietary or local models
- You want fine-grained cost or latency control
- You want to apply prompt filtering or auditing
- You need deterministic or policy-driven responses
Learn By Example
A complete working example is available here.