Default draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567)#1610
Open
halter73 wants to merge 16 commits into
Open
Default draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567)#1610halter73 wants to merge 16 commits into
halter73 wants to merge 16 commits into
Conversation
bb9f572 to
30782f6
Compare
This was referenced Jun 8, 2026
…d (SEP-2575, SEP-2567) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…options (MCP9005) Default `HttpServerTransportOptions.Stateless` to true so new code on the 2026-07-28 draft revision (SEP-2567) is sessionless from the start. Mark the surface that only makes sense in the legacy stateful HTTP mode as obsolete behind the new MCP9005 diagnostic so callers see a deprecation hint but can still pin Stateless = false to keep using session-based behaviors during back-compat: * `HttpServerTransportOptions.EventStreamStore` (resumability) * `HttpServerTransportOptions.SessionMigrationHandler` (multi-node migration) * `HttpServerTransportOptions.PerSessionExecutionContext` * `HttpServerTransportOptions.IdleTimeout` * `HttpServerTransportOptions.MaxIdleSessionCount` Internal infrastructure that legitimately reads those options for the back-compat stateful path now suppresses MCP9005 at the use site. Test projects suppress it globally via NoWarn because the suite intentionally exercises both modes. Update tests/samples that previously relied on the implicit `Stateless = false` default to set it explicitly: * TestSseServer.Program — SSE always needs stateful state shared across GET/POST. * ConformanceServer.Program — resumability + OAuth conformance scenarios are stateful. * ResumabilityIntegrationTestsBase — resumability is a stateful concern. * SseIntegrationTests / MapMcpSseTests — SSE requires stateful. * OAuthTestBase — OAuth flow uses the GET /sse session-based endpoint. * MrtrProtocolTests / SessionMigrationTests / StreamableHttpServerConformanceTests — these tests intentionally drive the legacy stateful session machinery. * DraftHttpHandlerTests — tests draft rejection of GET/DELETE endpoints, which are only mapped when Stateless = false. Rework HTTP header conformance helpers (HttpHeaderConformanceTests + StreamableHttpServerConformanceTests) to stop asserting an mcp-session-id response header from draft/non-draft initialize, because the sessionless default means none is returned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…to legacy protocol Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…of overwriting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Detect fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… #2855) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pec-version strings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- RawStreamConformanceTests.cs: wrap in #if !NET472 to avoid ReadLineAsync(CancellationToken) overload missing on .NET Framework. - HttpMcpServerBuilderExtensionsTests: IdleTrackingBackgroundService_StartsTimer_WhenStateful needs explicit Stateless=false after the default flipped to true in commit 8904958. - HttpHeaderConformanceTests: two tests used the old DRAFT-2026-v1 wire-version string which the server now rejects; updated to 2026-07-28. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
d9277e0 to
f3f6843
Compare
The server validated that Mcp-Param-* header values are conformantly encoded (printable ASCII, or a "=?base64?...?=" wrapper for non-ASCII) but applied no such validation to the standard Mcp-Name header. Raw non-ASCII Mcp-Name values were passed through and compared byte-for-byte against the body name. Mirror the existing Mcp-Param-* validation for Mcp-Name: reject values containing characters outside the valid HTTP header value range, then decode the "=?base64?...?=" wrapper before comparing to the body value. This makes the server reject mis-encoded non-ASCII names and correctly accept compliant base64-wrapped non-ASCII tool/resource/prompt names. Fixes HttpHeaderConformanceTests.Server_RejectsInvalidUtf8EncodedHeaderValue, which previously passed only incidentally on the stateful draft path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The draft 2026-07-28 protocol removes stream resumability entirely (a dropped connection is treated as cancellation), so the SSE event stream store surface is legacy-only. Mark the resumability interfaces, options, and wire-up [Obsolete] under the existing MCP9005 (LegacyStatefulHttp) diagnostic, matching the already-obsoleted stateful HTTP options: - ISseEventStreamReader / ISseEventStreamWriter / ISseEventStreamStore - SseEventStreamMode / SseEventStreamOptions - StreamableHttpServerTransport.EventStreamStore - DistributedCacheEventStreamStoreOptions - WithDistributedCacheEventStreamStore Internal SDK usage of these now-obsolete types is suppressed with targeted MCP9005 pragmas (and project-level NoWarn where source generators emit code over the obsolete types). External consumers still receive the obsolete warning. Behavior is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Under the draft revision (SEP-2575 + SEP-2567) the HTTP request lifetime is the request lifetime: there are no sessions, so a dropped connection is equivalent to cancelling the in-flight request. Verify that aborting the HTTP request flows cancellation into a running tool handler's CancellationToken, covering both draft sessionless mode and legacy stateless mode (both are 1:1 request-to-handler). The tests drive raw HTTP via the in-memory Kestrel transport: a tool blocks on its injected CancellationToken, the client aborts the request mid-flight, and the tests assert the server observes RequestAborted and the tool's token fires. No production change was required; the existing session-disposal path already propagates the abort. These pin that behavior going forward. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Jun 16, 2026
Closed
…scenarios Adds the two SEP-2322 ConformanceServer tools that RunMrtrConformanceTest previously skipped as "not yet implemented": - test_input_required_result_tampered_state: R1 issues an HMAC-signed requestState; R2 with a tampered requestState surfaces a -32602 JSON-RPC error (McpProtocolException propagates as a protocol error, not an isError CallToolResult). - test_input_required_result_capabilities: emits inputRequests only for the capabilities the client declared on the per-request _meta clientCapabilities envelope (read via JsonRpcMessageContext.ClientCapabilities). Removes the per-row Skip from both [InlineData] rows so they run under the same HasMrtrScenarios() gate as the other MRTR scenarios. Verified live: 14/14 RunMrtrConformanceTest scenarios pass against the local compat/conformance-draft build (which emits the 2026-07-28 wire string). Adds in-process wire-level regression coverage in MrtrProtocolTests (TamperedRequestState_ReturnsJsonRpcError and CapabilityCheck_OnlyEmitsInputRequestsForDeclaredCapabilities) so both behaviors stay verified in CI even while the published conformance package's draft wire string lags this SDK. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…atch When a draft request's MCP-Protocol-Version header disagrees with the per-request _meta io.modelcontextprotocol/protocolVersion value (SEP-2575), the server already rejected the request in PopulateContextFromMeta, but it used -32602 InvalidParams. A conformant draft client's server/discover probe treats any non-modern JSON-RPC error (including InvalidParams) as a legacy-server signal and falls back to the initialize handshake. That means a modern draft server that detected a genuine header/body mismatch would be misread as legacy. Emit -32001 HeaderMismatch instead -- the same code already used for the Mcp-Method/Mcp-Name header-vs-body checks and the exact code the client's probe recognizes as a modern-server signal to surface as-is (see McpClientImpl's catch (McpProtocolException ex) when (ex.ErrorCode == McpErrorCode.HeaderMismatch)). Adds a RawHttpConformanceTests regression asserting a header/_meta protocol-version mismatch yields -32001. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Flip McpClientImpl.ConnectAsync so a null ProtocolVersion (the default) prefers the draft revision (SEP-2575 + SEP-2567): the client probes with server/discover and transparently falls back to the legacy initialize handshake when the server doesn't support draft. The legacy branch now runs only when the caller explicitly pins a non-draft version, making draft opt-out rather than opt-in. Merge (rather than overwrite) the session-level client capabilities into each request's _meta envelope so per-request opt-ins already written by higher layers (e.g. the tasks-extension capability from GetMetaWithTaskCapability) survive now that draft _meta injection is the default path. Refresh the XML docs on McpClientOptions.ProtocolVersion / MinProtocolVersion, McpSession.DraftProtocolVersion, and McpSessionHandler.DraftProtocolVersion to describe the new default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
docs/concepts/stateless/stateless.md references xref:list-of-diagnostics#obsolete-apis, but list-of-diagnostics.md had no uid front matter, so `make generate-docs` (docfx --warningsAsErrors) failed on every CI job that reached it. Add the uid following the existing stateless.md pattern; the ## Obsolete APIs heading already resolves to the obsolete-apis anchor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With the client default flipped to draft, every test that builds a default client now negotiates server/discover instead of initialize. Sweep the suite: tests whose purpose is the legacy initialize handshake, Mcp-Session-Id lifecycle, session resumption/reconnect, DELETE, event-stream polling, or server->client sampling over a persistent stream are pinned to 2025-11-25 (these behaviors don't exist under the sessionless draft revision); handshake-agnostic tests run on the draft default with incidental assertions (message counts, captured initialize requests, session-id headers) adjusted. The ConformanceClient pins the legacy "initialize" and "sse-retry" scenarios while letting the others exercise the draft probe plus transparent legacy fallback. Draft sampling/elicitation coverage is retained via the stdio MRTR tests (ClientServerTestBase / MapMcpTests.Mrtr). 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 the draft MCP protocol revision (
2026-07-28) in the C# SDK — removing theinitializehandshake andMcp-Session-Idper SEP-2575 and SEP-2567, while preserving back-compat with legacy clients/servers via probe-and-fallback negotiation.Stacked on the now-merged #1458 (MRTR). Opt in to draft by setting
ProtocolVersion = McpSessionHandler.DraftProtocolVersion.What's in
Protocol
DraftProtocolVersionvalue set to"2026-07-28"(spec string, replaces MRTR's"DRAFT-2026-v1"placeholder).server/discoverregistered on every server; serves as the bootstrap mechanism (clients send it first under draft)._meta.io.modelcontextprotocol/protocolVersionis validated server-side; unsupported versions return-32004UnsupportedProtocolVersionErrorwith{supported, requested}data.ttlMs+cacheScopeadded toDiscoverResultper spec PR #2855; defaults tottlMs: 0+cacheScope: "private"under draft (immediate-stale, not shareable) for safe back-compat behavior.Transport
HttpServerTransportOptions.Statelessdefaults totruefor new code.EventStreamStore,SessionMigrationHandler,PerSessionExecutionContext,IdleTimeout,MaxIdleSessionCount, plusISseEventStreamStore/ISessionMigrationHandler) are marked[Obsolete(MCP9005)]— seedocs/list-of-diagnostics.md.Client negotiation
400 Bad Requestit parses the body — modern JSON-RPC errors (-32004,-32003,-32001) surface asMcpProtocolExceptionto the caller; any other JSON-RPC error (legacy-32600,-32601,-32700, parse fail, empty body) → switch to legacy andinitialize. Matches spec PR #2844 ("the fallback MUST NOT be keyed to a single error code").server/discoverfirst.DiscoverResult→ modern.-32004with shaped data → retry withsupported[]. Anything else, or silence past the 5-second probe timeout → fall back toinitializeon the same stdin/stdout (no process restart per spec).AutoDetectingClientSessionTransportnow recognizes JSON-RPC error envelopes in HTTP 400 bodies; adopts StreamableHttp instead of silently falling back to SSE on modern-error responses.Public API
McpClientOptions.MinProtocolVersion : string?— when set, the client refuses to fall back below this version and surfaces a clearMcpExceptioninstead. Useful for strict-modern production code and for tests that want to assert draft-only behavior.What's tested
tests/ModelContextProtocol.Tests/Server/RawStreamConformanceTests.cs— drivesMcpServerdirectly via pairedPipestreams withoutMcpClient. 5 tests coveringserver/discoverfirst, drafttools/callafter no init,-32004on unsupported version, legacyinitializestill works, dual-era dispatch on the same stream.tests/ModelContextProtocol.AspNetCore.Tests/RawHttpConformanceTests.cs— drives the C# server with rawHttpClientagainst in-memory Kestrel. 5 tests covering drafttools/callwith full_meta, rawserver/discover,-32004on unsupportedMCP-Protocol-Versionheader, legacyinitializeon the default (stateless+draft) server, andGET /mcpreturning405when not stateful.tests/ModelContextProtocol.AspNetCore.Tests/DraftHttpFallbackTests.cs): three Kestrel-in-memory cases covering Python-shape (-32600 legacy error), Go-shape (-32004 withsupporteddata), and HeaderMismatch-shape (-32001) on real HTTP. Plus null-id parser tests and HeaderMismatch passthrough test on the in-memory transport.HttpTaskIntegrationTests) now explicitly opt intoStateless = false.d9277e0c:ModelContextProtocol.Tests, net9.0, excluding env-dependentClientIntegrationTests/DockerEverythingServerTestsand the env-quirk-onlyStdioClientTransportTests.EscapesCliArgumentsCorrectlywhich depends on local PATH/CMD.EXE config): 2052 passed / 4 skipped. The full suite reports 72 fails forEscapesCliArgumentsCorrectly, all on a parameterized test that'sgit diff origin/main..HEAD = 0(i.e. unchanged in this PR); CI on main is green.ModelContextProtocol.AspNetCore.Tests, net9.0): 482 passed / 3 failed / 29 skipped. The 3 failures are all pre-existing on main:Server_RejectsInvalidUtf8EncodedHeaderValue,RunConformanceTest_Sep2243("http-custom-headers")(the SEP-2243 finding below), andRunCachingConformanceTest(parallel-run port collision; passes in 1s in isolation).Cross-SDK compatibility (Phase 7 + Phase 11d)
Validated against the other Tier-1 SDKs (TypeScript, Python, Go) in their current
main/ draft-branch states. Wire-trace artifacts kept in this branch's session state.sep-2575-2567-draft-protocol)tools/listsucceedssimple-streamablehttp-stateless(origin/main)-32600, falls back to legacyinitialize, negotiates2025-06-18simple-tool(origin/main)server/discover, gets-32601, falls back toinitializeon the same stdin/stdout, negotiates2025-06-18-32004in 400 body, adopts StreamableHttp, retries legacyinitializewith2025-11-25, lists 10 toolsserver/discovernatively; C# negotiates down to2025-11-25compat/go-draft-forkwith version-string + exported opt-in patches)server/discoverandtools/listsimple-toolclientinitializewith max2025-06-18; C# server (stateless default) serves single-shot legacy sessionα-findings fixed in this PR (post-cross-SDK testing)
ccdd4223simple-streamablehttp-statelessreturnsid: nullon errors before the request id can be determined).00d57f71McpProtocolExceptionper spec PR #2844 (not just modern -32004/-32003) so the connect-time fallback chain can dispatch on the error code.276bde45initialize.3778e00eAutoDetectingClientSessionTransportnow recognizes JSON-RPC error envelopes in HTTP 400 bodies; adopts StreamableHttp instead of silently falling back to SSE.β-findings (peer-SDK issues, informational)
compat/ts-draft) doesn't yet emitMcp-Method/Mcp-Nameheaders (the fix is on a different branch). Closure awaits upstream merge.mainno longer crashes on draft probe, now returns clean JSON-RPC error envelope.origin/mainstill uses2026-06-30version string and unexportedClientSessionOptions.protocolVersion. Documented; patches applied locally for cross-SDK testing only.Conformance suite (Phase 12)
Ran the upstream
@modelcontextprotocol/conformancesuite against the C# SDK. Two tracks:Track A — bump the published npm pin
Bumped
tests/Common/Utils/package.jsonfrom0.1.16→0.2.0-alpha.2(d539e7fd). This activates 5 previously-gated test classes (ClientConformanceTests.RunConformanceTest_Sep2243,ServerConformanceTests.RunConformanceTest_HttpHeaderValidation,ServerConformanceTests.RunConformanceTest_HttpCustomHeaderServerValidation,ServerConformanceTests.RunMrtrConformanceTest,CachingConformanceTests.RunCachingConformanceTest).Because
0.2.0-alpha.2still emits the placeholder wire versionDRAFT-2026-v1(the spec-aligned2026-07-28only landed in unpublishedalpha.3), a wire-version-match gate (HasMatchingDraftWireVersion()intests/Common/Utils/NodeHelpers.cs, commitf3698c71) is ANDed into each draft-onlyHasXxxskip predicate so the 14 draft-scenario rows skip cleanly under the published alpha.2 instead of failing with mismatched-string assertions.Track B — local build of
compat/conformance-draftAssembled a local
compat/conformance-draftbranch inmodelcontextprotocol/conformance(tip50ad0fa) by merging the following SEP-relevant open PRs on top ofmain:#310 (SEP-2549 absence-assert) was skipped — too-deep conflict with main's
RunContextrefactor (PRs #319 / #317 / #321 / #318). Deferred to a follow-up.Installed locally with⚠️ Note:
npm install --no-save H:\modelcontextprotocol\conformance.npm cireverts to pinnedalpha.2; reviewers reproducing locally must re-run the path-install after dependency restore. Flipped 3--spec-version DRAFT-2026-v1references inServerConformanceTests.cs+ 1 inCachingConformanceTests.csto2026-07-28(commitd9277e0c), and renamed 6 tools + 1 prompt inIncompleteResultTools.cs/IncompleteResultPrompts.csto match conformance's rename ofincomplete-result-* → input-required-result-*(mirrors the SDK's MRTRIncompleteResult → InputRequiredResultrename).Outcome (serial run on stateless HTTP):
input-required-result-*scenarios — thetampered-state(HMAC-protected requestState) andcapability-check(per-request capability-aware inputRequest gating) rows are now implemented inConformanceServerand un-skipped — plus bothSep2243.http-{standard,invalid-tool}-headersand CachingClientConformanceTests.RunConformanceTest_Sep2243("http-custom-headers")— not a C# bug: the harness scenario putsx-mcp-headeron atype: "number"parameter, which SEP-2243 forbids (the spec's earlier self-contradiction was resolved againstnumberupstream in modelcontextprotocol/modelcontextprotocol#2863). The C# client correctly excludes the malformed tool, so noMcp-Param-*headers are sent. Stays skipped until a conformant package ships; tracked in #1655.Modes: only stateless HTTP exercised so far. Stateful HTTP and stdio modes deferred to a follow-up — Track B already validates draft conformance on the most important transport, and the published-pin gate (Track A) ensures CI on pinned alpha.2 keeps working without local conformance-build dependencies.
Parallel-run flakiness:
CachingConformanceTestshows a port-pool collision (port 301x range) under parallel xUnit collections; passes consistently in isolation in under 2 s. Documented as known-flaky-in-parallel; the test suite was not switched to serial.Out of scope
InitializeRequestParams,Mcp-Session-Idconstants,PingRequestParams, …) are still current in2025-11-25and remain un-obsoleted in this PR.2024-11-05) transport stays mapped under/sseand/messagefor legacy back-compat.Resolved during review (originally punted, now done in this PR)
-32001) validation — the server already compared the HTTPMCP-Protocol-Versionheader against the body_meta.io.modelcontextprotocol/protocolVersion, but threw-32602 InvalidParams. A draft client'sserver/discoverprobe treats any non-modern error (includingInvalidParams) as a legacy signal and falls back toinitialize, so a modern server detecting a genuine mismatch was misread as legacy. It now emits-32001 HeaderMismatch— the code the client recognizes as a modern-server signal — with aRawHttpConformanceTestsregression.input-required-result-tampered-stateHMAC +input-required-result-capability-checkper-request gating) are now implemented inConformanceServerand un-skipped (14/14RunMrtrConformanceTestpass against the localcompat/conformance-draftbuild), with in-process wire-level regressions inMrtrProtocolTests.Punted to follow-up PRs
mcp-session-idheader: server returns400rather than silently ignoring it. This is the deliberate choice — rejecting surfaces the client bug, and SEP-2567 removes the header from the draft revision so "ignore" is a robustness option, not a requirement. Not tracking (closed Draft server: validate body/header protocolVersion mismatch and no-op stray Mcp-Session-Id #1654).IsStatefulSession()gate review inMcpServerImpl.IsMrtrSupported(the existing TODO from the MRTR PR) — tracked in Draft follow-up tidy-ups: configurable stdio probe timeout + IsStatefulSession/IsMrtrSupported gate review #1652.Mcp-Param-*header emission forx-mcp-headerontype: "number"parameters: no change — SEP-2243 forbidsnumber(resolved upstream in (chore): sep-to-spec consistency pass modelcontextprotocol#2863), so the harnesshttp-custom-headersscenario is the non-conformant party. Tracked in Upstream conformance harness: http-custom-headers tests float x-mcp-header (forbidden by SEP-2243) #1655 (re-enable the xunit case once upstream ships a conformant package).#310) after the conformanceRunContextrefactor settles, (3) file upstream issue for missingserver/discoverstandalone scenario — tracked in Conformance Track B: stateful HTTP/stdio modes, SEP-2549 absence-assert, remaining MRTR scenarios #1653.