Multi Round-Trip Requests (MRTR)#1458
Open
halter73 wants to merge 15 commits into
Open
Conversation
9 tasks
f1dd4c4 to
5845866
Compare
This was referenced Mar 23, 2026
9 tasks
tarekgh
reviewed
May 26, 2026
tarekgh
reviewed
May 26, 2026
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>
…l framing, restore lost theory coverage - Revert BOM-only diffs on AIFunctionMcpServerTool.cs and DelegatingMcpServerTool.cs. - Drop the unused System.Diagnostics.CodeAnalysis using in McpServerTool.cs. - Restore the trailing newline in McpServerToolAttribute.cs. - Revert the NegotiatedProtocolVersion stub change in McpServerTests.cs (only the deleted ThrowIfDraftProtocol gate needed it). - Drop the stray blank line in MapMcpTests.cs. - Inline IsLowLevelMrtrAvailable into a public override IsMrtrSupported on McpServerImpl; DestinationBoundMcpServer.IsMrtrSupported is now a simple proxy. - Rewrite the stale IsStatefulSession XML doc. - Rename MrtrLowLevelApiTests -> MrtrInputRequiredExceptionTests, and drop low-level/high-level adjectives from MRTR tests + docs. - Restore InlineData(true) on Mrtr_MixedExceptionAndAwaitStyle (covers draft+stateful mixed mode); add AssertMrtrUsedAtLeastOnce helper. - Collapse Mrtr_ParallelAwaits to a Fact (under the new contract draft+stateful behaves the same as legacy+stateful). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… to fix docfx warnings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
…nsport to avoid GET-stream race Server-side InputRequiredException backcompat resolver was calling this.ElicitAsync / SampleAsync / RequestRootsAsync, which routes outgoing requests through the session-level _transport. StreamableHttpServerTransport.SendMessageAsync silently drops messages when no GET request has arrived yet, so under CI load the McpClient's async GET startup could race with the in-flight tools/call, causing the resolver to wait on a TCS forever. Route the outgoing requests through CreateDestinationBoundServer(request) instead, matching the pattern used by tool-initiated server.SampleAsync etc. Outgoing JSON-RPC then flows back through the original POST's response stream (always open during the tool call) instead of the standalone GET. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
halter73
commented
May 28, 2026
MrtrProtocolTests.BackcompatResolver_SendsServerRequestOverPostStream_WithoutGetStream deliberately never opens a GET stream, so it deterministically fails if the server's backcompat resolver routes its outgoing roots/list request through the session-level transport instead of the POST's RelatedTransport. Verified the test hangs/fails with the fix reverted and passes with it applied. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced May 28, 2026
tarekgh
reviewed
May 29, 2026
tarekgh
reviewed
May 29, 2026
tarekgh
reviewed
May 29, 2026
tarekgh
reviewed
May 29, 2026
tarekgh
reviewed
May 29, 2026
tarekgh
reviewed
May 30, 2026
- McpSessionHandler.SendRequestAsync no longer double-wraps SendToRelatedTransportAsync in _outgoingMessageFilter and no longer duplicates the per-request logging that SendToRelatedTransportAsync already emits. Restores main's once-per-send semantics. - McpClientImpl.ResolveInputRequestAsync gracefully handles roots/list InputRequests with no params (ListRootsRequestParams is optional per spec) by falling back to a default instance, matching the server-side resolver. - Rename local var (McpClientImpl) and parameter (McpServerImpl.SerializeInputRequiredResult) from PascalCase 'InputRequiredResult' to camelCase 'inputRequiredResult'. - StreamableHttpHandler.ValidateProtocolVersionHeader restored to private static (uses only a const and a static field; no instance state). - Tighten InputRequiredResult XML doc to note that this SDK currently only wires the MRTR interceptor into tools/call, even though SEP-2322 defines the wire format for prompts/get and resources/read too. - Tighten outgoing- and incoming-filter tests (AddOutgoingMessageFilter_Sees_Responses_Notifications_And_Requests, OutgoingFilter_SeesResponsesAndRequests, AddIncomingMessageFilter_Intercepts_Request_Messages, and AddIncomingMessageFilter_Multiple_Filters_Execute_In_Order) from substring/Contains/IndexOf checks to strict per-category counts. The substring assertions passed even when SendRequestAsync invoked the outgoing filter twice per request, so the regression went undetected; the new counts catch it (sampling/createMessage and tool-call response counts double when the bug is present). The symmetric incoming-side tightening guards against an analogous future regression on the receive pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/callwithinputResponsesattached, 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
InputRequiredExceptionis the only way to do MRTR. A tool throws it with anInputRequiredResultcontaininginputRequestsand/orrequestState, and the SDK turns that into the right wire response for the negotiated protocol.McpServer.IsMrtrSupportedreturnstruewhenever the SDK can satisfyInputRequiredException— either natively (draft) or via the legacy resolver (current+stateful).Compatibility matrix
DRAFT-2026-v1InputRequiredResultis serialized straight to the wire.2025-06-18and earlier)elicitation/create/sampling/createMessage/roots/listrequests, collects responses, retries the handler withinputResponses. Capped at 10 rounds.InputRequiredExceptionraises anMcpException.Breaking changes under
DRAFT-2026-v1The draft revision removes the server-to-client
elicitation/create,sampling/createMessage, androots/listrequest methods. The SDK fails fast:McpServer.ElicitAsync,SampleAsync,RequestRootsAsync,AsSamplingChatClient,ElicitAsTaskAsync,SampleAsTaskAsyncall throwInvalidOperationExceptionafter aDRAFT-2026-v1session is negotiated. The exception message points to theInputRequest.ForElicitation/ForSampling/ForRootsListreplacement.Removed
ElicitAsync/SampleAsynccalls and suspended the handler across MRTR rounds. (Replaced by the explicitInputRequiredExceptioncontract.)DeferTaskCreationon[McpServerTool]/McpServerToolCreateOptionsand the server-sideCreateTaskAsyncAPI tied to it. Long-running tasks still useIMcpTaskStoreas before.ExperimentalProtocolVersionopt-in — replaced by negotiatingDRAFT-2026-v1directly.Follow-ups (intentionally left out of this PR)
Mcp-Session-Idand theStatefulmode. When that lands, the current-protocol-stateful row of the matrix collapses into the stateless row, and the legacyelicitation/create/sampling/createMessage/roots/listresolver path can be deleted. The code has// TODO(stateless-draft):markers where that simplification will go.tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTestsand gated onNodeHelpers.HasMrtrScenarios(). Once conformance#188 merges, the gate can be removed.Tests
ModelContextProtocol.Tests: 1980 passed, 0 failed, 4 skippedModelContextProtocol.AspNetCore.Tests: 410 passed, 0 failed, 33 skippedIncludes new coverage:
DraftProtocolGuardTests— verifies the legacy methods throw underDRAFT-2026-v1.MrtrLowLevelApiTests,MrtrSerializationTests— exerciseInputRequiredExceptionand its wire format.MapMcpTests.Mrtr— end-to-end Streamable HTTP coverage.ServerConformanceTests(8 ephemeral + 3 task-based deferred).Docs
docs/concepts/mrtr/mrtr.mdrewritten aroundInputRequiredExceptionwith the new compatibility matrix.docs/concepts/elicitation/,sampling/,roots/updated to call out theDRAFT-2026-v1behavior change.docs/concepts/tasks/tasks.md—DeferTaskCreationsection removed.