| title | Multi Round-Trip Requests (MRTR) |
|---|---|
| author | halter73 |
| description | How servers request client input during tool execution using Multi Round-Trip Requests. |
| uid | mrtr |
Warning
MRTR is an experimental feature based on a draft MCP specification proposal. The API may change in future releases. See the Experimental APIs documentation for details on working with experimental APIs. Both the client and server must opt in via xref:ModelContextProtocol.Client.McpClientOptions.ExperimentalProtocolVersion and xref:ModelContextProtocol.Server.McpServerOptions.ExperimentalProtocolVersion respectively.
Multi Round-Trip Requests (MRTR) allow a server tool to request input from the client — such as elicitation, sampling, or roots — as part of a single tool call, without requiring a separate JSON-RPC request for each interaction. Instead of sending 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.
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
- The client calls a tool on the server via
tools/call. - The server tool determines it needs client input and returns an
IncompleteResultcontaininginputRequestsand/orrequestState. - The client resolves each input request (e.g., prompts the user for elicitation, calls an LLM for sampling).
- The client retries the original
tools/callwithinputResponses(keyed to the input requests) andrequestStateechoed back. - The server processes the responses and either returns a final result or another
IncompleteResultfor additional rounds.
MRTR requires both the client and server to opt in by setting ExperimentalProtocolVersion to a draft protocol version. Currently, this is "2026-06-XX":
// Server
var builder = Host.CreateApplicationBuilder();
builder.Services.AddMcpServer(options =>
{
options.ExperimentalProtocolVersion = "2026-06-XX";
})
.WithTools<MyTools>();// Client
var options = new McpClientOptions
{
ExperimentalProtocolVersion = "2026-06-XX",
Handlers = new McpClientHandlers
{
ElicitationHandler = HandleElicitationAsync,
SamplingHandler = HandleSamplingAsync,
}
};When both sides opt in, the negotiated protocol version activates MRTR. When either side does not opt in, the SDK gracefully falls back to standard behavior.
The high-level API lets tool handlers call xref:ModelContextProtocol.Server.McpServer.ElicitAsync* and xref:ModelContextProtocol.Server.McpServer.SampleAsync* as if they were simple async calls. The SDK transparently manages the incomplete result / retry cycle.
[McpServerToolType]
public class InteractiveTools
{
[McpServerTool, Description("Asks the user for confirmation before proceeding")]
public static async Task<string> ConfirmAction(
McpServer server,
[Description("The action to confirm")] string action,
CancellationToken cancellationToken)
{
var result = await server.ElicitAsync(new ElicitRequestParams
{
Message = $"Do you want to proceed with: {action}?",
RequestedSchema = new()
{
Properties = new Dictionary<string, ElicitRequestParams.PrimitiveSchemaDefinition>
{
["confirm"] = new ElicitRequestParams.BooleanSchema
{
Description = "Confirm the action"
}
}
}
}, cancellationToken);
return result.Action == "accept" ? "Action confirmed!" : "Action cancelled.";
}
}From the client's perspective, this is a single CallToolAsync call. The SDK handles all retries automatically:
var result = await client.CallToolAsync("ConfirmAction", new { action = "delete all files" });
Console.WriteLine(result.Content.OfType<TextContentBlock>().First().Text);Tip
The high-level API requires session affinity — the handler task stays suspended in server memory between round trips. This works well for stateful (non-stateless) server configurations.
The low-level API gives tool handlers direct control over inputRequests and requestState. This enables stateless multi-round-trip flows where the server does not need to keep handler state in memory between retries.
Before using the low-level API, check xref:ModelContextProtocol.Server.McpServer.IsMrtrSupported to determine if the connected client supports MRTR. If it does not, provide a fallback experience:
[McpServerTool, Description("A tool that uses low-level MRTR")]
public static string MyTool(
McpServer server,
RequestContext<CallToolRequestParams> context)
{
if (!server.IsMrtrSupported)
{
return "This tool requires a client that supports multi-round-trip requests. "
+ "Please upgrade your client or enable experimental protocol support.";
}
// ... MRTR logic
}Throw xref:ModelContextProtocol.Protocol.IncompleteResultException to return an incomplete result to the client. The exception carries an xref:ModelContextProtocol.Protocol.IncompleteResult containing inputRequests and/or requestState:
[McpServerTool, Description("Stateless tool managing its own MRTR flow")]
public static string StatelessTool(
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"].ElicitationResult;
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 IncompleteResultException(
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");
}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
Each InputResponse has typed accessors for the response type:
ElicitationResult— the result of an elicitation requestSamplingResult— the result of a sampling requestRootsResult— the result of a roots list request
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:
[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 IncompleteResultException(
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.
A tool can perform multiple rounds of interaction by throwing IncompleteResultException multiple times across retries:
[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"].ElicitationResult?.Content?.FirstOrDefault().Value;
var age = inputResponses["age"].ElicitationResult?.Content?.FirstOrDefault().Value;
return $"Welcome, {name}! You are {age} years old.";
}
if (requestState == "step-1" && inputResponses is not null)
{
var name = inputResponses["name"].ElicitationResult?.Content?.FirstOrDefault().Value;
// Second round — ask for age
throw new IncompleteResultException(
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 IncompleteResultException(
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");
}When MRTR is not supported, you can provide domain-specific guidance:
if (!server.IsMrtrSupported)
{
return "This tool requires interactive input, but your client doesn't support "
+ "multi-round-trip requests. To use this feature:\n"
+ "1. Update to a client that supports MCP protocol version 2026-06-XX or later\n"
+ "2. Enable the experimental protocol version in your client configuration\n"
+ "\nFor more information, see: https://example.com/mrtr-setup";
}The SDK handles all four combinations of experimental/non-experimental client and server:
| Server Experimental | Client Experimental | Behavior |
|---|---|---|
| ✅ | ✅ | MRTR — incomplete results with retry cycle |
| ✅ | ❌ | Server falls back to legacy JSON-RPC requests for elicitation/sampling |
| ❌ | ✅ | Client accepts stable protocol version; MRTR retry loop is a no-op |
| ❌ | ❌ | Standard behavior — no MRTR |
When a server has MRTR enabled but the connected client does not:
- The high-level API (
ElicitAsync,SampleAsync) automatically falls back to sending standard JSON-RPC requests — no code changes needed. - The low-level API reports
IsMrtrSupported == false, allowing the tool to provide a custom fallback message.
Tools written with the low-level MRTR pattern (IncompleteResultException) work automatically with clients that don't support MRTR. When a tool throws IncompleteResultException and the client hasn't negotiated MRTR, the SDK resolves each InputRequest by sending the corresponding standard JSON-RPC call (elicitation, sampling, or roots) to the client, then retries the handler with the resolved responses.
This means you can write a single tool implementation using the MRTR-native pattern and it will work with any client:
[McpServerTool, Description("Get weather with user's preferred units")]
public static string GetWeather(
RequestContext<CallToolRequestParams> context,
string location)
{
// On retry, inputResponses and requestState are populated
if (context.Params!.InputResponses?.TryGetValue("units", out var response) == true)
{
var units = response.ElicitationResult?.Content?.FirstOrDefault().Value;
return $"Weather for {location} in {units}: 72°";
}
// First call: request the user's preferred units
throw new IncompleteResultException(
inputRequests: new Dictionary<string, InputRequest>
{
["units"] = InputRequest.ForElicitation(new ElicitRequestParams
{
Message = "Which temperature units?",
RequestedSchema = new()
})
},
requestState: "awaiting-units");
}- With an MRTR client: The
IncompleteResultis sent over the wire. The client resolves the elicitation and retries withinputResponses. - Without MRTR: The SDK sends a standard
elicitation/createJSON-RPC request to the client, collects the response, and retries the handler internally. The client never sees theIncompleteResult.
Note
The backcompat retry loop resolves up to 10 rounds. Tools that need more rounds should use the high-level API (ElicitAsync) instead.
Warning
Deferred task creation depends on both the MRTR and Tasks experimental features.
Some tools need user input before they can decide whether to start a long-running background task. For example, a VM provisioning tool might confirm costs with the user before committing to a task that takes minutes. Deferred task creation lets a tool perform ephemeral MRTR exchanges first, then transition to a background task only when ready.
- The tool sets
DeferTaskCreation = trueon its attribute or options. - When the client sends task metadata with the
tools/callrequest, the SDK runs the tool through the normal MRTR-wrapped path instead of creating a task immediately. - The tool calls
ElicitAsyncorSampleAsyncas usual — these use MRTR (incomplete result / retry cycles). - When the tool is ready, it calls
await server.CreateTaskAsync(cancellationToken)to transition to a background task. - After
CreateTaskAsync, the MRTR phase ends. Any subsequentElicitAsyncorSampleAsynccalls use the task's owninput_required/tasks/input_responsemechanism instead. - If the tool returns without calling
CreateTaskAsync, a normal (non-task) result is sent to the client.
McpServerTool.Create(
async (string vmName, McpServer server, CancellationToken ct) =>
{
// Phase 1: Ephemeral MRTR — confirm with user before starting expensive work.
var confirmation = await server.ElicitAsync(new ElicitRequestParams
{
Message = $"Provision VM '{vmName}'? This will incur costs.",
RequestedSchema = new()
}, ct);
if (confirmation.Action != "confirm")
{
return "Cancelled by user.";
}
// Phase 2: Transition to a background task.
await server.CreateTaskAsync(ct);
// Phase 3: Background work — runs as a task, client polls for status.
await Task.Delay(TimeSpan.FromMinutes(5), ct);
return $"VM '{vmName}' provisioned successfully.";
},
new McpServerToolCreateOptions
{
Name = "provision-vm",
Description = "Provisions a VM with user confirmation",
DeferTaskCreation = true,
Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional },
})The attribute-based equivalent uses DeferTaskCreation on xref:ModelContextProtocol.Server.McpServerToolAttribute:
[McpServerTool(DeferTaskCreation = true, TaskSupport = ToolTaskSupport.Optional)]
[Description("Provisions a VM with user confirmation")]
public static async Task<string> ProvisionVm(
string vmName, McpServer server, CancellationToken ct)
{
var confirmation = await server.ElicitAsync(new ElicitRequestParams
{
Message = $"Provision VM '{vmName}'? This will incur costs.",
RequestedSchema = new()
}, ct);
if (confirmation.Action != "confirm")
return "Cancelled by user.";
await server.CreateTaskAsync(ct);
await Task.Delay(TimeSpan.FromMinutes(5), ct);
return $"VM '{vmName}' provisioned successfully.";
}- One-way transition: Once
CreateTaskAsyncis called, the tool cannot go back to ephemeral MRTR. All subsequent input requests use the task workflow. - Optional task creation: A
DeferTaskCreationtool can return a normal result without ever callingCreateTaskAsync. The tool decides at runtime whether to create a task. - No task metadata, no deferral: If the client calls the tool without task metadata, the tool runs normally with MRTR —
DeferTaskCreationhas no effect.
For more details on task configuration and lifecycle, see the Tasks documentation.
| Consideration | High-level API | Low-level API |
|---|---|---|
| Session affinity | Required — handler stays suspended in memory | Not required — handler completes each round |
| State management | Automatic (SDK manages via MrtrContext) |
Manual (requestState encoded by you) |
| Complexity | Simple await calls |
More code, but full control |
| Stateless servers | Not compatible | Designed for stateless scenarios |
| Fallback | Automatic — SDK sends legacy requests | Manual — check IsMrtrSupported |
| Multiple input types | One at a time (elicit or sample) | Multiple in a single round |