Skip to content

Add draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567)#1610

Draft
halter73 wants to merge 33 commits into
mainfrom
halter73/remove-session-id-draft
Draft

Add draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567)#1610
halter73 wants to merge 33 commits into
mainfrom
halter73/remove-session-id-draft

Conversation

@halter73
Copy link
Copy Markdown
Contributor

Stacked on #1458 (halter73/mrtr). Do not merge until that PR lands.

What

Adds C# SDK support for the protocol-level changes introduced by the draft MCP revision:

  • SEP-2575 — removes the initialize handshake; per-request _meta carries protocol version, client info, and client capabilities; introduces server/discover and subscriptions/listen; new error codes -32004 (UnsupportedProtocolVersion) and -32003 (MissingRequiredClientCapability).
  • SEP-2567 — removes the Mcp-Session-Id header and the session concept from the protocol; state lives in explicit handles passed as tool arguments.

The feature is gated on the existing ExperimentalProtocolVersion setting ("2026-06-XX" placeholder), which already gates MRTR (#1458). A single server can service legacy clients (initialize + Mcp-Session-Id) and draft clients (server/discover + sessionless) concurrently.

Changes

Protocol types & constants

  • RequestMethods.ServerDiscover, RequestMethods.SubscriptionsListen
  • NotificationMethods.SubscriptionsAcknowledgedNotification
  • _meta key constants: ProtocolVersionMetaKey, ClientInfoMetaKey, ClientCapabilitiesMetaKey, LogLevelMetaKey, SubscriptionIdMetaKey
  • McpErrorCode.UnsupportedProtocolVersion = -32004, McpErrorCode.MissingRequiredClientCapability = -32003
  • DiscoverRequestParams / DiscoverResult
  • SubscriptionsListenRequestParams (with SubscriptionsListenNotifications) + SubscriptionsAcknowledgedNotificationParams
  • UnsupportedProtocolVersionErrorData / MissingRequiredClientCapabilityErrorData
  • UnsupportedProtocolVersionException / MissingRequiredClientCapabilityException (typed McpProtocolException derivatives with structured error data round-tripping)

Client (McpClientImpl)

  • ConnectAsync now skips initialize/notifications/initialized when ExperimentalProtocolVersion is set; calls server/discover instead to populate ServerCapabilities / ServerInfo / ServerInstructions.
  • Falls back to legacy initialize (with the highest mutually-supported version) when the server doesn't support the experimental version — either because server/discover is missing (MethodNotFound), the server returned -32004, or the supportedVersions reply doesn't include the requested version.
  • Every outgoing request from a draft client carries protocol version, client info, and client capabilities in _meta via the shared McpSessionHandler.InjectDraftMeta helper. IncompleteResult retries preserve the _meta.

Server (McpServerImpl)

  • Adds ConfigureDiscover (always registered) and ConfigureSubscriptions (basic listen handler that emits the acknowledgement notification and keeps the stream open until cancelled).
  • New built-in incoming message filter CreateDraftStateSyncFilter projects per-request _meta values onto the per-session _negotiatedProtocolVersion / _clientCapabilities / _clientInfo state for draft clients. Legacy clients still populate via initialize and the filter is a no-op for them.
  • McpSessionHandler.PopulateContextFromMeta parses _meta early in incoming-message processing and projects the values onto JsonRpcMessageContext.{ProtocolVersion, ClientInfo, ClientCapabilities, LogLevel} so filters and handlers can read them without re-parsing. Throws -32602 if the per-request _meta protocol version disagrees with the transport-level header (per SEP-2575).

Streamable HTTP (StreamableHttpHandler)

  • A draft request (declared via the MCP-Protocol-Version header) without an Mcp-Session-Id is routed through the stateless path regardless of HttpServerTransportOptions.Stateless — sessions are not created and the Mcp-Session-Id response header is suppressed.
  • Draft requests with an Mcp-Session-Id still flow through the legacy session lookup, preserving back-compat for clients that opted into the experimental version on top of the stateful MRTR model (the MRTR PR's tests rely on this).
  • Draft GET / DELETE without a session ID return 400 with a useful message pointing to subscriptions/listen.
  • ValidateProtocolVersionHeader now returns -32004 (UnsupportedProtocolVersion) with a structured {supported: [...], requested} payload instead of a plain 400.

Streamable HTTP client transport

  • Carries the MCP-Protocol-Version header on every request — sourced from the outgoing request's _meta field when present (the first draft request, server/discover, doesn't have a previously-negotiated version yet).
  • Detects -32004 and -32003 from HTTP error responses and re-throws them as the typed McpProtocolException derivatives so the connection logic can react. Other JSON-RPC errors carried in 4xx/5xx bodies (e.g., 403, 404 session-not-found) continue to surface as HttpRequestException for back-compat.

Tests

  • DiscoverProtocolTests / SubscriptionsListenProtocolTests / DraftErrorDataTests — JSON-serialization round-trips for the new types.
  • DraftConnectionTests — end-to-end coverage for draft client × draft server (negotiates draft), draft client × legacy server (falls back), legacy client × draft server, and explicit server/discover calls from legacy clients.
  • DraftHttpHandlerTests (AspNetCore) — Mcp-Session-Id suppression, -32004 structured-data response shape, the back-compat path for draft requests with a session id, and GET/DELETE rejection under draft.

Test results

  • Core: 1916 / 1920 passing (4 skipped, pre-existing).
  • AspNetCore (net10): 361 / 390 passing. 2 failures are pre-existing Node.js conformance test infrastructure issues unrelated to this PR (also fail on halter73/mrtr). 27 skipped, including 4 new skips for await-style MRTR scenarios that fundamentally require session affinity (draft mode is sessionless, so the high-level continuation tracker can't span requests; the low-level IncompleteResultException path still works).
  • One MRTR integration test (LegacyRequestOnMrtrSession_LogsWarning) was updated to expect server/discover instead of initialize from a draft client.

Out of scope (deliberate follow-ups)

  • McpClient.SubscribeToListenAsync — a high-level client method for opening subscriptions/listen and a draft-mode rewrite of the resource-subscribe convenience methods. The server-side handler exists; the client just doesn't have a typed wrapper yet.
  • ILoggersubscriptions/listen compat shim — server-side log notifications outside a request flow through any active subscriptions/listen subscription's _meta/logLevel. Requires the client method first.
  • Await-style MRTR over sessionless transports — high-level server.ElicitAsync / server.SampleAsync continuations don't span requests under draft mode because the server is forced stateless per request. Could be re-enabled with a requestState-keyed continuation lookup; documented as a skip in the relevant tests for now.
  • Tasks extension (SEP-2663) — separate PR per the task owner.
  • [Obsolete] on legacy SDK surface — explicitly out of scope; both legacy and draft paths coexist without deprecation annotations.
  • HTTP+SSE (2024-11-05) transport — untouched; remains mapped under /sse and /message for legacy back-compat.

Spec status

The draft revision (where SEP-2575 and SEP-2567 are merged) doesn't yet have a finalized protocol version label. The SDK uses the existing placeholder "2026-06-XX"; swap when the real label lands.

Risk

  • New code paths are additive and gated on ExperimentalProtocolVersion. Existing legacy paths (initialize, Mcp-Session-Id, GET stream, Last-Event-ID, resources/subscribe, ping, logging/setLevel) remain functional and untouched.
  • One behavioral change: HTTP 4xx with a -32004 or -32003 JSON-RPC body now throws a typed McpProtocolException from the streamable HTTP client transport instead of HttpRequestException. Other error codes are unchanged. This was scoped down from a broader behavior change after observing the Messages_FromNewUser_AreRejected regression.

halter73 and others added 30 commits March 20, 2026 11:32
Move MRTR logic out of McpServer and McpClient base classes into their
internal implementations, keeping the mockable API surface clean.

Server side:
- Remove McpServer.ActiveMrtrContext (was internal)
- Add MRTR interception to DestinationBoundMcpServer.SendRequestAsync
  with task guard (SampleAsTaskAsync/ElicitAsTaskAsync bypass MRTR)
- Remove MRTR branches from SampleAsync, ElicitAsync, RequestRootsCoreAsync
- Task status tracking (InputRequired) now works during MRTR

Client side:
- Remove McpClient.ResolveInputRequestsAsync (was internal abstract)
- Move MRTR retry loop into McpClientImpl.SendRequestAsync override
- Replace SendRequestWithMrtrAsync with existing McpSession typed helper
- Make resolve methods private on McpClientImpl

Add 4 new tests for MRTR+Tasks interaction:
- Task-augmented tool call with MRTR sampling
- MRTR elicitation through tool call
- SampleAsTaskAsync bypasses MRTR interception
- MRTR tool call and task-based sampling coexist

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Gate MRTR on a draft protocol version ("2026-06-XX") instead of the
experimental["mrtr"] capability. This matches how the real protocol will
work when MRTR is ratified — the protocol version IS the signal.

Changes:
- Add ExperimentalProtocolVersion property to McpClientOptions and
  McpServerOptions, marked [Experimental(MCPEXP001)]
- Add ExperimentalProtocolVersion constant to McpSessionHandler
- Client: request experimental version when option is set; accept it
  in server response validation
- Server: accept experimental version from client when option matches;
  ClientSupportsMrtr() checks negotiated version instead of capability
- StreamableHttpHandler: accept experimental version in header validation
- Remove experimental["mrtr"] capability advertisement and
  MrtrContext.ExperimentalCapabilityKey

Compatibility matrix (no failures):
- Both experimental: MRTR via IncompleteResult + retry
- Server exp, client not: Legacy JSON-RPC requests
- Client exp, server not: Negotiates to stable, retry loop is no-op
- Neither: Standard behavior

Tests:
- Update all existing MRTR tests to set ExperimentalProtocolVersion
- Add 5 new compatibility tests covering all matrix combinations
- All 1886 core + 324 AspNetCore tests pass on net10.0 and net9.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add IncompleteResultException for tool handlers to return incomplete
  results with inputRequests and/or requestState directly
- Add McpServer.IsMrtrSupported property for checking client compatibility
- Handle IncompleteResultException in MRTR wrapper and race handler
- Validate MRTR support when exception is thrown (returns JSON-RPC error
  if client doesn't support MRTR)
- Fall through to MRTR-aware invocation for unmatched requestState retries
- Add 8 protocol conformance tests (raw HTTP) for low-level MRTR flows
- Add 7 integration tests for client auto-retry of low-level tools
- Add MRTR concept documentation covering both high-level and low-level APIs

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

- Test concurrent ElicitAsync+SampleAsync throws InvalidOperationException
  (MrtrContext prevents concurrent server-to-client requests)
- Test cancellation mid-retry stops the MRTR loop with OperationCanceledException
- Test via outgoing message filters that no old-style sampling/elicitation
  JSON-RPC requests are sent when MRTR is active
- Test that transport middleware sees IncompleteResult round-trips

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Allow IncompleteResultException to serialize as IncompleteResult in
stateless mode where ClientSupportsMrtr() returns false. The low-level
API is designed for stateless servers that cannot determine client MRTR
support.

Add 5 end-to-end tests using Streamable HTTP in stateless mode:
- Elicitation, sampling, and roots individually
- All three concurrent (with TCS concurrency proof barriers)
- Multi-round-trip with requestState across 2 retries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add high-level and low-level MRTR examples to each feature doc:
- elicitation.md: ElicitAsync (transparent) + IncompleteResultException
- sampling.md: SampleAsync (transparent) + IncompleteResultException
- roots.md: RequestRootsAsync (transparent) + IncompleteResultException

Fix missing entries in docs navigation:
- toc.yml: Add Sampling under Client Features
- index.md: Add Tasks and MRTR to Base Protocol table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move MrtrExchange and MrtrContinuation into their own files to follow
the convention of one top-level class per file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Proves that outgoing/incoming message filters can track and enforce
per-session MRTR flow limits using context.Server.SessionId:

- OutgoingFilter_TracksIncompleteResultsPerSession: verifies count
  increments on IncompleteResult and decrements after retry
- OutgoingFilter_CanEnforcePerSessionMrtrLimit: verifies replacing
  IncompleteResult with a JSON-RPC error when limit is exceeded

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

ClientSupportsMrtr now purely reflects whether the client negotiated
the MRTR protocol version, independent of server transport mode.
The stateless guard is moved to the call site that gates the high-level
await path (which requires storing continuations).

In stateless mode, each request creates a new McpServerImpl that never
sees the initialize handshake. The Mcp-Protocol-Version header is now
flowed via JsonRpcMessageContext.ProtocolVersion so the MRTR wrapper
can populate _negotiatedProtocolVersion, making IsMrtrSupported return
true when the client sends the experimental protocol version header.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tests that mirror the exact code patterns from mrtr.md and
elicitation.md docs in stateless mode:

- IsMrtrSupported returns false when client doesn't opt in
- IsMrtrSupported check + IncompleteResultException throw (the doc
  pattern) works end-to-end including ElicitResult.Content access
- Same pattern returns fallback when client doesn't opt in
- Load shedding (requestState-only) with IsMrtrSupported guard

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add SessionDelete_CancelsPendingMrtrContinuation test verifying:
  - MRTR continuation is cancelled on session DELETE
  - Debug-level log emitted for cancelled continuations
  - No Error-level log noise from handler cancellation
- Add SessionDelete_RetryAfterDelete_ReturnsSessionNotFound test
  verifying retry with stale requestState returns 404
- Add MrtrContinuationsCancelled debug log in DisposeAsync
- Skip ToolCallError log for OperationCanceledException during
  disposal (not a tool bug, just session teardown)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Harden the MRTR (Multi Round-Trip Request) implementation to correctly
handle cancellation across retries, clean shutdown, and handler lifecycle
tracking.

Thread-safety:
- Replace mutable ExchangeTask property with immutable InitialExchangeTask
  and return-value data flow from ResetForNextExchange
- Use Interlocked.CompareExchange in ResetForNextExchange to validate
  expected state, ensuring concurrent calls reliably fail
- Use TrySetResult as the sole atomicity gate in RequestInputAsync, with
  explicit failure on concurrent exchanges
- Store SourceTcs back-reference in MrtrExchange for CAS validation

Cancellation:
- Introduce a long-lived handler CTS (encapsulated in MrtrContinuation)
  that survives across retries, keeping the handler cancellable after the
  original request's combinedCts is disposed
- Bridge each retry's cancellation to the handler CTS via
  CancellationTokenRegistration in AwaitMrtrHandlerAsync
- Check TrySetResult/TrySetException return values on retry to detect
  already-cancelled exchanges
- CTS is never disposed (like Kestrel's HttpContext.RequestAborted) to
  avoid deadlock risks from Cancel/Dispose inside synchronization
  primitives. CancelHandler() is the sole operation and is thread-safe.

Shutdown:
- Dispose session handler before iterating _mrtrContinuations so no new
  continuations can be created during the cleanup loop
- Track MRTR handler tasks with inFlightCount + TCS drain pattern
  (matching McpSessionHandler.ProcessMessagesCoreAsync) so DisposeAsync
  waits for all handlers to complete before returning
- Add ObserveHandlerCompletionAsync fire-and-forget observer that logs
  unhandled handler exceptions at Error level

Logging:
- Exclude IncompleteResultException from Error-level ToolCallError logging
  since it is normal MRTR control flow, not an error

Simplifications:
- Flow MrtrContext via JsonRpcMessageContext property instead of
  _pendingMrtrContexts ConcurrentDictionary with synchronous-before-await
  assumptions
- MrtrContinuation is a lifecycle object created upfront, eliminating
  CTS disposal branching, orphanedCts tracking, and post-drain cleanup

Tests (8 new):
- ServerDisposal_CancelsHandlerCancellationToken_DuringMrtr
- CancellationNotification_DuringInFlightMrtrRetry_CancelsHandler
- CancellationNotification_ForExpiredRequestId_DoesNotAffectHandler
- DisposeAsync_WaitsForMrtrHandler_BeforeReturning
- HandlerException_DuringMrtr_IsLoggedAtErrorLevel
- IncompleteResultException_IsNotLoggedAtErrorLevel

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace non-generic TaskCompletionSource (introduced in .NET 5) with
TaskCompletionSource<bool> in McpClientMrtrTests.cs so the test project
compiles against net472, which only has TaskCompletionSource<TResult>.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add support for tools to perform ephemeral MRTR exchanges before
committing to a background task. This enables a two-phase workflow:

1. Ephemeral phase: The handler uses ElicitAsync/SampleAsync via MRTR
   to gather user input (e.g., confirmation before expensive operations).
2. Task phase: The handler calls CreateTaskAsync() to transition to a
   background task, receiving a task ID and cancellation token.

API surface:
- McpServerToolAttribute.DeferTaskCreation property
- McpServerToolCreateOptions.DeferTaskCreation property
- McpServerTool.DeferTaskCreation virtual property (overridden in
  AIFunctionMcpServerTool and DelegatingMcpServerTool)
- McpServer.CreateTaskAsync() virtual method (overridden in
  DestinationBoundMcpServer)

Implementation:
- DeferredTaskInfo carries task metadata across MRTR continuations,
  with signal/ack TCS pair for handler ↔ framework coordination.
- ConfigureTools attaches DeferredTaskInfo to MrtrContext when
  DeferTaskCreation is enabled and client provides task metadata.
- AwaitMrtrHandlerAsync races handler vs exchange vs task creation
  signal (3-way WhenAny).
- HandleDeferredTaskCreationAsync creates the task, re-links the
  handler CTS to the task cancellation token, and acknowledges the
  handler so it can continue as a background task.
- TrackDeferredHandlerTaskAsync tracks completion and stores results
  (handler already tracked by ObserveHandlerCompletionAsync for
  in-flight counting).

If the handler returns without calling CreateTaskAsync(), a normal
(non-task) result is returned to the client.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…w, fix logging

Document DeferTaskCreation and CreateTaskAsync in MRTR and Tasks
conceptual docs with cross-references and matching test coverage.

Revert MrtrContext flow from JsonRpcMessageContext property back to
_mrtrContextsByRequestId ConcurrentDictionary with try/finally.

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

Add test verifying a tool can use the high-level MRTR elicit API then
throw IncompleteResultException to drop to the low-level API in a
single call. Replace try/finally with 'using var' for the
CancellationTokenRegistration. Fix task/mrtr comment and use modern
indexing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…acy JSON-RPC

When a tool throws IncompleteResultException and the client doesn't
support MRTR, the server now resolves each InputRequest by sending the
corresponding standard JSON-RPC call (elicitation, sampling, roots) to
the client and retries the handler with the responses. This allows
authors to write a single MRTR-native tool implementation that works
with any client.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The backcompat layer now returns 'without input requests' instead of
'Multi Round-Trip Requests' when IncompleteResultException has no
inputRequests.

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

Backcompat tests (McpClientMrtrCompatTests):
- 10-retry limit enforcement (tool that never completes)
- Empty inputRequests dictionary triggers immediate error
- Error propagation when client handler throws during resolve

Experimental mode test (McpClientMrtrTests):
- Client handler throws during MRTR input resolution: exception
  surfaces to caller, server logs cancelled MRTR continuation on
  disposal. This exercises a fundamental MRTR limitation where the
  client has no channel to communicate input resolution failures
  back to the server.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
IsMrtrSupported now returns true whenever the low-level MRTR API
(IncompleteResultException) can be used, not just when the client
natively negotiated MRTR. The only case where it returns false is
stateless mode with a non-MRTR client, where nobody can drive the
retry loop.

- Add IsLowLevelMrtrAvailable() to McpServerImpl
- Update DestinationBoundMcpServer.IsMrtrSupported to use it
- Error explicitly for stateless + non-MRTR in the IncompleteResult
  handler instead of silently serializing an unusable response
- Add Stateless_IncompleteResultException_WithoutMrtrClient_ReturnsError
  test verifying the error path
- Fix existing tests: stateless tests now properly negotiate MRTR,
  protocol test verifies IsMrtrSupported=true via backcompat

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move server-behavior tests from Client/ to Server/ and drop the
misleading McpClient prefix. Most MRTR tests verify server-side
behavior (handler lifecycle, backcompat resolution, low-level API)
but were in Client/ because they use ClientServerTestBase.

Split McpClientMrtrTests (17 tests) into:
- Client/MrtrIntegrationTests: E2E round-trips and client retry (10)
- Server/MrtrHandlerLifecycleTests: cancellation, disposal, logging (7)

Rename and move to Server/:
- McpClientMrtrCompatTests → MrtrBackcompatTests
- McpClientMrtrLowLevelTests → MrtrLowLevelApiTests
- McpClientMrtrMessageFilterTests → MrtrMessageFilterTests
- McpClientMrtrSessionLimitTests → MrtrSessionLimitTests
- McpClientMrtrWithTasksTests → MrtrTaskIntegrationTests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add McpClient + Stateful Streamable HTTP integration tests for MRTR,
and a test verifying experimental protocol version negotiation. Fix the
sampling log assertion in MapMcpTests to handle MRTR mode where sampling
is embedded in the IncompleteResult exchange. Demote MrtrHandlerError
from Error to Debug to avoid double-logging since the observer just
confirms the exception was observed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a shared ServerMessageTracker utility that records outgoing server-to-
client JSON-RPC request methods via an outgoing message filter. Wire it into
all 8 MRTR test classes and call AssertNoLegacyMrtrRequests() in ~35
experimental-mode tests to verify no legacy elicitation/create or
sampling/createMessage requests are sent.

Simplify MrtrMessageFilterTests by replacing its manual ConcurrentBag-based
tracking with the shared tracker (-29 lines).

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

Fix McpSessionHandler.SendRequestAsync to route through the outgoing message
filter pipeline instead of calling SendToRelatedTransportAsync directly. This
makes server-originated JSON-RPC requests (elicitation/create,
sampling/createMessage, roots/list) visible to outgoing filters, matching the
documented behavior that filters see all outgoing messages.

Enhance ServerMessageTracker to use both incoming and outgoing filters for
comprehensive MRTR protocol mode verification:
- Outgoing: detects IncompleteResult responses (result_type=incomplete) and
  legacy JSON-RPC requests
- Incoming: detects MRTR retries (requests with inputResponses or requestState)

Add AssertMrtrUsed() to ~35 experimental MRTR tests and AssertMrtrNotUsed()
to ~15 backward-compatibility tests, ensuring every MRTR test verifies the
correct protocol mode was used.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor MRTR tests to provide extensive coverage with less code, making
it easy for reviewers and other SDK implementers to see edge case
coverage at a glance.

Test consolidation:
- Move MRTR tests into MapMcpTests.Mrtr.cs partial class, which runs
  every test across StreamableHttp, SSE, and Stateless transports
- Use Theory tests with (experimentalServer, experimentalClient) bools
  to cover all 4 MRTR/backcompat protocol combinations
- Delete MrtrBackcompatTests.cs, StatelessMrtrTests.cs, and
  StreamableHttpMrtrTests.cs (coverage subsumed by MapMcpTests)
- Reduce MrtrLowLevelApiTests.cs and MrtrProtocolTests.cs to unique
  protocol-level tests not covered by McpClient-based tests
- Update MrtrIntegrationTests doc to reflect edge-case focus and add
  simple happy-path smoke test for reviewer reference

Test improvements:
- Add ServerMessageTracker with AssertMrtrUsed()/AssertMrtrNotUsed()
  to verify correct protocol mode in every test
- Add NegotiatedProtocolVersion assertions to verify negotiation
- Add result.IsError checks on all success-path tool results
- Add ErrorCode assertions on all error-path tests
- Tighten all version assertions to exact values (no NotEqual)

Helpers:
- ConnectAsync takes Action<McpClientOptions>? to prevent bypassing
  transport config (path/TransportMode defaults)
- ConfigureExperimentalServer/ConfigureDefaultServer with distinct
  Implementation names for clear test output
- ConfigureMrtrHandlers configures elicitation, sampling, and roots
  handlers on an existing McpClientOptions object
- ConnectExperimentalAsync/ConnectDefaultAsync for common patterns
- Move EnablePollingAsync stateless test to MapMcpStatelessTests
- Add client-side warnings for legacy requests on MRTR sessions and
  IncompleteResult on non-MRTR sessions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
halter73 and others added 3 commits March 25, 2026 12:51
…tion

When the caller's CancellationToken triggers an OperationCanceledException
during a pipe write, re-throw it instead of wrapping it in IOException.
This ensures callers get the expected OperationCanceledException when they
cancel their own token.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the protocol-level changes for the draft revision (SEP-2575 stateless MCP and SEP-2567 sessionless MCP):

- New _meta keys for per-request protocolVersion / clientInfo / clientCapabilities / logLevel
- New RPCs: server/discover and subscriptions/listen, plus the acknowledgement notification
- New JSON-RPC error codes -32004 (UnsupportedProtocolVersion) and -32003 (MissingRequiredClientCapability) with typed exception classes
- Client skips initialize under draft mode, calls server/discover instead, and falls back to legacy initialize when the server doesn't support the experimental version
- Server keeps the legacy initialize handler for back-compat, and a new built-in incoming message filter projects per-request _meta values onto the per-session client info/capabilities/version state under draft
- HTTP server suppresses Mcp-Session-Id and routes draft requests through the stateless path regardless of HttpServerTransportOptions.Stateless
- HTTP server returns -32004 with a structured supportedVersions data payload when a client requests an unsupported protocol version
- HTTP client transport carries the protocol version header on every request (sourced from per-request _meta when present), and surfaces -32004/-32003 from HTTP error responses as typed McpProtocolException for the connection logic to react

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds round-trip tests for the new draft protocol revision and updates the XML
documentation on ExperimentalProtocolVersion (client + server) to describe the
full SEP-2575 + SEP-2567 behavior (sessionless, handshake-less, server/discover,
MRTR-only server-to-client interactions, fallback to legacy initialize on
unsupported-version responses).

Tests added:
- DiscoverProtocolTests / SubscriptionsListenProtocolTests / DraftErrorDataTests:
  JSON-serialization round-trip coverage for the new protocol types and error
  data payloads.
- DraftConnectionTests: end-to-end client/server connection flow for draft
  client vs. draft server, draft client vs. legacy server (fallback), legacy
  client vs. draft server, and explicit server/discover invocation.
- DraftHttpHandlerTests (AspNetCore): HTTP-level checks that draft requests
  don't emit Mcp-Session-Id, unsupported protocol versions return -32004 with
  the structured supportedVersions payload, draft requests carrying an
  Mcp-Session-Id route through the legacy lookup, and draft GET/DELETE are
  rejected.

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.

1 participant