Skip to content

Multi Round-Trip Requests (MRTR)#1458

Draft
halter73 wants to merge 8 commits into
mainfrom
halter73/mrtr
Draft

Multi Round-Trip Requests (MRTR)#1458
halter73 wants to merge 8 commits into
mainfrom
halter73/mrtr

Conversation

@halter73
Copy link
Copy Markdown
Contributor

@halter73 halter73 commented Mar 21, 2026

Summary

Implements SEP-2322: Multi Round-Trip Requests (MRTR) for the C# SDK.

MRTR lets a server tool ask the client for input — elicitation, sampling, or roots — as part of a single tool call by returning an incomplete result instead of a final one. The client resolves the input requests and retries the original tools/call with inputResponses attached, until the tool returns a final result.

This PR follows the ratified draft wire format and gates the new behavior on the negotiated protocol revision DRAFT-2026-v1. There are no experimental opt-in flags.

The API

InputRequiredException is the only way to do MRTR. A tool throws it with an InputRequiredResult containing inputRequests and/or requestState, and the SDK turns that into the right wire response for the negotiated protocol.

[McpServerTool]
public static string Ask(McpServer server, RequestContext<CallToolRequestParams> context, string question)
{
    if (context.Params!.InputResponses?["answer"].ElicitationResult is { } answered)
        return $"You said: {answered.Content?.FirstOrDefault().Value}";

    if (!server.IsMrtrSupported)
        return "MRTR is not supported by this client.";

    throw new InputRequiredException(
        inputRequests: new Dictionary<string, InputRequest>
        {
            ["answer"] = InputRequest.ForElicitation(new ElicitRequestParams { Message = question, RequestedSchema = new() }),
        },
        requestState: "awaiting");
}

McpServer.IsMrtrSupported returns true whenever the SDK can satisfy InputRequiredException — either natively (draft) or via the legacy resolver (current+stateful).

Compatibility matrix

Negotiated protocol Session Behavior
DRAFT-2026-v1 Stateful / Stateless Native — InputRequiredResult is serialized straight to the wire.
Current (2025-06-18 and earlier) Stateful Backcompat resolver — SDK sends standard elicitation/create / sampling/createMessage / roots/list requests, collects responses, retries the handler with inputResponses. Capped at 10 rounds.
Current Stateless Not supportedInputRequiredException raises an McpException.

Breaking changes under DRAFT-2026-v1

The draft revision removes the server-to-client elicitation/create, sampling/createMessage, and roots/list request methods. The SDK fails fast:

  • McpServer.ElicitAsync, SampleAsync, RequestRootsAsync, AsSamplingChatClient, ElicitAsTaskAsync, SampleAsTaskAsync all throw InvalidOperationException after a DRAFT-2026-v1 session is negotiated. The exception message points to the InputRequest.ForElicitation / ForSampling / ForRootsList replacement.
  • These methods continue to work normally under the current protocol revision and remain the recommended way to do simple one-shot client interactions.

Removed

  • The implicit MRTR machinery that intercepted high-level ElicitAsync/SampleAsync calls and suspended the handler across MRTR rounds. (Replaced by the explicit InputRequiredException contract.)
  • DeferTaskCreation on [McpServerTool] / McpServerToolCreateOptions and the server-side CreateTaskAsync API tied to it. Long-running tasks still use IMcpTaskStore as before.
  • The ExperimentalProtocolVersion opt-in — replaced by negotiating DRAFT-2026-v1 directly.

Follow-ups (intentionally left out of this PR)

  • The next protocol revision removes Mcp-Session-Id and the Stateful mode. When that lands, the current-protocol-stateful row of the matrix collapses into the stateless row, and the legacy elicitation/create / sampling/createMessage / roots/list resolver path can be deleted. The code has // TODO(stateless-draft): markers where that simplification will go.
  • Conformance scenarios for SEP-2322 are wired up in tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests and gated on NodeHelpers.HasMrtrScenarios(). Once conformance#188 merges, the gate can be removed.

Tests

  • ModelContextProtocol.Tests: 1980 passed, 0 failed, 4 skipped
  • ModelContextProtocol.AspNetCore.Tests: 410 passed, 0 failed, 33 skipped

Includes new coverage:

  • DraftProtocolGuardTests — verifies the legacy methods throw under DRAFT-2026-v1.
  • MrtrLowLevelApiTests, MrtrSerializationTests — exercise InputRequiredException and its wire format.
  • MapMcpTests.Mrtr — end-to-end Streamable HTTP coverage.
  • SEP-2322 conformance scenarios under ServerConformanceTests (8 ephemeral + 3 task-based deferred).

Docs

  • docs/concepts/mrtr/mrtr.md rewritten around InputRequiredException with the new compatibility matrix.
  • docs/concepts/elicitation/, sampling/, roots/ updated to call out the DRAFT-2026-v1 behavior change.
  • docs/concepts/tasks/tasks.mdDeferTaskCreation section removed.

@halter73 halter73 changed the title Multi Round-Trip Requests (MRTR) — C# SDK Reference Implementation Multi Round-Trip Requests (MRTR) Mar 21, 2026
@halter73 halter73 force-pushed the halter73/mrtr branch 2 times, most recently from f1dd4c4 to 5845866 Compare March 21, 2026 17:19
@halter73 halter73 requested a review from stephentoub March 21, 2026 18:40
@halter73 halter73 marked this pull request as ready for review May 26, 2026 14:53
@halter73 halter73 requested a review from tarekgh May 26, 2026 15:03
@halter73 halter73 marked this pull request as draft May 26, 2026 15:06
Comment thread src/ModelContextProtocol.Core/Protocol/InputResponse.cs Outdated
Comment thread src/ModelContextProtocol.Core/Client/McpClientImpl.cs
halter73 and others added 8 commits May 27, 2026 17:16
Resolves conflicts from rebasing the MRTR work (originally branched from
4140c6d) onto the current main (b8c4d95). Key conflict resolutions:

- McpClientImpl.SendRequestAsync: combine SEP-2243 tool-context attachment
  with MRTR retry loop for IncompleteResult.
- McpSessionHandler.SendRequestAsync: take MRTR's outgoing filter and
  request logging.
- McpServerImpl.InvokeHandlerAsync: take MRTR's CreateDestinationBoundServer.
- docs/concepts/index.md: combine main's Tasks entry with MRTR additions.
- MapMcpTests.cs: keep main's new IncomingFilter/OutgoingFilter tests in
  full, drop MRTR's outdated overload usage by going through configureClient.
- MrtrIntegrationTests.cs: gate with #if !NET472 (uses ReadLineAsync(CT)).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- IncompleteResult/IncompleteResultException -> InputRequiredResult/InputRequiredException
- Wire format: result_type -> resultType, `incomplete` -> `input_required`
- Drop ExperimentalProtocolVersion option; opt in via ProtocolVersion = `DRAFT-2026-v1`
- Add DraftProtocolVersion constant and include in SupportedProtocolVersions
- Restrict implicit MRTR continuation path to legacy stateful sessions; DRAFT-2026-v1
  and stateless sessions always use the exception-based path

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Implicit MRTR (handler suspension via ElicitAsync) requires both client
  support (DRAFT-2026-v1) and a stateful session. All other cases fall through
  to the exception-based path, which transparently resolves InputRequiredException
  via legacy JSON-RPC requests for clients that don't speak MRTR.
- Drop the now-redundant ProtocolVersion pin from ConfigureExperimentalServer in
  MapMcpTests.Mrtr; server uses the negotiated version like any other server.
- Rewrite the obsolete WithoutExperimental low-level test now that the experimental
  flag is gone; it now verifies retry exhaustion when no input requests are supplied.
- Update other test assertions to use the literal DRAFT-2026-v1 string.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…er draft

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ve input requests with WhenAll+CTS

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t protocol

ElicitAsync/SampleAsync/RequestRootsAsync now throw only when the server is stateless (the existing ThrowIf*Unsupported guards already handled this). Stdio + DRAFT-2026-v1 keeps working via the legacy server-to-client JSON-RPC path; stateless Streamable HTTP throws regardless of protocol revision. A follow-up will force DRAFT-2026-v1 Streamable HTTP to stateless mode.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants