Multi Round-Trip Requests (MRTR) (#1458)
This commit is contained in:
parent
8202bcc932
commit
f6dbe4372f
6 changed files with 5832 additions and 103 deletions
|
|
@ -170,6 +170,61 @@ Here's an example implementation of how a console application might handle elici
|
|||
|
||||
[!code-csharp[](samples/client/Program.cs?name=snippet_ElicitationHandler)]
|
||||
|
||||
### Multi Round-Trip Requests (MRTR)
|
||||
|
||||
[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `DRAFT-2026-v1`. Under the draft protocol, the server-to-client `elicitation/create` request method is removed; the recommended way to ask the user for input 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]
|
||||
> `ElicitAsync` throws `InvalidOperationException("Elicitation is not supported in stateless mode.")` whenever the server is running stateless — which includes every Streamable HTTP server under `DRAFT-2026-v1` once that revision is forced to stateless-only in a future PR. Stdio servers and current-protocol stateful Streamable HTTP servers continue to work via the legacy server-to-client `elicitation/create` request flow. For code that needs to run on stateless servers — including all `DRAFT-2026-v1` Streamable HTTP servers going forward — throw `InputRequiredException` from your handler instead. It works under both protocols and both session modes.
|
||||
|
||||
For example:
|
||||
|
||||
```csharp
|
||||
[McpServerTool, Description("Tool that elicits via MRTR")]
|
||||
public static string ElicitWithMrtr(
|
||||
McpServer server,
|
||||
RequestContext<CallToolRequestParams> context)
|
||||
{
|
||||
// On retry, process the client's elicitation response
|
||||
if (context.Params!.InputResponses?.TryGetValue("user_input", out var response) is true)
|
||||
{
|
||||
var elicitResult = response.Deserialize(InputResponse.ElicitResultJsonTypeInfo);
|
||||
return elicitResult?.Action == "accept"
|
||||
? $"User accepted: {elicitResult.Content?.FirstOrDefault().Value}"
|
||||
: "User declined.";
|
||||
}
|
||||
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "This tool requires MRTR support (DRAFT-2026-v1, or a stateful current-protocol session).";
|
||||
}
|
||||
|
||||
// First call — request user input
|
||||
throw new InputRequiredException(
|
||||
inputRequests: new Dictionary<string, InputRequest>
|
||||
{
|
||||
["user_input"] = InputRequest.ForElicitation(new ElicitRequestParams
|
||||
{
|
||||
Message = "Please confirm the action",
|
||||
RequestedSchema = new()
|
||||
{
|
||||
Properties = new Dictionary<string, ElicitRequestParams.PrimitiveSchemaDefinition>
|
||||
{
|
||||
["confirm"] = new ElicitRequestParams.BooleanSchema
|
||||
{
|
||||
Description = "Confirm the action"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
requestState: "awaiting-confirmation");
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> See [Multi Round-Trip Requests (MRTR)](xref:mrtr) for the full protocol details, including multiple round trips, concurrent input requests, and the compatibility matrix.
|
||||
|
||||
### URL Elicitation Required Error
|
||||
|
||||
When a tool cannot proceed without first completing a URL-mode elicitation (for example, when third-party OAuth authorization is needed), and calling `ElicitAsync` is not practical (for example in [stateless](xref:stateless) mode where server-to-client requests are disabled), the server may throw a <xref:ModelContextProtocol.UrlElicitationRequiredException>. This is a specialized error (JSON-RPC error code `-32042`) that signals to the client that one or more URL-mode elicitations must be completed before the original request can be retried.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ Install the SDK and build your first MCP client and server.
|
|||
| [Progress tracking](progress/progress.md) | Learn how to track progress for long-running operations through notification messages. |
|
||||
| [Cancellation](cancellation/cancellation.md) | Learn how to cancel in-flight MCP requests using cancellation tokens and notifications. |
|
||||
| [Tasks](tasks/tasks.md) | Learn how to use task-based execution for long-running operations that can be polled for status and results. |
|
||||
| [Multi Round-Trip Requests (MRTR)](mrtr/mrtr.md) | Learn how servers request client input during tool execution using input-required results and retries. |
|
||||
|
||||
### Client Features
|
||||
|
||||
|
|
|
|||
291
docs/concepts/mrtr/mrtr.md
Normal file
291
docs/concepts/mrtr/mrtr.md
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
---
|
||||
title: Multi Round-Trip Requests (MRTR)
|
||||
author: halter73
|
||||
description: How servers request client input during tool execution using Multi Round-Trip Requests.
|
||||
uid: mrtr
|
||||
---
|
||||
|
||||
# Multi Round-Trip Requests (MRTR)
|
||||
|
||||
<!-- mlc-disable-next-line -->
|
||||
> [!WARNING]
|
||||
> MRTR is part of the **`DRAFT-2026-v1`** revision of the MCP specification ([SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)). The wire format and API surface may change before the revision is ratified. See the [Experimental APIs](../../experimental.md) documentation for details on working with experimental APIs.
|
||||
|
||||
Multi Round-Trip Requests (MRTR) let a server tool request input from the client — such as [elicitation](xref:elicitation), [sampling](xref:sampling), or [roots](xref:roots) — as part of a single tool call, without requiring a separate server-to-client JSON-RPC request for each interaction. Instead of returning a final result, the server returns an **incomplete result** containing one or more input requests. The client fulfills those requests and retries the original tool call with the responses attached.
|
||||
|
||||
## Overview
|
||||
|
||||
MRTR is useful when:
|
||||
|
||||
- A tool needs user confirmation before proceeding (elicitation).
|
||||
- A tool needs LLM reasoning from the client (sampling).
|
||||
- A tool needs an updated list of client roots.
|
||||
- A tool needs to perform multiple rounds of interaction in a single logical operation.
|
||||
- A stateless server needs to orchestrate multi-step flows without keeping handler state in memory between rounds.
|
||||
|
||||
## How MRTR works
|
||||
|
||||
1. The client calls a tool on the server via `tools/call`.
|
||||
2. The server tool determines it needs client input and returns an `InputRequiredResult` containing `inputRequests` and/or `requestState`.
|
||||
3. The client resolves each input request (for example by prompting the user for elicitation, calling an LLM for sampling, or listing its roots).
|
||||
4. The client retries the original `tools/call` with `inputResponses` (keyed to the input requests) and `requestState` echoed back.
|
||||
5. The server processes the responses and either returns a final result or another `InputRequiredResult` for additional rounds.
|
||||
|
||||
## Opting in
|
||||
|
||||
MRTR activates when both peers negotiate protocol revision **`DRAFT-2026-v1`** during `initialize`. The C# SDK opts in by listing `DRAFT-2026-v1` as a supported protocol version on the client; servers automatically accept it when offered. No experimental flags are required.
|
||||
|
||||
```csharp
|
||||
// Client
|
||||
var clientOptions = new McpClientOptions
|
||||
{
|
||||
ProtocolVersion = "DRAFT-2026-v1",
|
||||
Handlers = new McpClientHandlers
|
||||
{
|
||||
ElicitationHandler = HandleElicitationAsync,
|
||||
SamplingHandler = HandleSamplingAsync,
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Under `DRAFT-2026-v1`, MRTR is the recommended way to obtain client input from a server handler. The spec removes the legacy server-to-client `elicitation/create`, `sampling/createMessage`, and `roots/list` request methods, so any code that needs to work on a `DRAFT-2026-v1` Streamable HTTP server (which will be stateless-only in a future revision) must use `InputRequiredException` rather than <xref:ModelContextProtocol.Server.McpServer.ElicitAsync*>, <xref:ModelContextProtocol.Server.McpServer.SampleAsync*>, or <xref:ModelContextProtocol.Server.McpServer.RequestRootsAsync*>. The legacy methods still work on stateful sessions — that's how stdio servers keep working under draft today — but they throw `InvalidOperationException("X is not supported in stateless mode.")` on any stateless session, current or draft.
|
||||
|
||||
Under the current protocol revision (`2025-06-18` and earlier), `InputRequiredException` is still supported in stateful sessions via a backward-compatibility resolver — see [Compatibility](#compatibility) below.
|
||||
|
||||
## Authoring an MRTR tool
|
||||
|
||||
A tool participates in MRTR by throwing <xref:ModelContextProtocol.Protocol.InputRequiredException> with an <xref:ModelContextProtocol.Protocol.InputRequiredResult> describing what it needs. On retry, the client's responses arrive on the request parameters and the tool inspects them to decide what to do next.
|
||||
|
||||
### Checking MRTR support
|
||||
|
||||
Tools should check <xref:ModelContextProtocol.Server.McpServer.IsMrtrSupported> before throwing `InputRequiredException`. It returns `true` when either:
|
||||
|
||||
- The negotiated protocol revision is `DRAFT-2026-v1` (MRTR is native), or
|
||||
- The session is stateful under the current protocol (the SDK can resolve input requests via legacy JSON-RPC and retry the handler).
|
||||
|
||||
```csharp
|
||||
[McpServerTool, Description("A tool that uses MRTR")]
|
||||
public static string MyTool(
|
||||
McpServer server,
|
||||
RequestContext<CallToolRequestParams> context)
|
||||
{
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "This tool requires a client that negotiates DRAFT-2026-v1, "
|
||||
+ "or a stateful current-protocol session.";
|
||||
}
|
||||
|
||||
// ... MRTR logic
|
||||
}
|
||||
```
|
||||
|
||||
### Returning an incomplete result
|
||||
|
||||
Throw <xref:ModelContextProtocol.Protocol.InputRequiredException> to return an incomplete result. The exception carries an <xref:ModelContextProtocol.Protocol.InputRequiredResult> containing `inputRequests` and/or `requestState`:
|
||||
|
||||
```csharp
|
||||
[McpServerTool, Description("Tool managing its own MRTR flow")]
|
||||
public static string AnswerTool(
|
||||
McpServer server,
|
||||
RequestContext<CallToolRequestParams> context,
|
||||
[Description("The user's question")] string question)
|
||||
{
|
||||
var requestState = context.Params!.RequestState;
|
||||
var inputResponses = context.Params!.InputResponses;
|
||||
|
||||
// On retry, process the client's responses
|
||||
if (requestState is not null && inputResponses is not null)
|
||||
{
|
||||
var elicitResult = inputResponses["user_answer"].Deserialize(InputResponse.ElicitResultJsonTypeInfo);
|
||||
return $"You answered: {elicitResult?.Content?.FirstOrDefault().Value}";
|
||||
}
|
||||
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "MRTR is not supported by this client.";
|
||||
}
|
||||
|
||||
// First call — request user input
|
||||
throw new InputRequiredException(
|
||||
inputRequests: new Dictionary<string, InputRequest>
|
||||
{
|
||||
["user_answer"] = InputRequest.ForElicitation(new ElicitRequestParams
|
||||
{
|
||||
Message = $"Please answer: {question}",
|
||||
RequestedSchema = new()
|
||||
{
|
||||
Properties = new Dictionary<string, ElicitRequestParams.PrimitiveSchemaDefinition>
|
||||
{
|
||||
["answer"] = new ElicitRequestParams.StringSchema
|
||||
{
|
||||
Description = "Your answer"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
requestState: "awaiting-answer");
|
||||
}
|
||||
```
|
||||
|
||||
### Accessing retry data
|
||||
|
||||
When the client retries a tool call, the retry data is available on the request parameters:
|
||||
|
||||
- <xref:ModelContextProtocol.Protocol.RequestParams.InputResponses> — a dictionary of client responses keyed by the same keys used in `inputRequests`.
|
||||
- <xref:ModelContextProtocol.Protocol.RequestParams.RequestState> — the opaque state string echoed back by the client.
|
||||
|
||||
Use <xref:ModelContextProtocol.Protocol.InputResponse.Deserialize*> with the `JsonTypeInfo<T>` matching the response type. The expected type follows from the matching <xref:ModelContextProtocol.Protocol.InputRequest.Method> in the original `inputRequests` map — there is no on-the-wire discriminator.
|
||||
|
||||
- Elicitation — `response.Deserialize(InputResponse.ElicitResultJsonTypeInfo)`
|
||||
- Sampling — `response.Deserialize(InputResponse.CreateMessageResultJsonTypeInfo)`
|
||||
- Roots list — `response.Deserialize(InputResponse.ListRootsResultJsonTypeInfo)`
|
||||
|
||||
### Load shedding with requestState-only responses
|
||||
|
||||
A server can return a `requestState`-only incomplete result (without any `inputRequests`) to defer processing. This is useful for load shedding or breaking up long-running work across multiple requests:
|
||||
|
||||
```csharp
|
||||
[McpServerTool, Description("Tool that defers work using requestState")]
|
||||
public static string DeferredTool(
|
||||
McpServer server,
|
||||
RequestContext<CallToolRequestParams> context)
|
||||
{
|
||||
var requestState = context.Params!.RequestState;
|
||||
|
||||
if (requestState is not null)
|
||||
{
|
||||
// Resume deferred work
|
||||
var state = JsonSerializer.Deserialize<MyState>(
|
||||
Convert.FromBase64String(requestState));
|
||||
return $"Completed step {state!.Step}";
|
||||
}
|
||||
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "MRTR is not supported by this client.";
|
||||
}
|
||||
|
||||
// Defer work to a later retry
|
||||
var initialState = new MyState { Step = 1 };
|
||||
throw new InputRequiredException(
|
||||
requestState: Convert.ToBase64String(
|
||||
JsonSerializer.SerializeToUtf8Bytes(initialState)));
|
||||
}
|
||||
```
|
||||
|
||||
The client automatically retries `requestState`-only incomplete results, echoing the state back without needing to resolve any input requests.
|
||||
|
||||
### Multiple round trips
|
||||
|
||||
A tool can perform multiple rounds of interaction by throwing `InputRequiredException` multiple times across retries. Use `requestState` to track which round you're on:
|
||||
|
||||
```csharp
|
||||
[McpServerTool, Description("Multi-step wizard")]
|
||||
public static string WizardTool(
|
||||
McpServer server,
|
||||
RequestContext<CallToolRequestParams> context)
|
||||
{
|
||||
var requestState = context.Params!.RequestState;
|
||||
var inputResponses = context.Params!.InputResponses;
|
||||
|
||||
if (requestState == "step-2" && inputResponses is not null)
|
||||
{
|
||||
var name = inputResponses["name"].Deserialize(InputResponse.ElicitResultJsonTypeInfo)?.Content?.FirstOrDefault().Value;
|
||||
var age = inputResponses["age"].Deserialize(InputResponse.ElicitResultJsonTypeInfo)?.Content?.FirstOrDefault().Value;
|
||||
return $"Welcome, {name}! You are {age} years old.";
|
||||
}
|
||||
|
||||
if (requestState == "step-1" && inputResponses is not null)
|
||||
{
|
||||
var name = inputResponses["name"].Deserialize(InputResponse.ElicitResultJsonTypeInfo)?.Content?.FirstOrDefault().Value;
|
||||
|
||||
// Second round — ask for age
|
||||
throw new InputRequiredException(
|
||||
inputRequests: new Dictionary<string, InputRequest>
|
||||
{
|
||||
["age"] = InputRequest.ForElicitation(new ElicitRequestParams
|
||||
{
|
||||
Message = $"Hi {name}! How old are you?",
|
||||
RequestedSchema = new()
|
||||
{
|
||||
Properties = new Dictionary<string, ElicitRequestParams.PrimitiveSchemaDefinition>
|
||||
{
|
||||
["age"] = new ElicitRequestParams.NumberSchema
|
||||
{
|
||||
Description = "Your age"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
requestState: "step-2");
|
||||
}
|
||||
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "MRTR is not supported. Please use a compatible client.";
|
||||
}
|
||||
|
||||
// First round — ask for name
|
||||
throw new InputRequiredException(
|
||||
inputRequests: new Dictionary<string, InputRequest>
|
||||
{
|
||||
["name"] = InputRequest.ForElicitation(new ElicitRequestParams
|
||||
{
|
||||
Message = "What's your name?",
|
||||
RequestedSchema = new()
|
||||
{
|
||||
Properties = new Dictionary<string, ElicitRequestParams.PrimitiveSchemaDefinition>
|
||||
{
|
||||
["name"] = new ElicitRequestParams.StringSchema
|
||||
{
|
||||
Description = "Your name"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
requestState: "step-1");
|
||||
}
|
||||
```
|
||||
|
||||
### Providing custom error messages
|
||||
|
||||
When MRTR is not supported, you can provide domain-specific guidance:
|
||||
|
||||
```csharp
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "This tool requires interactive input. To use it:\n"
|
||||
+ "1. Connect with a client that negotiates MCP protocol revision DRAFT-2026-v1, or\n"
|
||||
+ "2. Use a stateful current-protocol session so the server can resolve the input requests for you.\n"
|
||||
+ "\nStateless current-protocol sessions cannot resolve MRTR input requests.";
|
||||
}
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
The SDK supports `InputRequiredException` across two protocol revisions and two session modes:
|
||||
|
||||
| Negotiated protocol | Session mode | Behavior |
|
||||
|---|---|---|
|
||||
| `DRAFT-2026-v1` | Stateful | Native MRTR — `InputRequiredResult` is serialized directly to the wire. |
|
||||
| `DRAFT-2026-v1` | Stateless | Native MRTR — `InputRequiredResult` is serialized directly to the wire. No server-side handler state needed. |
|
||||
| Current (`2025-06-18` and earlier) | Stateful | Backward-compatibility resolver — the SDK sends standard `elicitation/create` / `sampling/createMessage` / `roots/list` JSON-RPC requests to the client, collects the responses, and retries the handler with `inputResponses` populated. Up to 10 retry rounds. |
|
||||
| Current (`2025-06-18` and earlier) | Stateless | **Not supported** — `InputRequiredException` raises an `McpException`. The client doesn't speak MRTR, and the server can't resolve input requests via JSON-RPC without a persistent session. |
|
||||
|
||||
> [!NOTE]
|
||||
> The backcompat resolver is intentionally limited to 10 retry rounds. Tools that need more rounds should require `DRAFT-2026-v1` (check `IsMrtrSupported`).
|
||||
|
||||
### Why `ElicitAsync` / `SampleAsync` / `RequestRootsAsync` throw on stateless servers
|
||||
|
||||
`ElicitAsync` / `SampleAsync` / `RequestRootsAsync` issue a JSON-RPC request to the client and wait for the response on the same session. Stateless servers don't have a persistent session to wait on, so the SDK fails fast with `InvalidOperationException("X is not supported in stateless mode.")` (the check is `McpServer.ClientCapabilities is null`, which is the SDK's proxy for stateless).
|
||||
|
||||
Under the current protocol revision (`2025-06-18` and earlier), stdio and stateful Streamable HTTP keep `ClientCapabilities` populated, so the legacy methods work normally and remain the recommended way to do one-shot client interactions. Under `DRAFT-2026-v1`, the spec removes those request methods from Streamable HTTP entirely; the SDK still allows the legacy methods on draft stdio sessions because stdio is implicitly single-process / stateful and the client handler is wired up regardless of negotiated revision. `InputRequiredException` is the way to write tools that work on every supported configuration.
|
||||
|
||||
### Future direction
|
||||
|
||||
The `DRAFT-2026-v1` revision is moving toward a stateless-only model: `Mcp-Session-Id` is being removed, and Streamable HTTP servers will run statelessly by default under the draft revision. When that lands, the `Stateful` row for `DRAFT-2026-v1` in the compatibility matrix above collapses into the `Stateless` row (Streamable HTTP under draft becomes stateless-only), and `InputRequiredException` becomes uniformly required for non-stdio servers. The current-protocol resolver path will remain for backward compatibility with older clients and stateful servers.
|
||||
|
||||
This work is a follow-up to the present PR.
|
||||
|
|
@ -103,3 +103,43 @@ server.RegisterNotificationHandler(
|
|||
Console.WriteLine($"Roots updated. {result.Roots.Count} roots available.");
|
||||
});
|
||||
```
|
||||
|
||||
### Multi Round-Trip Requests (MRTR)
|
||||
|
||||
[MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `DRAFT-2026-v1`. Under the draft protocol, the server-to-client `roots/list` request method is removed; the recommended way to ask the client for its roots 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]
|
||||
> `RequestRootsAsync` throws `InvalidOperationException("Roots are not supported in stateless mode.")` whenever the server is running stateless — which includes every Streamable HTTP server under `DRAFT-2026-v1` once that revision is forced to stateless-only in a future PR. Stdio servers and current-protocol stateful Streamable HTTP servers continue to work via the legacy server-to-client `roots/list` request flow. For code that needs to run on stateless servers — including all `DRAFT-2026-v1` Streamable HTTP servers going forward — throw `InputRequiredException` from your handler instead. It works under both protocols and both session modes.
|
||||
|
||||
For example:
|
||||
|
||||
```csharp
|
||||
[McpServerTool, Description("Tool that requests roots via MRTR")]
|
||||
public static string ListRootsWithMrtr(
|
||||
McpServer server,
|
||||
RequestContext<CallToolRequestParams> context)
|
||||
{
|
||||
// On retry, process the client's roots response
|
||||
if (context.Params!.InputResponses?.TryGetValue("get_roots", out var response) is true)
|
||||
{
|
||||
var roots = response.Deserialize(InputResponse.ListRootsResultJsonTypeInfo)?.Roots ?? [];
|
||||
return $"Found {roots.Count} roots: {string.Join(", ", roots.Select(r => r.Uri))}";
|
||||
}
|
||||
|
||||
if (!server.IsMrtrSupported)
|
||||
{
|
||||
return "This tool requires MRTR support (DRAFT-2026-v1, or a stateful current-protocol session).";
|
||||
}
|
||||
|
||||
// First call — request the client's root list
|
||||
throw new InputRequiredException(
|
||||
inputRequests: new Dictionary<string, InputRequest>
|
||||
{
|
||||
["get_roots"] = InputRequest.ForRootsList(new ListRootsRequestParams())
|
||||
},
|
||||
requestState: "awaiting-roots");
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> See [Multi Round-Trip Requests (MRTR)](xref:mrtr) for the full protocol details, including load shedding, multiple round trips, and the compatibility matrix.
|
||||
|
|
|
|||
|
|
@ -120,3 +120,55 @@ McpClientOptions options = new()
|
|||
### 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 `DRAFT-2026-v1`. Under the draft protocol, 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 — which includes every Streamable HTTP server under `DRAFT-2026-v1` once that revision is forced to stateless-only in a future PR. Stdio servers and current-protocol stateful Streamable HTTP servers continue to work via the legacy server-to-client `sampling/createMessage` request flow. For code that needs to run on stateless servers — including all `DRAFT-2026-v1` Streamable HTTP servers going forward — throw `InputRequiredException` from your handler instead. It works under both protocols and both session modes.
|
||||
|
||||
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 (DRAFT-2026-v1, or a stateful current-protocol session).";
|
||||
}
|
||||
|
||||
// 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]
|
||||
> See [Multi Round-Trip Requests (MRTR)](xref:mrtr) for the full protocol details, including load shedding, multiple round trips, and the compatibility matrix.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ items:
|
|||
uid: cancellation
|
||||
- name: Tasks
|
||||
uid: tasks
|
||||
- name: Multi Round-Trip Requests (MRTR)
|
||||
uid: mrtr
|
||||
- name: Client Features
|
||||
items:
|
||||
- name: Sampling
|
||||
|
|
|
|||
Loading…
Reference in a new issue