Skip to content

Commit 137f3fe

Browse files
jeffhandleyCopilot
andcommitted
Add sampling docs, tools error handling, and SSE server example
- New docs/concepts/sampling/sampling.md covering server-side SampleAsync and AsSamplingChatClient, client-side CreateSamplingHandler and custom SamplingHandler delegate, and capability negotiation - Add error handling section to docs/concepts/tools/tools.md with examples for automatic exception handling, McpProtocolException, and client-side IsError checking - Add SSE server code example to docs/concepts/transports/transports.md - Add sampling link to docs/concepts/index.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a807ba8 commit 137f3fe

File tree

4 files changed

+198
-1
lines changed

4 files changed

+198
-1
lines changed

docs/concepts/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Welcome to the conceptual documentation for the Model Context Protocol SDK. Here
1919

2020
| Title | Description |
2121
| - | - |
22+
| [Sampling](sampling/sampling.md) | Learn how servers request LLM completions from the client using the sampling feature. |
2223
| [Roots](roots/roots.md) | Learn how clients provide filesystem roots to servers for context-aware operations. |
2324
| [Elicitation](elicitation/elicitation.md) | Learn how to request additional information from users during interactions. |
2425

docs/concepts/sampling/sampling.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
title: Sampling
3+
author: jeffhandley
4+
description: How servers request LLM completions from the client using the sampling feature.
5+
uid: sampling
6+
---
7+
8+
## Sampling
9+
10+
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.
11+
12+
[sampling]: https://modelcontextprotocol.io/specification/2025-11-25/client/sampling
13+
14+
### How sampling works
15+
16+
1. The server calls <xref:ModelContextProtocol.Server.McpServer.SampleAsync*> (or uses the <xref:ModelContextProtocol.Server.McpServer.AsSamplingChatClient*> adapter) during tool execution.
17+
2. The request is sent to the connected client over MCP.
18+
3. The client's <xref:ModelContextProtocol.Client.McpClientHandlers.SamplingHandler> processes the request — typically by forwarding it to an LLM.
19+
4. The client returns the LLM response to the server, which continues tool execution.
20+
21+
### Server: requesting a completion
22+
23+
Inject <xref:ModelContextProtocol.Server.McpServer> into a tool method and use the <xref:ModelContextProtocol.Server.McpServer.AsSamplingChatClient*> extension method to get an <xref:Microsoft.Extensions.AI.IChatClient> that sends requests through the connected client:
24+
25+
```csharp
26+
[McpServerTool(Name = "SummarizeContent"), Description("Summarizes the given text")]
27+
public static async Task<string> Summarize(
28+
McpServer server,
29+
[Description("The text to summarize")] string text,
30+
CancellationToken cancellationToken)
31+
{
32+
ChatMessage[] messages =
33+
[
34+
new(ChatRole.User, "Briefly summarize the following content:"),
35+
new(ChatRole.User, text),
36+
];
37+
38+
ChatOptions options = new()
39+
{
40+
MaxOutputTokens = 256,
41+
Temperature = 0.3f,
42+
};
43+
44+
return $"Summary: {await server.AsSamplingChatClient().GetResponseAsync(messages, options, cancellationToken)}";
45+
}
46+
```
47+
48+
Alternatively, use <xref:ModelContextProtocol.Server.McpServer.SampleAsync*> directly for lower-level control:
49+
50+
```csharp
51+
CreateMessageResult result = await server.SampleAsync(
52+
new CreateMessageRequestParams
53+
{
54+
Messages =
55+
[
56+
new SamplingMessage
57+
{
58+
Role = Role.User,
59+
Content = [new TextContentBlock { Text = "What is 2 + 2?" }]
60+
}
61+
],
62+
MaxTokens = 100,
63+
},
64+
cancellationToken);
65+
66+
string response = result.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text ?? string.Empty;
67+
```
68+
69+
### Client: handling sampling requests
70+
71+
Set <xref:ModelContextProtocol.Client.McpClientHandlers.SamplingHandler> when creating the client. This handler is called when a server sends a `sampling/createMessage` request.
72+
73+
#### Using an IChatClient
74+
75+
The simplest approach is to use <xref:ModelContextProtocol.AIContentExtensions.CreateSamplingHandler*> with any <xref:Microsoft.Extensions.AI.IChatClient> implementation:
76+
77+
```csharp
78+
IChatClient chatClient = new OllamaChatClient(new Uri("http://localhost:11434"), "llama3");
79+
80+
McpClientOptions options = new()
81+
{
82+
Handlers = new()
83+
{
84+
SamplingHandler = chatClient.CreateSamplingHandler()
85+
}
86+
};
87+
88+
await using var client = await McpClient.CreateAsync(transport, options);
89+
```
90+
91+
#### Custom handler
92+
93+
For full control, provide a custom delegate:
94+
95+
```csharp
96+
McpClientOptions options = new()
97+
{
98+
Handlers = new()
99+
{
100+
SamplingHandler = async (request, progress, cancellationToken) =>
101+
{
102+
// Forward to your LLM, apply content filtering, etc.
103+
string prompt = request?.Messages?.LastOrDefault()?.Content switch
104+
{
105+
TextContentBlock text => text.Text,
106+
_ => string.Empty
107+
};
108+
109+
return new CreateMessageResult
110+
{
111+
Model = "my-model",
112+
Role = Role.Assistant,
113+
Content = [new TextContentBlock { Text = $"Response to: {prompt}" }]
114+
};
115+
}
116+
}
117+
};
118+
```
119+
120+
### Capability negotiation
121+
122+
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>.

docs/concepts/tools/tools.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,66 @@ foreach (var content in result.Content)
189189
}
190190
```
191191

192+
### Error handling
193+
194+
Tool errors in MCP are distinct from protocol errors. When a tool encounters an error during execution, the error is reported inside the <xref:ModelContextProtocol.Protocol.CallToolResult> with <xref:ModelContextProtocol.Protocol.CallToolResult.IsError> set to `true`, rather than as a protocol-level exception. This allows the LLM to see the error and potentially recover.
195+
196+
#### Automatic exception handling
197+
198+
When a tool method throws an exception, the server automatically catches it and returns a `CallToolResult` with `IsError = true`. <xref:ModelContextProtocol.McpProtocolException> is re-thrown as a JSON-RPC error, and <xref:System.OperationCanceledException> is re-thrown when the cancellation token was triggered. All other exceptions are returned as tool error results. For <xref:ModelContextProtocol.McpException> subclasses, the exception message is included in the error text; for other exceptions, a generic message is returned to avoid leaking internal details.
199+
200+
```csharp
201+
[McpServerTool, Description("Divides two numbers")]
202+
public static double Divide(double a, double b)
203+
{
204+
if (b == 0)
205+
{
206+
// ArgumentException is not an McpException, so the client receives a generic message:
207+
// "An error occurred invoking 'divide'."
208+
throw new ArgumentException("Cannot divide by zero");
209+
}
210+
211+
return a / b;
212+
}
213+
```
214+
215+
#### Protocol errors
216+
217+
Throw <xref:ModelContextProtocol.McpProtocolException> to signal a protocol-level error (e.g., invalid parameters or unknown tool). These exceptions propagate as JSON-RPC error responses rather than tool error results:
218+
219+
```csharp
220+
[McpServerTool, Description("Processes the input")]
221+
public static string Process(string input)
222+
{
223+
if (string.IsNullOrEmpty(input))
224+
{
225+
// Propagates as a JSON-RPC error with code -32602 (InvalidParams)
226+
// and message "Missing required input"
227+
throw new McpProtocolException("Missing required input", McpErrorCode.InvalidParams);
228+
}
229+
230+
return $"Processed: {input}";
231+
}
232+
```
233+
234+
#### Checking for errors on the client
235+
236+
On the client side, inspect the <xref:ModelContextProtocol.Protocol.CallToolResult.IsError> property after calling a tool:
237+
238+
```csharp
239+
CallToolResult result = await client.CallToolAsync("divide", new Dictionary<string, object?>
240+
{
241+
["a"] = 10,
242+
["b"] = 0
243+
});
244+
245+
if (result.IsError is true)
246+
{
247+
// Prints: "Tool error: An error occurred invoking 'divide'."
248+
Console.WriteLine($"Tool error: {result.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}");
249+
}
250+
```
251+
192252
### Tool list change notifications
193253

194254
Servers can dynamically add, remove, or modify tools at runtime. When the tool list changes, the server notifies connected clients so they can refresh their tool list.

docs/concepts/transports/transports.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,21 @@ SSE-specific configuration options:
160160

161161
#### SSE server (ASP.NET Core)
162162

163-
The ASP.NET Core integration supports SSE transport alongside Streamable HTTP. The same `MapMcp()` endpoint handles both protocols.
163+
The ASP.NET Core integration supports SSE transport alongside Streamable HTTP. The same `MapMcp()` endpoint handles both protocols — clients connecting with SSE are automatically served using the legacy SSE mechanism:
164+
165+
```csharp
166+
var builder = WebApplication.CreateBuilder(args);
167+
168+
builder.Services.AddMcpServer()
169+
.WithHttpTransport()
170+
.WithTools<MyTools>();
171+
172+
var app = builder.Build();
173+
app.MapMcp(); // Handles both Streamable HTTP and SSE clients
174+
app.Run();
175+
```
176+
177+
No additional configuration is needed. When a client connects using the SSE protocol, the server responds with an SSE stream for server-to-client messages and accepts client-to-server messages via a separate POST endpoint.
164178

165179
### Transport mode comparison
166180

0 commit comments

Comments
 (0)