Multi Round-Trip Requests (MRTR) (#1458)
This commit is contained in:
parent
8202bcc932
commit
f6dbe4372f
47 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
|
||||
|
|
|
|||
|
|
@ -110,4 +110,23 @@ internal static class Experimentals
|
|||
/// URL for the experimental <c>RunSessionHandler</c> API.
|
||||
/// </summary>
|
||||
public const string RunSessionHandler_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp002";
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic ID for the experimental Multi Round-Trip Requests (MRTR) feature.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This uses the same diagnostic ID as <see cref="Tasks_DiagnosticId"/> because MRTR
|
||||
/// is an experimental feature in the MCP specification (SEP-2322).
|
||||
/// </remarks>
|
||||
public const string Mrtr_DiagnosticId = "MCPEXP001";
|
||||
|
||||
/// <summary>
|
||||
/// Message for the experimental MRTR feature.
|
||||
/// </summary>
|
||||
public const string Mrtr_Message = "The Multi Round-Trip Requests (MRTR) feature is experimental per the MCP specification (SEP-2322) and is subject to change.";
|
||||
|
||||
/// <summary>
|
||||
/// URL for the experimental MRTR feature.
|
||||
/// </summary>
|
||||
public const string Mrtr_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<Description>ASP.NET Core extensions for the C# Model Context Protocol (MCP) SDK.</Description>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
<NoWarn>$(NoWarn);MCPEXP001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -508,12 +508,25 @@ internal sealed class StreamableHttpHandler(
|
|||
// Implementation for reading a JSON-RPC message from the request body
|
||||
var message = await context.Request.ReadFromJsonAsync(s_messageTypeInfo, context.RequestAborted);
|
||||
|
||||
if (context.User?.Identity?.IsAuthenticated == true && message is not null)
|
||||
if (message is not null)
|
||||
{
|
||||
message.Context = new()
|
||||
var protocolVersion = context.Request.Headers[McpProtocolVersionHeaderName].ToString();
|
||||
var isAuthenticated = context.User?.Identity?.IsAuthenticated == true;
|
||||
|
||||
if (isAuthenticated || !string.IsNullOrEmpty(protocolVersion))
|
||||
{
|
||||
User = context.User,
|
||||
};
|
||||
message.Context ??= new();
|
||||
|
||||
if (isAuthenticated)
|
||||
{
|
||||
message.Context.User = context.User;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(protocolVersion))
|
||||
{
|
||||
message.Context.ProtocolVersion = protocolVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
|
|
@ -812,7 +825,7 @@ internal sealed class StreamableHttpHandler(
|
|||
// JSON Schema defines two numeric types: "number" (any numeric value including
|
||||
// decimals like 3.14) and "integer" (whole numbers only like 42). Both produce
|
||||
// JsonValueKind.Number in the JSON body and are sent as numeric strings in headers.
|
||||
// We check for both because different SDKs may serialize them differently —
|
||||
// We check for both because different SDKs may serialize them differently -
|
||||
// e.g., a client might send header "42.0" for an "integer" body value of 42,
|
||||
// or header "42" for a "number" body value of 42.0. Without handling both types,
|
||||
// valid cross-SDK requests would be incorrectly rejected.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
|||
using ModelContextProtocol.Protocol;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
|
|
@ -142,6 +143,8 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
RequestMethods.SamplingCreateMessage,
|
||||
async (request, jsonRpcRequest, cancellationToken) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.SamplingCreateMessage);
|
||||
|
||||
// Check if this is a task-augmented request
|
||||
if (request?.Task is { } taskMetadata)
|
||||
{
|
||||
|
|
@ -176,10 +179,14 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
{
|
||||
requestHandlers.Set(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
(request, _, cancellationToken) => samplingHandler(
|
||||
request,
|
||||
request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
|
||||
cancellationToken),
|
||||
(request, _, cancellationToken) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.SamplingCreateMessage);
|
||||
return samplingHandler(
|
||||
request,
|
||||
request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
|
||||
cancellationToken);
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
|
||||
}
|
||||
|
|
@ -192,7 +199,11 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
{
|
||||
requestHandlers.Set(
|
||||
RequestMethods.RootsList,
|
||||
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
|
||||
(request, _, cancellationToken) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.RootsList);
|
||||
return rootsHandler(request, cancellationToken);
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
||||
|
||||
|
|
@ -209,6 +220,8 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
RequestMethods.ElicitationCreate,
|
||||
async (request, jsonRpcRequest, cancellationToken) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.ElicitationCreate);
|
||||
|
||||
// Check if this is a task-augmented request
|
||||
if (request?.Task is { } taskMetadata)
|
||||
{
|
||||
|
|
@ -241,6 +254,7 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
RequestMethods.ElicitationCreate,
|
||||
async (request, _, cancellationToken) =>
|
||||
{
|
||||
WarnIfLegacyRequestOnMrtrSession(RequestMethods.ElicitationCreate);
|
||||
var result = await elicitationHandler(request, cancellationToken).ConfigureAwait(false);
|
||||
return ElicitResult.WithDefaults(request, result);
|
||||
},
|
||||
|
|
@ -547,6 +561,98 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
/// <inheritdoc/>
|
||||
public override Task<ClientCompletionDetails> Completion => _sessionHandler.CompletionTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
private async ValueTask<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(
|
||||
IDictionary<string, InputRequest> inputRequests,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Resolve all input requests concurrently. If any fails, cancel the rest so user-facing
|
||||
// handlers (sampling/elicitation prompts) don't keep running for a request whose caller
|
||||
// has already given up, and ensure exceptions from late-completing tasks are observed.
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
var keyed = new (string Key, Task<InputResponse> Task)[inputRequests.Count];
|
||||
int i = 0;
|
||||
foreach (var kvp in inputRequests)
|
||||
{
|
||||
keyed[i++] = (kvp.Key, ResolveInputRequestAsync(kvp.Value, linkedCts.Token));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(Array.ConvertAll(keyed, k => k.Task)).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
linkedCts.Cancel();
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(Array.ConvertAll(keyed, k => k.Task)).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Observed; the original exception is the one we want to surface.
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
var responses = new Dictionary<string, InputResponse>(keyed.Length);
|
||||
foreach (var (key, task) in keyed)
|
||||
{
|
||||
responses[key] = task.Result;
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
private async Task<InputResponse> ResolveInputRequestAsync(InputRequest inputRequest, CancellationToken cancellationToken)
|
||||
{
|
||||
switch (inputRequest.Method)
|
||||
{
|
||||
case RequestMethods.SamplingCreateMessage:
|
||||
if (_options.Handlers.SamplingHandler is { } samplingHandler)
|
||||
{
|
||||
var samplingParams = inputRequest.SamplingParams
|
||||
?? throw new McpException($"Failed to deserialize sampling parameters from MRTR input request.");
|
||||
var result = await samplingHandler(
|
||||
samplingParams,
|
||||
samplingParams.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
return InputResponse.FromSamplingResult(result);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Server sent a sampling input request, but no {nameof(McpClientHandlers.SamplingHandler)} is registered.");
|
||||
|
||||
case RequestMethods.ElicitationCreate:
|
||||
if (_options.Handlers.ElicitationHandler is { } elicitationHandler)
|
||||
{
|
||||
var elicitParams = inputRequest.ElicitationParams
|
||||
?? throw new McpException($"Failed to deserialize elicitation parameters from MRTR input request.");
|
||||
var result = await elicitationHandler(elicitParams, cancellationToken).ConfigureAwait(false);
|
||||
result = ElicitResult.WithDefaults(elicitParams, result);
|
||||
return InputResponse.FromElicitResult(result);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Server sent an elicitation input request, but no {nameof(McpClientHandlers.ElicitationHandler)} is registered.");
|
||||
|
||||
case RequestMethods.RootsList:
|
||||
if (_options.Handlers.RootsHandler is { } rootsHandler)
|
||||
{
|
||||
// ListRootsRequest params are optional per the spec, so fall back to an empty params instance.
|
||||
var rootsParams = inputRequest.RootsParams ?? new ListRootsRequestParams();
|
||||
var result = await rootsHandler(rootsParams, cancellationToken).ConfigureAwait(false);
|
||||
return InputResponse.FromRootsResult(result);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Server sent a roots list input request, but no {nameof(McpClientHandlers.RootsHandler)} is registered.");
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported input request method: '{inputRequest.Method}'.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously connects to an MCP server, establishes the transport connection, and completes the initialization handshake.
|
||||
/// </summary>
|
||||
|
|
@ -718,13 +824,13 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
||||
public override async Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// For tools/call requests, attach the cached tool definition to the message context
|
||||
// so the transport can add custom Mcp-Param-* headers based on x-mcp-header schema annotations.
|
||||
if (request.Method == RequestMethods.ToolsCall &&
|
||||
request.Params is System.Text.Json.Nodes.JsonObject paramsObj &&
|
||||
paramsObj.TryGetPropertyValue("name", out var nameNode) &&
|
||||
request.Params is System.Text.Json.Nodes.JsonObject paramsObjForHeaders &&
|
||||
paramsObjForHeaders.TryGetPropertyValue("name", out var nameNode) &&
|
||||
nameNode?.GetValue<string>() is { } toolName)
|
||||
{
|
||||
if (_toolCache.TryGetValue(toolName, out var tool))
|
||||
|
|
@ -739,7 +845,67 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
}
|
||||
}
|
||||
|
||||
return _sessionHandler.SendRequestAsync(request, cancellationToken);
|
||||
const int maxRetries = 10;
|
||||
|
||||
for (int attempt = 0; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
JsonRpcResponse response = await _sessionHandler.SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check if the result is an InputRequiredResult by looking at result_type.
|
||||
if (response.Result is JsonObject resultObj &&
|
||||
resultObj.TryGetPropertyValue("resultType", out var resultTypeNode) &&
|
||||
resultTypeNode?.GetValue<string>() is "input_required")
|
||||
{
|
||||
WarnIfInputRequiredResultOnNonMrtrSession(request.Method);
|
||||
|
||||
var inputRequiredResult = JsonSerializer.Deserialize(response.Result, McpJsonUtilities.JsonContext.Default.InputRequiredResult)
|
||||
?? throw new JsonException("Failed to deserialize InputRequiredResult.");
|
||||
|
||||
if (inputRequiredResult.InputRequests is { Count: > 0 } inputRequests)
|
||||
{
|
||||
IDictionary<string, InputResponse> inputResponses =
|
||||
await ResolveInputRequestsAsync(inputRequests, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Clone the original request params and add inputResponses + requestState for the retry.
|
||||
var paramsObj = request.Params?.DeepClone() as JsonObject ?? new JsonObject();
|
||||
|
||||
paramsObj["inputResponses"] = JsonSerializer.SerializeToNode(
|
||||
inputResponses, McpJsonUtilities.JsonContext.Default.IDictionaryStringInputResponse);
|
||||
|
||||
if (inputRequiredResult.RequestState is { } requestState)
|
||||
{
|
||||
paramsObj["requestState"] = requestState;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Strip any stale requestState carried over from the previous round's clone so
|
||||
// the server doesn't see a continuation token the current round is not using.
|
||||
paramsObj.Remove("requestState");
|
||||
}
|
||||
|
||||
request = new JsonRpcRequest { Method = request.Method, Params = paramsObj, Context = request.Context };
|
||||
}
|
||||
else if (inputRequiredResult.RequestState is not null)
|
||||
{
|
||||
// No input requests but has requestState (e.g., load shedding) - just retry with state.
|
||||
var paramsObj = request.Params?.DeepClone() as JsonObject ?? new JsonObject();
|
||||
paramsObj["requestState"] = inputRequiredResult.RequestState;
|
||||
paramsObj.Remove("inputResponses");
|
||||
|
||||
request = new JsonRpcRequest { Method = request.Method, Params = paramsObj, Context = request.Context };
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new McpException("Server returned an InputRequiredResult without inputRequests or requestState.");
|
||||
}
|
||||
|
||||
continue; // retry with the updated request
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
throw new McpException($"Server returned InputRequiredResult more than {maxRetries} times.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -775,6 +941,30 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
await Completion.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Logs a warning if the session negotiated MRTR but the server sent a legacy JSON-RPC request.</summary>
|
||||
private void WarnIfLegacyRequestOnMrtrSession(string method)
|
||||
{
|
||||
if (_negotiatedProtocolVersion == McpSessionHandler.DraftProtocolVersion)
|
||||
{
|
||||
LogLegacyRequestOnMrtrSession(_endpointName, method);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Logs a warning if the session did not negotiate MRTR but the server sent an InputRequiredResult.</summary>
|
||||
private void WarnIfInputRequiredResultOnNonMrtrSession(string method)
|
||||
{
|
||||
if (_negotiatedProtocolVersion != McpSessionHandler.DraftProtocolVersion)
|
||||
{
|
||||
LogInputRequiredResultOnNonMrtrSession(_endpointName, method, _negotiatedProtocolVersion);
|
||||
}
|
||||
}
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} received legacy '{Method}' JSON-RPC request on session that negotiated MRTR. The server should use InputRequiredResult instead of sending direct requests.")]
|
||||
private partial void LogLegacyRequestOnMrtrSession(string endpointName, string method);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} received InputRequiredResult for '{Method}' on session that did not negotiate MRTR (protocol version '{ProtocolVersion}'). The server may not be spec-compliant.")]
|
||||
private partial void LogInputRequiredResultOnNonMrtrSession(string endpointName, string method, string? protocolVersion);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client received server '{ServerInfo}' capabilities: '{Capabilities}'.")]
|
||||
private partial void LogServerCapabilitiesReceived(string endpointName, string capabilities, string serverInfo);
|
||||
|
||||
|
|
@ -798,5 +988,4 @@ internal sealed partial class McpClientImpl : McpClient
|
|||
|
||||
[LoggerMessage(Level = LogLevel.Warning, Message = "Tool '{ToolName}' excluded from tools/list: {Reason}")]
|
||||
private partial void LogToolRejected(string toolName, string reason);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Authentication;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
|
@ -144,6 +144,13 @@ public static partial class McpJsonUtilities
|
|||
[JsonSerializable(typeof(SubscribeRequestParams))]
|
||||
[JsonSerializable(typeof(UnsubscribeRequestParams))]
|
||||
|
||||
// MCP MRTR (Multi Round-Trip Requests)
|
||||
[JsonSerializable(typeof(InputRequiredResult))]
|
||||
[JsonSerializable(typeof(InputRequest))]
|
||||
[JsonSerializable(typeof(InputResponse))]
|
||||
[JsonSerializable(typeof(IDictionary<string, InputRequest>))]
|
||||
[JsonSerializable(typeof(IDictionary<string, InputResponse>))]
|
||||
|
||||
// MCP Task Request Params / Results
|
||||
[JsonSerializable(typeof(McpTask))]
|
||||
[JsonSerializable(typeof(McpTaskStatus))]
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ public abstract partial class McpSession : IAsyncDisposable
|
|||
/// </param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task that represents the asynchronous send operation.</returns>
|
||||
/// <exception cref="InvalidOperationException">The transport is not connected.</exception>
|
||||
/// <exception cref="InvalidOperationException">The transport is not connected, or <paramref name="message"/> is a <see cref="JsonRpcRequest"/>. Use <see cref="SendRequestAsync"/> for requests.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="message"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
/// <summary>The latest version of the protocol supported by this implementation.</summary>
|
||||
internal const string LatestProtocolVersion = "2025-11-25";
|
||||
|
||||
/// <summary>
|
||||
/// The draft protocol version that enables MRTR (Multi Round-Trip Requests) per SEP-2322.
|
||||
/// Clients and servers opt in by setting <see cref="McpClientOptions.ProtocolVersion"/>
|
||||
/// or <see cref="McpServerOptions.ProtocolVersion"/> to this value.
|
||||
/// </summary>
|
||||
internal const string DraftProtocolVersion = "DRAFT-2026-v1";
|
||||
|
||||
/// <summary>
|
||||
/// All protocol versions supported by this implementation.
|
||||
/// Keep in sync with s_supportedProtocolVersions in StreamableHttpHandler.
|
||||
|
|
@ -41,7 +48,7 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
"2025-03-26",
|
||||
"2025-06-18",
|
||||
LatestProtocolVersion,
|
||||
"DRAFT-2026-v1",
|
||||
DraftProtocolVersion,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -642,6 +649,13 @@ internal sealed partial class McpSessionHandler : IAsyncDisposable
|
|||
{
|
||||
Throw.IfNull(message);
|
||||
|
||||
if (message is JsonRpcRequest request)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot send '{request.Method}' request via {nameof(SendMessageAsync)}. " +
|
||||
$"Use {nameof(SendRequestAsync)} instead to get a correlated response.");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Histogram<double> durationMetric = _isServer ? s_serverOperationDuration : s_clientOperationDuration;
|
||||
|
|
|
|||
197
src/ModelContextProtocol.Core/Protocol/InputRequest.cs
Normal file
197
src/ModelContextProtocol.Core/Protocol/InputRequest.cs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a server-initiated request that the client must fulfill as part of an MRTR
|
||||
/// (Multi Round-Trip Request) flow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// An <see cref="InputRequest"/> wraps a server-to-client request such as
|
||||
/// <see cref="RequestMethods.SamplingCreateMessage"/>, <see cref="RequestMethods.ElicitationCreate"/>,
|
||||
/// or <see cref="RequestMethods.RootsList"/>. It is included in an <see cref="InputRequiredResult"/>
|
||||
/// when the server needs additional input before it can complete a client-initiated request.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <see cref="Method"/> property identifies the type of request, and the corresponding
|
||||
/// parameters can be accessed via the typed accessor properties.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Mrtr_DiagnosticId, UrlFormat = Experimentals.Mrtr_Url)]
|
||||
[JsonConverter(typeof(Converter))]
|
||||
public sealed class InputRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the method name identifying the type of this input request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Standard values include:
|
||||
/// <list type="bullet">
|
||||
/// <item><term><see cref="RequestMethods.SamplingCreateMessage"/></term><description>A sampling request.</description></item>
|
||||
/// <item><term><see cref="RequestMethods.ElicitationCreate"/></term><description>An elicitation request.</description></item>
|
||||
/// <item><term><see cref="RequestMethods.RootsList"/></term><description>A roots list request.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
[JsonPropertyName("method")]
|
||||
public required string Method { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the raw JSON parameters for this input request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use the typed accessor properties (<see cref="SamplingParams"/>, <see cref="ElicitationParams"/>,
|
||||
/// <see cref="RootsParams"/>) for convenient strongly-typed access.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("params")]
|
||||
public JsonElement? Params { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters as <see cref="CreateMessageRequestParams"/> when <see cref="Method"/>
|
||||
/// is <see cref="RequestMethods.SamplingCreateMessage"/>.
|
||||
/// </summary>
|
||||
/// <returns>The deserialized sampling parameters, or <see langword="null"/> if the method does not match or params are absent.</returns>
|
||||
[JsonIgnore]
|
||||
public CreateMessageRequestParams? SamplingParams =>
|
||||
string.Equals(Method, RequestMethods.SamplingCreateMessage, StringComparison.Ordinal) && Params is { } p
|
||||
? JsonSerializer.Deserialize(p, McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams)
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters as <see cref="ElicitRequestParams"/> when <see cref="Method"/>
|
||||
/// is <see cref="RequestMethods.ElicitationCreate"/>.
|
||||
/// </summary>
|
||||
/// <returns>The deserialized elicitation parameters, or <see langword="null"/> if the method does not match or params are absent.</returns>
|
||||
[JsonIgnore]
|
||||
public ElicitRequestParams? ElicitationParams =>
|
||||
string.Equals(Method, RequestMethods.ElicitationCreate, StringComparison.Ordinal) && Params is { } p
|
||||
? JsonSerializer.Deserialize(p, McpJsonUtilities.JsonContext.Default.ElicitRequestParams)
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameters as <see cref="ListRootsRequestParams"/> when <see cref="Method"/>
|
||||
/// is <see cref="RequestMethods.RootsList"/>.
|
||||
/// </summary>
|
||||
/// <returns>The deserialized roots list parameters, or <see langword="null"/> if the method does not match or params are absent.</returns>
|
||||
[JsonIgnore]
|
||||
public ListRootsRequestParams? RootsParams =>
|
||||
string.Equals(Method, RequestMethods.RootsList, StringComparison.Ordinal) && Params is { } p
|
||||
? JsonSerializer.Deserialize(p, McpJsonUtilities.JsonContext.Default.ListRootsRequestParams)
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="InputRequest"/> for a sampling request.
|
||||
/// </summary>
|
||||
/// <param name="requestParams">The sampling request parameters.</param>
|
||||
/// <returns>A new <see cref="InputRequest"/> instance.</returns>
|
||||
public static InputRequest ForSampling(CreateMessageRequestParams requestParams)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
return new()
|
||||
{
|
||||
Method = RequestMethods.SamplingCreateMessage,
|
||||
Params = JsonSerializer.SerializeToElement(requestParams, McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="InputRequest"/> for an elicitation request.
|
||||
/// </summary>
|
||||
/// <param name="requestParams">The elicitation request parameters.</param>
|
||||
/// <returns>A new <see cref="InputRequest"/> instance.</returns>
|
||||
public static InputRequest ForElicitation(ElicitRequestParams requestParams)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
return new()
|
||||
{
|
||||
Method = RequestMethods.ElicitationCreate,
|
||||
Params = JsonSerializer.SerializeToElement(requestParams, McpJsonUtilities.JsonContext.Default.ElicitRequestParams),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="InputRequest"/> for a roots list request.
|
||||
/// </summary>
|
||||
/// <param name="requestParams">The roots list request parameters.</param>
|
||||
/// <returns>A new <see cref="InputRequest"/> instance.</returns>
|
||||
public static InputRequest ForRootsList(ListRootsRequestParams requestParams)
|
||||
{
|
||||
Throw.IfNull(requestParams);
|
||||
return new()
|
||||
{
|
||||
Method = RequestMethods.RootsList,
|
||||
Params = JsonSerializer.SerializeToElement(requestParams, McpJsonUtilities.JsonContext.Default.ListRootsRequestParams),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Provides JSON serialization support for <see cref="InputRequest"/>.</summary>
|
||||
public sealed class Converter : JsonConverter<InputRequest>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override InputRequest? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
throw new JsonException("Expected StartObject token.");
|
||||
}
|
||||
|
||||
string? method = null;
|
||||
JsonElement? parameters = null;
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
{
|
||||
throw new JsonException("Expected PropertyName token.");
|
||||
}
|
||||
|
||||
string propertyName = reader.GetString()!;
|
||||
reader.Read();
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case "method":
|
||||
method = reader.GetString();
|
||||
break;
|
||||
case "params":
|
||||
parameters = JsonElement.ParseValue(ref reader);
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (method is null)
|
||||
{
|
||||
throw new JsonException("InputRequest must have a 'method' property.");
|
||||
}
|
||||
|
||||
return new InputRequest
|
||||
{
|
||||
Method = method,
|
||||
Params = parameters,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, InputRequest value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("method", value.Method);
|
||||
if (value.Params is { } p)
|
||||
{
|
||||
writer.WritePropertyName("params");
|
||||
p.WriteTo(writer);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
109
src/ModelContextProtocol.Core/Protocol/InputRequiredException.cs
Normal file
109
src/ModelContextProtocol.Core/Protocol/InputRequiredException.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// The exception that is thrown by a server handler to return an <see cref="Protocol.InputRequiredResult"/>
|
||||
/// to the client, signaling that additional input is needed before the request can be completed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This exception is part of the Multi Round-Trip Requests (MRTR) API. Tool handlers
|
||||
/// throw this exception to directly control the input-required result payload, including
|
||||
/// <see cref="Protocol.InputRequiredResult.InputRequests"/> and <see cref="Protocol.InputRequiredResult.RequestState"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For stateless servers, this enables multi-round-trip flows without requiring the handler to stay
|
||||
/// alive between round trips. The server encodes its state in <see cref="Protocol.InputRequiredResult.RequestState"/>
|
||||
/// and receives it back on retry via <see cref="RequestParams.RequestState"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// To return a <c>requestState</c>-only response (e.g., for load shedding), omit
|
||||
/// <see cref="Protocol.InputRequiredResult.InputRequests"/> and set only <see cref="Protocol.InputRequiredResult.RequestState"/>.
|
||||
/// The client will retry the request with the state echoed back.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This exception can only be used when MRTR is supported by the client. Check
|
||||
/// <see cref="Server.McpServer.IsMrtrSupported"/> before throwing. If thrown when MRTR is not
|
||||
/// supported, the exception will propagate as a JSON-RPC internal error.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// [McpServerTool, Description("A stateless tool using MRTR")]
|
||||
/// public static string MyTool(McpServer server, RequestContext<CallToolRequestParams> context)
|
||||
/// {
|
||||
/// if (context.Params.RequestState is { } state)
|
||||
/// {
|
||||
/// // Retry: process accumulated state and input responses
|
||||
/// var responses = context.Params.InputResponses;
|
||||
/// return "Final result";
|
||||
/// }
|
||||
///
|
||||
/// if (!server.IsMrtrSupported)
|
||||
/// {
|
||||
/// return "This tool requires MRTR support.";
|
||||
/// }
|
||||
///
|
||||
/// throw new InputRequiredException(
|
||||
/// inputRequests: new Dictionary<string, InputRequest>
|
||||
/// {
|
||||
/// ["user_input"] = InputRequest.ForElicitation(new ElicitRequestParams { ... })
|
||||
/// },
|
||||
/// requestState: "encoded-state");
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
[Experimental(Experimentals.Mrtr_DiagnosticId, UrlFormat = Experimentals.Mrtr_Url)]
|
||||
public class InputRequiredException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputRequiredException"/> class
|
||||
/// with the specified <see cref="Protocol.InputRequiredResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The input-required result to return to the client.</param>
|
||||
public InputRequiredException(InputRequiredResult result)
|
||||
: base("The server returned an input-required result requiring additional client input.")
|
||||
{
|
||||
Throw.IfNull(result);
|
||||
Result = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputRequiredException"/> class
|
||||
/// with the specified input requests and/or request state.
|
||||
/// </summary>
|
||||
/// <param name="inputRequests">
|
||||
/// Server-initiated requests that the client must fulfill before retrying.
|
||||
/// Keys are server-assigned identifiers.
|
||||
/// </param>
|
||||
/// <param name="requestState">
|
||||
/// Opaque state to be echoed back by the client when retrying. The client must
|
||||
/// treat this as an opaque blob and must not inspect or modify it.
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Both <paramref name="inputRequests"/> and <paramref name="requestState"/> are <see langword="null"/>.
|
||||
/// At least one must be provided.
|
||||
/// </exception>
|
||||
public InputRequiredException(
|
||||
IDictionary<string, InputRequest>? inputRequests = null,
|
||||
string? requestState = null)
|
||||
: base("The server returned an input-required result requiring additional client input.")
|
||||
{
|
||||
if (inputRequests is null && requestState is null)
|
||||
{
|
||||
throw new ArgumentException("At least one of inputRequests or requestState must be provided.");
|
||||
}
|
||||
|
||||
Result = new InputRequiredResult
|
||||
{
|
||||
InputRequests = inputRequests,
|
||||
RequestState = requestState,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input-required result to return to the client.
|
||||
/// </summary>
|
||||
public InputRequiredResult Result { get; }
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an input-required result sent by the server to indicate that additional input is needed
|
||||
/// before the request can be completed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// An <see cref="InputRequiredResult"/> is returned in response to a client-initiated request when
|
||||
/// the server needs the client to fulfill one or more server-initiated requests before it can produce
|
||||
/// a final result. Per SEP-2322 the wire format is valid for <see cref="RequestMethods.ToolsCall"/>,
|
||||
/// <see cref="RequestMethods.PromptsGet"/>, and <c>resources/read</c>; this SDK wires the MRTR
|
||||
/// interceptor into all three methods.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// At least one of <see cref="InputRequests"/> or <see cref="RequestState"/> must be present.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This type is part of the Multi Round-Trip Requests (MRTR) mechanism defined in SEP-2322.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Mrtr_DiagnosticId, UrlFormat = Experimentals.Mrtr_Url)]
|
||||
public sealed class InputRequiredResult : Result
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InputRequiredResult"/> class.
|
||||
/// </summary>
|
||||
public InputRequiredResult()
|
||||
{
|
||||
ResultType = "input_required";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server-initiated requests that the client must fulfill before retrying the original request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The keys are server-assigned identifiers. The client must include a response for each key in the
|
||||
/// <see cref="RequestParams.InputResponses"/> map when retrying the original request.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonPropertyName("inputRequests")]
|
||||
public IDictionary<string, InputRequest>? InputRequests { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets opaque state to be echoed back by the client when retrying the original request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The client must treat this as an opaque blob and must not inspect, parse, modify, or make
|
||||
/// any assumptions about the contents. If present, the client must include this value in the
|
||||
/// <see cref="RequestParams.RequestState"/> property when retrying the original request.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Servers may encode request state in any format (e.g., plain JSON, base64-encoded JSON,
|
||||
/// encrypted JWT, serialized binary). If the state contains sensitive data, servers should
|
||||
/// encrypt it to ensure confidentiality and integrity.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonPropertyName("requestState")]
|
||||
public string? RequestState { get; set; }
|
||||
}
|
||||
128
src/ModelContextProtocol.Core/Protocol/InputResponse.cs
Normal file
128
src/ModelContextProtocol.Core/Protocol/InputResponse.cs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a client's response to a server-initiated <see cref="InputRequest"/> as part of an MRTR
|
||||
/// (Multi Round-Trip Request) flow.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// An <see cref="InputResponse"/> wraps the result of a server-to-client request such as
|
||||
/// <see cref="CreateMessageResult"/>, <see cref="ElicitResult"/>, or <see cref="ListRootsResult"/>.
|
||||
/// The type of the inner response corresponds to the <see cref="InputRequest.Method"/> of the
|
||||
/// associated input request.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The input response does not carry its own type discriminator in JSON. The type is determined by
|
||||
/// the corresponding <see cref="InputRequest.Method"/> key in the <see cref="InputRequiredResult.InputRequests"/> map.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Mrtr_DiagnosticId, UrlFormat = Experimentals.Mrtr_Url)]
|
||||
[JsonConverter(typeof(Converter))]
|
||||
public sealed class InputResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the raw JSON element representing the response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use <see cref="Deserialize{T}"/> with the <c>JsonTypeInfo<T></c> matching the
|
||||
/// associated <see cref="InputRequest.Method"/> - for elicitation, sampling, or roots see
|
||||
/// <see cref="ElicitResultJsonTypeInfo"/>, <see cref="CreateMessageResultJsonTypeInfo"/>, and
|
||||
/// <see cref="ListRootsResultJsonTypeInfo"/>.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public JsonElement RawValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the raw value to the specified result type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize to (e.g., <see cref="CreateMessageResult"/>, <see cref="ElicitResult"/>).</typeparam>
|
||||
/// <param name="typeInfo">The JSON type information for <typeparamref name="T"/>.</param>
|
||||
/// <returns>The deserialized result, or <see langword="null"/> if deserialization fails.</returns>
|
||||
public T? Deserialize<T>(System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> typeInfo) =>
|
||||
JsonSerializer.Deserialize(RawValue, typeInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="JsonTypeInfo{T}"/> for <see cref="ElicitResult"/>, suitable for use with
|
||||
/// <see cref="Deserialize{T}"/> when the corresponding <see cref="InputRequest.Method"/> is
|
||||
/// <see cref="RequestMethods.ElicitationCreate"/>.
|
||||
/// </summary>
|
||||
public static JsonTypeInfo<ElicitResult> ElicitResultJsonTypeInfo => McpJsonUtilities.JsonContext.Default.ElicitResult;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="JsonTypeInfo{T}"/> for <see cref="CreateMessageResult"/>, suitable for use with
|
||||
/// <see cref="Deserialize{T}"/> when the corresponding <see cref="InputRequest.Method"/> is
|
||||
/// <see cref="RequestMethods.SamplingCreateMessage"/>.
|
||||
/// </summary>
|
||||
public static JsonTypeInfo<CreateMessageResult> CreateMessageResultJsonTypeInfo => McpJsonUtilities.JsonContext.Default.CreateMessageResult;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="JsonTypeInfo{T}"/> for <see cref="ListRootsResult"/>, suitable for use with
|
||||
/// <see cref="Deserialize{T}"/> when the corresponding <see cref="InputRequest.Method"/> is
|
||||
/// <see cref="RequestMethods.RootsList"/>.
|
||||
/// </summary>
|
||||
public static JsonTypeInfo<ListRootsResult> ListRootsResultJsonTypeInfo => McpJsonUtilities.JsonContext.Default.ListRootsResult;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="InputResponse"/> from a <see cref="CreateMessageResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The sampling result.</param>
|
||||
/// <returns>A new <see cref="InputResponse"/> instance.</returns>
|
||||
public static InputResponse FromSamplingResult(CreateMessageResult result)
|
||||
{
|
||||
Throw.IfNull(result);
|
||||
return new()
|
||||
{
|
||||
RawValue = JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CreateMessageResult),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="InputResponse"/> from an <see cref="ElicitResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The elicitation result.</param>
|
||||
/// <returns>A new <see cref="InputResponse"/> instance.</returns>
|
||||
public static InputResponse FromElicitResult(ElicitResult result)
|
||||
{
|
||||
Throw.IfNull(result);
|
||||
return new()
|
||||
{
|
||||
RawValue = JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.ElicitResult),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="InputResponse"/> from a <see cref="ListRootsResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The roots list result.</param>
|
||||
/// <returns>A new <see cref="InputResponse"/> instance.</returns>
|
||||
public static InputResponse FromRootsResult(ListRootsResult result)
|
||||
{
|
||||
Throw.IfNull(result);
|
||||
return new()
|
||||
{
|
||||
RawValue = JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.ListRootsResult),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Provides JSON serialization support for <see cref="InputResponse"/>.</summary>
|
||||
public sealed class Converter : JsonConverter<InputResponse>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override InputResponse? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var element = JsonElement.ParseValue(ref reader);
|
||||
return new InputResponse { RawValue = element };
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, InputResponse value, JsonSerializerOptions options)
|
||||
{
|
||||
value.RawValue.WriteTo(writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -74,4 +74,15 @@ public sealed class JsonRpcMessageContext
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
public IDictionary<string, object?>? Items { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the protocol version from the transport-level header (e.g. <c>Mcp-Protocol-Version</c>)
|
||||
/// that accompanied this JSON-RPC message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In stateless Streamable HTTP mode, the protocol version cannot be negotiated via the <c>initialize</c>
|
||||
/// handshake because each request creates a new server instance. This property allows the transport layer
|
||||
/// to flow the protocol version header so the server can determine client capabilities.
|
||||
/// </remarks>
|
||||
public string? ProtocolVersion { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
|
@ -25,6 +26,52 @@ public abstract class RequestParams
|
|||
[JsonPropertyName("_meta")]
|
||||
public JsonObject? Meta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the responses to server-initiated input requests from a previous <see cref="InputRequiredResult"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property is populated when retrying a request after receiving an <see cref="InputRequiredResult"/>.
|
||||
/// Each key corresponds to a key from the <see cref="InputRequiredResult.InputRequests"/> map, and
|
||||
/// the value is the client's response to that input request.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Mrtr_DiagnosticId, UrlFormat = Experimentals.Mrtr_Url)]
|
||||
[JsonIgnore]
|
||||
public IDictionary<string, InputResponse>? InputResponses
|
||||
{
|
||||
get => InputResponsesCore;
|
||||
set => InputResponsesCore = value;
|
||||
}
|
||||
|
||||
// See ExperimentalInternalPropertyTests.cs before modifying this property.
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("inputResponses")]
|
||||
internal IDictionary<string, InputResponse>? InputResponsesCore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets opaque request state echoed back from a previous <see cref="InputRequiredResult"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property is populated when retrying a request after receiving an <see cref="InputRequiredResult"/>
|
||||
/// that included a <see cref="InputRequiredResult.RequestState"/> value. The client must echo back the
|
||||
/// exact value without modification.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Experimental(Experimentals.Mrtr_DiagnosticId, UrlFormat = Experimentals.Mrtr_Url)]
|
||||
[JsonIgnore]
|
||||
public string? RequestState
|
||||
{
|
||||
get => RequestStateCore;
|
||||
set => RequestStateCore = value;
|
||||
}
|
||||
|
||||
// See ExperimentalInternalPropertyTests.cs before modifying this property.
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("requestState")]
|
||||
internal string? RequestStateCore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the opaque token that will be attached to any subsequent progress notifications.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -21,4 +21,18 @@ public abstract class Result
|
|||
/// </remarks>
|
||||
[JsonPropertyName("_meta")]
|
||||
public JsonObject? Meta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the result, which allows the client to determine how to parse the result object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When absent or set to <c>"complete"</c>, the result is a normal completed response.
|
||||
/// When set to <c>"input_required"</c>, the result is an <see cref="InputRequiredResult"/> indicating
|
||||
/// that additional input is needed before the request can be completed.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <value>Defaults to <see langword="null"/>, which is equivalent to <c>"complete"</c>.</value>
|
||||
[JsonPropertyName("resultType")]
|
||||
public string? ResultType { get; set; }
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue