Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com>
177 lines
8.2 KiB
Markdown
177 lines
8.2 KiB
Markdown
---
|
|
title: Sampling
|
|
author: jeffhandley
|
|
description: How servers request LLM completions from the client using the sampling feature.
|
|
uid: sampling
|
|
---
|
|
|
|
## Sampling
|
|
|
|
MCP [sampling] allows servers to request LLM completions from the client. This enables agentic behaviors where a server-side tool delegates reasoning back to the client's language model — for example, summarizing content, generating text, or making decisions.
|
|
|
|
[sampling]: https://modelcontextprotocol.io/specification/2025-11-25/client/sampling
|
|
|
|
> [!IMPORTANT]
|
|
> Sampling is **deprecated** as of MCP specification revision `2026-07-28` ([SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577), [MCP9005](xref:list-of-diagnostics#obsolete-apis)) and may be removed in a future version.
|
|
|
|
> [!NOTE]
|
|
> Sampling is a **server-to-client request** — the server sends a request back to the client over an open connection. This requires [stateful mode or stdio](xref:stateless). Sampling is not available in [stateless mode](xref:stateless#stateless-mode-recommended) because stateless servers cannot send requests to clients. For a stateless-compatible alternative, throw `InputRequiredException` from your handler through MRTR — see [Multi round-trip requests (MRTR)](#multi-round-trip-requests-mrtr).
|
|
|
|
### How sampling works
|
|
|
|
1. The server calls <xref:ModelContextProtocol.Server.McpServer.SampleAsync*> (or uses the <xref:ModelContextProtocol.Server.McpServer.AsSamplingChatClient*> adapter) during tool execution.
|
|
2. The request is sent to the connected client over MCP.
|
|
3. The client's <xref:ModelContextProtocol.Client.McpClientHandlers.SamplingHandler> processes the request — typically by forwarding it to an LLM.
|
|
4. The client returns the LLM response to the server, which continues tool execution.
|
|
|
|
### Server: requesting a completion
|
|
|
|
To get an <xref:Microsoft.Extensions.AI.IChatClient> that sends requests through the connected client, inject <xref:ModelContextProtocol.Server.McpServer> into a tool method and use the <xref:ModelContextProtocol.Server.McpServer.AsSamplingChatClient*> extension method:
|
|
|
|
```csharp
|
|
[McpServerTool(Name = "SummarizeContent"), Description("Summarizes the given text")]
|
|
public static async Task<string> Summarize(
|
|
McpServer server,
|
|
[Description("The text to summarize")] string text,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ChatMessage[] messages =
|
|
[
|
|
new(ChatRole.User, "Briefly summarize the following content:"),
|
|
new(ChatRole.User, text),
|
|
];
|
|
|
|
ChatOptions options = new()
|
|
{
|
|
MaxOutputTokens = 256,
|
|
Temperature = 0.3f,
|
|
};
|
|
|
|
return $"Summary: {await server.AsSamplingChatClient().GetResponseAsync(messages, options, cancellationToken)}";
|
|
}
|
|
```
|
|
|
|
Alternatively, use <xref:ModelContextProtocol.Server.McpServer.SampleAsync*> directly for lower-level control:
|
|
|
|
```csharp
|
|
CreateMessageResult result = await server.SampleAsync(
|
|
new CreateMessageRequestParams
|
|
{
|
|
Messages =
|
|
[
|
|
new SamplingMessage
|
|
{
|
|
Role = Role.User,
|
|
Content = [new TextContentBlock { Text = "What is 2 + 2?" }]
|
|
}
|
|
],
|
|
MaxTokens = 100,
|
|
},
|
|
cancellationToken);
|
|
|
|
string response = result.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text ?? string.Empty;
|
|
```
|
|
|
|
### Client: handling sampling requests
|
|
|
|
Set <xref:ModelContextProtocol.Client.McpClientHandlers.SamplingHandler> when creating the client. This handler is called when a server sends a `sampling/createMessage` request.
|
|
|
|
#### Using an IChatClient
|
|
|
|
The simplest approach is to use <xref:ModelContextProtocol.AIContentExtensions.CreateSamplingHandler*> with any <xref:Microsoft.Extensions.AI.IChatClient> implementation:
|
|
|
|
```csharp
|
|
IChatClient chatClient = new OllamaChatClient(new Uri("http://localhost:11434"), "llama3");
|
|
|
|
McpClientOptions options = new()
|
|
{
|
|
Handlers = new()
|
|
{
|
|
SamplingHandler = chatClient.CreateSamplingHandler()
|
|
}
|
|
};
|
|
|
|
await using var client = await McpClient.CreateAsync(transport, options);
|
|
```
|
|
|
|
#### Custom handler
|
|
|
|
For full control, provide a custom delegate:
|
|
|
|
```csharp
|
|
McpClientOptions options = new()
|
|
{
|
|
Handlers = new()
|
|
{
|
|
SamplingHandler = async (request, progress, cancellationToken) =>
|
|
{
|
|
// Forward to your LLM, apply content filtering, etc.
|
|
string prompt = request?.Messages?.LastOrDefault()?.Content
|
|
.OfType<TextContentBlock>().FirstOrDefault()?.Text ?? string.Empty;
|
|
|
|
return new CreateMessageResult
|
|
{
|
|
Model = "my-model",
|
|
Role = Role.Assistant,
|
|
Content = [new TextContentBlock { Text = $"Response to: {prompt}" }]
|
|
};
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### Capability negotiation
|
|
|
|
Sampling requires the client to advertise the `sampling` capability. This is handled automatically — when a <xref:ModelContextProtocol.Client.McpClientHandlers.SamplingHandler> is set, the client includes the sampling capability during initialization. The server can check whether the client supports sampling before calling <xref:ModelContextProtocol.Server.McpServer.SampleAsync*>; if sampling is not supported, the method throws <xref:System.InvalidOperationException>.
|
|
|
|
### Multi round-trip requests (MRTR)
|
|
|
|
[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `2026-07-28`. In that revision, the server-to-client `sampling/createMessage` request method is removed; the recommended way to ask the client to sample from a server handler is to throw <xref:ModelContextProtocol.Protocol.InputRequiredException> and let the SDK emit an <xref:ModelContextProtocol.Protocol.InputRequiredResult> on the wire.
|
|
|
|
> [!IMPORTANT]
|
|
> `SampleAsync` and `AsSamplingChatClient` throw `InvalidOperationException("Sampling is not supported in stateless mode.")` whenever the server is running stateless — including every Streamable HTTP request served under `2026-07-28`. Stdio servers and initialize-handshake stateful Streamable HTTP sessions continue to work via the initialize-era server-to-client `sampling/createMessage` request flow; an HTTP server set to `SessionMode = HttpServerSessionMode.Stateful` refuses `2026-07-28` so dual-path clients can fall back before using that flow, while `HttpServerSessionMode.StatefulForInitializeClients` instead serves `2026-07-28` statelessly on the same endpoint (see [hybrid mode](xref:stateless#hybrid-mode-sessions-for-initialize-clients-only)), so those requests use MRTR while `initialize`-handshake sessions keep this flow. For code that needs to run on stateless servers — including `2026-07-28` Streamable HTTP — throw `InputRequiredException` from your handler instead. It works across both protocol eras and all three HTTP `SessionMode` configurations.
|
|
|
|
For example:
|
|
|
|
```csharp
|
|
[McpServerTool, Description("Tool that samples via MRTR")]
|
|
public static string SampleWithMrtr(
|
|
McpServer server,
|
|
RequestContext<CallToolRequestParams> context)
|
|
{
|
|
// On retry, process the client's sampling response
|
|
if (context.Params!.InputResponses?.TryGetValue("llm_call", out var response) is true)
|
|
{
|
|
var text = response.Deserialize(InputResponse.CreateMessageResultJsonTypeInfo)?.Content
|
|
.OfType<TextContentBlock>().FirstOrDefault()?.Text;
|
|
return $"LLM said: {text}";
|
|
}
|
|
|
|
if (!server.IsMrtrSupported)
|
|
{
|
|
return "This tool requires MRTR support (2026-07-28, or a stateful session using protocol revision 2025-11-25).";
|
|
}
|
|
|
|
// First call — request LLM completion from the client
|
|
throw new InputRequiredException(
|
|
inputRequests: new Dictionary<string, InputRequest>
|
|
{
|
|
["llm_call"] = InputRequest.ForSampling(new CreateMessageRequestParams
|
|
{
|
|
Messages =
|
|
[
|
|
new SamplingMessage
|
|
{
|
|
Role = Role.User,
|
|
Content = [new TextContentBlock { Text = "Summarize the data" }]
|
|
}
|
|
],
|
|
MaxTokens = 256
|
|
})
|
|
},
|
|
requestState: "awaiting-sample");
|
|
}
|
|
```
|
|
|
|
> [!TIP]
|
|
> For the full protocol details, including load shedding, multiple round trips, and the compatibility matrix, see [Multi Round-Trip Requests (MRTR)](xref:mrtr).
|