Default draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567)#1610
Open
halter73 wants to merge 11 commits into
Open
Default draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567)#1610halter73 wants to merge 11 commits into
halter73 wants to merge 11 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
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.