Add draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567)#1610
Draft
halter73 wants to merge 25 commits into
Draft
Add draft protocol support: sessionless + handshake-less (SEP-2575 + SEP-2567)#1610halter73 wants to merge 25 commits into
halter73 wants to merge 25 commits into
Conversation
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>
…rsion to 2026-07-28 - Drop ExperimentalProtocolVersion from McpClientOptions/McpServerOptions and use ProtocolVersion == McpSessionHandler.DraftProtocolVersion as the draft predicate. - McpHttpHeaders.DraftProtocolVersion and McpSessionHandler.DraftProtocolVersion are now `2026-07-28` (matches the published spec) instead of `DRAFT-2026-v1`. - Server always advertises draft via SupportedProtocolVersions; ConfigureDiscover no longer takes an opt-in flag. - Drop _serverHasExperimental machinery from DraftConnectionTests; the test class now relies on the unconditional draft support. - Skip Mrtr_MixedExceptionAndAwaitStyle(experimentalClient: True) over Streamable HTTP; the await-style path needs session affinity and draft HTTP is sessionless. Stdio coverage stays in DraftProtocolBackcompatTests. - Sweep the remaining DRAFT-2026-v1 / 2026-06-XX literals across docs and tests. 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>
…colVersion validation, and raw stream conformance tests Phase 2-5 of the draft (2026-07-28) rollout: - McpClientImpl.ConnectAsync: tighten the draft probe fallback to match the spec's stdio rules. Apply a 5-second probe timeout (bounded by InitializationTimeout), broaden the catch to treat any McpProtocolException OR probe-timeout as a legacy-server signal, and special-case the two modern-server JSON-RPC errors (-32004 retries with supported[]; -32003 surfaces). Honor MinProtocolVersion before falling back to legacy initialize. - McpClientOptions: add MinProtocolVersion public string? with XML docs. Setting this to McpSessionHandler.DraftProtocolVersion disables the automatic legacy fallback. - McpServerImpl.CreateDraftStateSyncFilter: reject any per-request _meta/io.modelcontextprotocol/protocolVersion that is not in SupportedProtocolVersions with UnsupportedProtocolVersionException (-32004). The HTTP handler already validated the MCP-Protocol-Version header; this closes the corresponding gap for stdio/Stream and for HTTP bodies where the header is absent. - HttpTaskIntegrationTests: Tasks pin per-session state into the in-memory store, so the tests require stateful HTTP. With Stateless=true the default after Phase 1, the tests would deadlock on the second request because the task ID wasn't visible to the new stateless server invocation. Opt back into stateful mode with WithHttpTransport(options => options.Stateless = false). - New tests/ModelContextProtocol.Tests/Server/RawStreamConformanceTests.cs: drive McpServer directly via paired Pipe streams without going through McpClient. Hand-writes JSON-RPC messages and asserts on the exact bytes the server emits. Covers server/discover -> supportedVersions[], draft tools/call without initialize, -32004 with data.supported on unsupported version, legacy initialize on the same dual-era server, and a mixed Discover->Initialize->ToolsCall sequence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 5b + 6 of the SEP-2575/SEP-2567 work. - tests/ModelContextProtocol.AspNetCore.Tests/RawHttpConformanceTests.cs: 5 new tests that drive the C# server directly with hand-crafted HttpClient requests, no McpClient involvement. Covers draft tools/call with full _meta, server/discover, -32004 on unsupported MCP-Protocol-Version, legacy initialize on a default (stateless+draft) server, and GET returning 405 when not stateful. - docs/list-of-diagnostics.md: add MCP9005 row describing the stateful Streamable HTTP options as back-compat-only knobs since the draft revision is sessionless by default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bb9f572 to
30782f6
Compare
Closes the doc gap that was left dangling in the prior commits. - src/ModelContextProtocol.Core/McpSession.cs: add public 'LatestProtocolVersion' and 'DraftProtocolVersion' constants so user code can opt into a specific revision without typing string literals. The two constants forward to the existing internal values on 'McpSessionHandler'. - src/ModelContextProtocol.Core/Client/McpClientOptions.cs: retarget XML cref + example to 'McpSession.DraftProtocolVersion' (the new public constant) instead of the internal 'McpSessionHandler.DraftProtocolVersion'. - docs/concepts/stateless/stateless.md: flip every stale 'Stateless = false is the default' claim, rewrite the 'Why isn't stateless the default?' note, mark MRTR as merged (no longer 'proposed'), update the property reference table with 'true' default and 'MCP9005' callouts on stateful-only knobs, and add a new 'The 2026-07-28 draft revision' subsection covering wire-level changes, server stateless routing, client probe-and-fallback negotiation (HTTP + stdio), and 'MinProtocolVersion' opt-out. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Brings in 9 commits including af6fcff (InitializeMeta), ed19286 (SEP-2243 header standardization), a4157f3 (skip IdleTrackingBackgroundService timer when stateless), and assorted fixes. Conflict resolution in McpClientImpl.cs: kept the draft probe-timeout structure from this branch while threading the new 'Meta = _options.InitializeMeta' through to PerformLegacyInitializeAsync. Merge fallout fixes: - tests/.../HttpHeaderConformanceTests.cs: five MCP-Protocol-Version header values updated from the stale 'DRAFT-2026-v1' placeholder to the spec value '2026-07-28' so the new SEP-2243 tests actually exercise the draft path on this branch. - tests/.../HttpMcpServerBuilderExtensionsTests.cs: 'IdleTrackingBackgroundService_StartsTimer_WhenStateful' now opts into 'Stateless = false' (under MCP9005 suppression) so it exercises the stateful timer path, since the default is now 'Stateless = true' on this branch. - tests/.../RawStreamConformanceTests.cs: 'ReadLineAsync(cancellationToken)' replaced with '.ReadLineAsync().WaitAsync(...)' to fix a pre-existing net472 build error that wasn't caught earlier (net472 'StreamReader.ReadLineAsync' has no cancellation overload). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a client configured with ProtocolVersion = DraftProtocolVersion probed
a legacy server with server/discover and fell back to the initialize handshake,
PerformLegacyInitializeAsync rejected the server's response because the post-
handshake validation compared _options.ProtocolVersion ('2026-07-28') against
the legacy server's negotiated version (e.g. Python's '2025-06-18'). The strict
comparison was correct for legacy explicit pinning but wrong for the draft-
fallback path, where the spec requires the client to accept whatever supported
version the legacy server advertises.
Now we only require an exact match when the user pinned a legacy (non-draft)
version; otherwise we accept any version in SupportedProtocolVersions. We also
enforce MinProtocolVersion against the negotiated response in case the server
downgrades further than the version we requested.
Adds DraftProtocolFallbackTests covering:
* -32601 MethodNotFound fallback with version downgrade
* -32602 InvalidParams fallback
* MinProtocolVersion refuses fallback below the configured minimum
* Legacy explicit-pin still requires exact version match
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The ping RPC was removed in the draft 2026-07-28 revision (SEP-2575). TypeScript and Go SDKs both reject ping under draft; C# was responding to ping unconditionally with the comment 'must always be handled', which was a holdover from the older spec. The built-in handler now throws McpProtocolException(MethodNotFound) for any per-request protocol version >= DraftProtocolVersion, falling back to the session-level NegotiatedProtocolVersion when the per-request _meta is absent (legacy sessions). Liveness on draft sessions belongs to transport- and request-level timeouts, not a removed MCP RPC. Adds PingProtocolGatingTests covering: * Ping under draft returns MethodNotFound * Ping under legacy still succeeds Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Jun 8, 2026
Brings in PR #1579 SEP-2663 Tasks (squash dbb7a20), SEP-990 Enterprise Managed Authorization (8202bcc), SEP-2243 alignment (ed19286), ttlMs renames in McpSessionHandler (711e5bb), and several quality-of-life fixes that landed between the previous merge and today. Conflict resolutions: - src/ModelContextProtocol.Core/McpJsonUtilities.cs: keep both sides' JsonSerializable additions. Our draft additions (JsonElement, Implementation, ClientCapabilities, ServerCapabilities, LoggingLevel) coexist with origin/main's IDictionary<string,object> addition. - src/ModelContextProtocol.Core/Protocol/NotificationMethods.cs: take origin/main's renamed TaskStatusNotification value ('notifications/tasks', formerly 'notifications/tasks/status') and the updated XML docs from PR #1579. Keep all our draft additions (RelatedTaskMetaKey, SubscriptionsAcknowledgedNotification, ProtocolVersionMetaKey, ClientInfoMetaKey, ClientCapabilitiesMetaKey, LogLevelMetaKey, SubscriptionIdMetaKey). - tests/ModelContextProtocol.AspNetCore.Tests/HttpTaskIntegrationTests.cs: removed. Our pre-rebase tweak to the old SEP-1686 file is moot now that PR #1579's reimplementation deleted it; the new task tests live elsewhere. PR #1579 author addressed the reconciliation items predicted in the preview-merge analysis: '17f95f79 Fix _meta' nests tasks opt-in inside the SEP-2575 capabilities envelope (preview commit 89295fb3 no longer needed); '8b47086d Address PR feedback' fixes Failed task payload shape + JsonDocument lifetime (preview commit 8817c9fc no longer needed); '0b8944f9 Address PR feedback: docs' adds the IMcpTaskStore lifetime/ stateless docs (preview commit 072222db no longer needed). The only preview reconciliation that may still be required is gating per-request capability merge to stateful sessions only (preview commit 8b95d2ca), which is evaluated separately after this merge by re-running StatelessServerTests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR #1579's GetMetaWithTaskCapability writes a partial SEP-2575 capabilities envelope (only `extensions.io.modelcontextprotocol/tasks`) on every `tools/call`, regardless of negotiated protocol version. The server's `CreateDraftStateSyncFilter` was treating the envelope as authoritative and overwriting the session-scoped `_clientCapabilities` with the partial value - wiping out whatever the initialize handshake had captured. Most visibly, a legacy client that handshook with `Elicitation = new()` would lose elicitation support the moment it issued a tools/call, and the back-compat MRTR resolver would then fail with "Client does not support elicitation requests". Switch the per-request synchronization to a defensive merge that preserves fields the envelope leaves null and additively merges extension keys. Gate the merge behind `IsStatefulSession()` so per-request envelope state doesn't leak into `_clientCapabilities` on stateless HTTP sessions (where StatelessServerTests rely on the null invariant to surface "X is not supported in stateless mode" errors). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements SEP-2549 "TTL for List Results", which lets servers attach optional caching freshness hints to the five cacheable result types: tools/list, prompts/list, resources/list, resources/templates/list, and resources/read. Protocol changes: - Add ICacheableResult with TimeToLive (serialized as integer-millisecond ttlMs) and CacheScope (serialized as cacheScope). - Add the CacheScope enum (public, private) with lowercase wire values. - Implement the interface on the five cacheable result types. - Register CacheScope for source-generated serialization. Both fields are optional and omitted when unset, so the change is fully backward compatible and requires no capability negotiation. The SDK propagates the values without consuming them. Robustness and security: - ttlMs deserialization clamps out-of-range, fractional, and overflowing values (including positive and negative infinity) to TimeSpan.MinValue or MaxValue instead of throwing, so a malformed or hostile hint cannot break reading of the enclosing result. The shared TimeSpanMillisecondsConverter uses the non-throwing TryGetDouble and clamps by token sign, giving identical behavior on .NET and on .NET Framework (whose number parser reports failure on overflow rather than returning infinity). - cacheScope deserialization tolerates unknown or future values by mapping them to null (treated as the public default) instead of failing the whole result, and matches the known values case-insensitively so a mis-cased "private" is honored rather than silently downgraded to public. Tests: - Serialization, round-trip, omission, and clamping edge cases for ttlMs. - Unknown, partial, and case-insensitive cacheScope handling. - Per-page independence of caching hints for pagination. - End-to-end propagation of hints from server to client. - Regression coverage for the shared converter used by McpTask ttl and pollInterval. - Caching conformance scenario wiring, gated to the conformance build that provides it. Verified across net8.0, net9.0, net10.0, and net472, and under Native AOT publish with no trimming or AOT warnings.
- CacheScopeConverter.Read now consumes non-string tokens with reader.Skip() before returning null. Previously an object or array value for cacheScope left the reader mispositioned and threw "read too much or not enough", breaking deserialization of the whole result. Added object and array cases to the tolerant-deserialization test. - GetInstalledConformanceVersion no longer calls EnsureNpmDependenciesInstalled. The version check backs Theory skip gates and must be side-effect-free; it now returns null when the conformance package is absent. The actual scenario run path still restores npm dependencies via ConformanceTestStartInfo.
PR #1579 (SEP-2663) replaced the SEP-1686 McpTask type with CreateTaskResult and switched its ttl/pollInterval to bare `long?` properties, so the TimeSpanMillisecondsConverter no longer has a second consumer. The shared regression suite cherry-picked from PR #1623 references the now-removed McpTask type and stops compiling. The converter's clamp-instead-of-throw branches are still fully exercised by CacheableResultTests (oversized, large-negative, +Inf, -Inf round-trips), so no coverage is lost. Drop the file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SEP-2549 (PR #1623 cherry-picked in 51571c6) added the ICacheableResult contract with tlMs and cacheScope to the five list/read results, but the spec was subsequently amended by spec PR #2855 to also require both fields on server/discover responses. Implement that on DiscoverResult and emit safe defaults from the built-in handler so existing servers keep their "do not cache" behavior while remaining wire-compliant under draft. Changes: - `DiscoverResult` now implements `ICacheableResult` and carries `TimeToLive`/`CacheScope` properties with the same wire shape as the list/read results. - `ICacheableResult` xmldoc updated to mention `server/discover` alongside the existing list/read implementers. - `McpServerImpl.ConfigureDiscover` emits `ttlMs: 0` + `cacheScope: "private"` (immediately stale, not shareable) on the built-in handler. The values match halter73's design call on PR #1623: the safest defaults preserve today's behavior without requiring server authors to opt-in to caching, while still satisfying the wire requirement under draft. - `RawHttpConformanceTests.ServerDiscover_RawPost_ReturnsDiscoverResult` and `RawStreamConformanceTests.ServerDiscover_ReturnsSupportedVersionsIncludingDraft` now assert the fields are emitted with the expected values. - New `DiscoverResultCacheableTests` exercises the round-trip on `DiscoverResult` (the existing parameterized `CacheableResultTests` cannot cover it because `DiscoverResult` has required CLR properties that block reflection-based `Activator.CreateInstance`). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Spec PR #2759 promotes params._meta to required on tools/list, resources/list, resources/templates/list, prompts/list, and server/discover under draft. The C# client already injects the SEP-2575 envelope on every outgoing request via McpSessionHandler.InjectDraftMeta when the session has negotiated draft; this test file pins that behavior so future refactors cannot silently regress the envelope on list-style requests when the caller passes no params/RequestOptions. Six tests under ClientServerTestBase: - DraftClient_ListTools_NoOptions_EmitsRequiredMeta - DraftClient_ListPrompts_NoOptions_EmitsRequiredMeta - DraftClient_ListResources_NoOptions_EmitsRequiredMeta - DraftClient_ListResourceTemplates_NoOptions_EmitsRequiredMeta - DraftClient_ServerDiscover_EmitsRequiredMeta - LegacyClient_ListTools_DoesNotEmitDraftMeta (negative control) The four list-method tests attach server-side request filters that capture request.Params?.Meta and assert the three required SEP-2575 keys (protocolVersion, clientInfo, clientCapabilities) are present plus that protocolVersion matches the negotiated draft revision. The server/discover test asserts round-trip success — if the client had omitted _meta, the server would have rejected with -32602/-32003 rather than returning a DiscoverResult; the wire-level shape is covered by the existing RawHttp/RawStream conformance tests. The legacy negative-control test pins that the draft-meta injector is gated correctly on the negotiated protocol version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The probe-error fallback comment in McpClientImpl.cs listed -32601/-32602/-32700 as the exemplar error codes that trigger the legacy-server fallback. Spec PR #2844 (June 3 2026) clarifies that the fallback MUST NOT be keyed to a single error code: any non-modern JSON-RPC error or probe timeout means legacy. Our code already does the spec-conformant thing — the catch block is keyed on the McpProtocolException base class, not specific error codes — but the enumeration in the comment was misleading. Replace it with text that makes the spec-conformant intent explicit and notes that the two modern-server signals (-32004 UnsupportedProtocolVersion, -32003 MissingRequiredClientCapability) are caught upstream and never reach this branch. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JSON-RPC 2.0 §5.1 requires the server to respond with `id: null` when an
error occurs before the request id can be determined (e.g. parse error, invalid
request, or transport-level rejection). The `RequestId.Converter` previously
threw on a `null` token and `JsonRpcMessage.Converter` rejected responses
with both `error` and a null `id`, so any peer that legitimately produced
that shape (e.g. Python's `simple-streamablehttp-stateless` on a 400) was
surfaced as `HttpRequestException` instead of the structured JSON-RPC error
that the SEP-2575 fallback logic needs.
Cross-SDK testing against Python's `main` branch reproduces the shape:
`{""jsonrpc"":""2.0"",""id"":null,""error"":{""code"":-32600,...}}`.
Accept that shape so the draft-to-legacy fallback path can recognize it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per spec PR #2844 (HTTP backwards compatibility for SEP-2575), a 400 Bad Request that carries a JSON-RPC error envelope means the peer is signalling something application-level about the request shape (not a transport failure). The connect-time fallback path needs to see the structured exception so it can decide between retrying with a server-advertised version (-32004), surfacing a capability gap to the caller (-32003, -32001), or falling back to legacy `initialize` for any other JSON-RPC error code (e.g. -32600 from a legacy server that doesn't understand the draft `_meta` envelope). Previously only the three modern draft error codes were surfaced; everything else became `HttpRequestException` and bypassed the fallback chain. Now any JSON-RPC error in a 400 body becomes `McpProtocolException`, plus the three modern codes are surfaced for non-400 status codes for robustness (servers occasionally pair them with other 4xx codes). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
McpClientImpl.ConnectAsync's draft probe path catches McpProtocolException to fall back to a legacy `initialize` exchange. The handler had passthrough cases for the two modern draft error codes whose semantics are not ""I'm a legacy server"" (UnsupportedProtocolVersion -32004 and MissingRequiredClientCapability -32003) but was missing the third: HeaderMismatch -32001 from SEP-2243. That code means the body's `_meta.io.modelcontextprotocol/protocolVersion` does not match the `MCP-Protocol-Version` HTTP header — which is a client-side bug that should surface to the caller, not a signal to retry with `initialize`. Add the third passthrough so all three modern draft codes propagate, with an in-memory transport regression test that exercises the path against a server which only returns `-32001`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AutoDetectingClientSessionTransport's job is to pick between StreamableHttp (post-SEP-2243) and legacy SSE based on the server's response to the first POST. The detection logic only checked the HTTP status code, so a draft server returning `400 Bad Request` with a JSON-RPC error body (e.g. `-32004` UnsupportedProtocolVersion or `-32600` from a legacy server that didn't understand the draft `_meta` envelope) was being treated as ""this isn't StreamableHttp"" — and the transport silently fell back to SSE, breaking fallback negotiation against any HTTP/1.1 peer that follows the spec. Detect a JSON-RPC error envelope in the 400 body, adopt StreamableHttp, then re-throw the structured exception so McpClientImpl can dispatch on the error code (retry with advertised version, surface capability gap, or fall back to `initialize`). Use a deferred throw guarded by `catch when (ActiveTransport is null)` to preserve transport ownership across the deferred error path. Cross-SDK validation: against vanilla Go SDK `origin/main` HTTP everything server, default `--http-mode autodetect` now successfully adopts StreamableHttp, recognizes `-32004` with `data.supported`, retries with `2025-11-25`, and lists 10 tools. Against Python `main`'s `simple-streamablehttp-stateless`, the same flow recognizes the `-32600` returned from the draft probe, adopts StreamableHttp, and falls back to `initialize` with `2025-06-18` to complete the handshake. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The latest published prerelease (0.2.0-alpha.2, Jun 3 2026) adds gated scenarios for SEP-2243 (HTTP headers), SEP-2549 (caching), and SEP-2322 (MRTR/incomplete result). Pinning to it makes the 14 currently-skipped scenarios available to HasSep2243Scenarios()/HasCachingScenario()/HasMrtrScenarios(). However, alpha.2 still ships the placeholder wire string 'DRAFT-2026-v1' for draft scenarios, while this SDK (and the conformance main branch, awaiting alpha.3 publish) emits the spec-ratified '2026-07-28'. Without an additional gate, the 14 newly-activated scenarios all fail with mismatched draft wire strings. The next commit tightens the gates with a HasMatchingDraftWireVersion() guard so they remain skipped on alpha.2 and activate on alpha.3+ (or on a local build of conformance main installed via 'npm install --no-save'). Baseline test surface unchanged: 126 pass / 14 skip / 2 pre-existing skip in ConformanceTests on net9.0 (one pre-existing failure in HttpHeaderConformanceTests.Server_RejectsInvalidUtf8EncodedHeaderValue predates this change). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous gates activated draft-only scenarios as soon as the conformance package version reached 0.2.0. That works for conformance >= 0.2.0-alpha.3 (or a local build of main) where the bundled DRAFT_PROTOCOL_VERSION constant matches this SDK's value, but breaks under 0.2.0-alpha.2 because alpha.2 still ships the placeholder 'DRAFT-2026-v1' wire string while this SDK only accepts the ratified '2026-07-28'. Add HasMatchingDraftWireVersion() that greps the bundled node_modules/@modelcontextprotocol/conformance/dist/index.js for this SDK's McpHttpHeaders.DraftProtocolVersion (the bundle is minified so we can't grep the constant name, but the literal version string survives bundling and is specific enough to be reliable). AND it into the three gates: HasSep2243Scenarios(), HasCachingScenario(), HasMrtrScenarios(). Also unify HasMrtrScenarios() with HasSep2243Scenarios()/HasCachingScenario(): read the installed version from node_modules instead of the pinned version from package.json. This lets a local 'npm install --no-save <path-to-conformance>' activate MRTR scenarios the same way it already activates SEP-2243/caching. Under 0.2.0-alpha.2 (this PR's pin), the 14 gated draft scenarios all SKIP cleanly instead of failing on wire-string mismatch. Once 0.2.0-alpha.3 publishes (or a local main build is installed), the gates auto-activate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nce main The conformance suite's main branch (post 0.2.0-alpha.2) made two breaking changes that our pinned wire-name / spec-version contracts had to follow: 1. `DRAFT-2026-v1` wire literal flipped to `2026-07-28` (commit 6f83abf in the conformance repo). 2. `incomplete-result-*` server scenarios renamed to `input-required-result-*` and extended from 8 to 14 scenarios (PR #262 in the conformance repo, follow-on to our own MRTR rename in PR #1458 where `IncompleteResult` became `InputRequiredResult`). Track A (the published 0.2.0-alpha.2 pin) is unaffected and continues to skip these tests via the wire-version-match gate introduced in `f3698c71`. Track B (a private install of the conformance `main` branch with the four merged sessionless/MRTR/error-code/tasks PRs) now exercises 12 of the 14 MRTR scenarios end-to-end against this SDK's `ConformanceServer`. Changes in this commit: - `IncompleteResultTools.cs` and `IncompleteResultPrompts.cs` — rename 6 tool wire names plus 1 prompt wire name from `test_incomplete_result_*` (and `test_tool_with_elicitation`) to `test_input_required_result_*` so the conformance scenarios can find them. The C# class names are intentionally left as `Incomplete*` to keep the diff minimal; the comment block above `RunMrtrConformanceTest` documents the asymmetry. - `ServerConformanceTests.cs` — replace the 8-row MRTR theory with the 14-row theory matching the new conformance scenario set; flip the two `--spec-version DRAFT-2026-v1` references to `2026-07-28`; mark two scenarios skipped that require server-side patterns the `ConformanceServer` tools don't yet implement (HMAC-signed requestState for `input-required-result-tampered-state`; per-request capability gating for `input-required-result-capability-check`). Those scenarios are still feature-flagged behind the wire-version gate so they only attempt to run when the installed conformance package speaks `2026-07-28`. - `CachingConformanceTests.cs` — flip the `--spec-version` reference and rewrite the doc remarks to explain the wire-version-match gate. Validation: `dotnet test` over the targeted slice (5 conformance test classes) under serial run reports 17 pass / 1 fail / 2 skip against the private `compat/conformance-draft` build. The single failure (`Sep2243.http-custom-headers`) is a pre-existing C# SDK client bug exposed by the conformance-pin bump: the client sends no `Mcp-Param-*` headers when the server's tool advertises `x-mcp-header` annotations. The bug exists on `origin/main` (`git diff origin/main..HEAD -- src/ModelContextProtocol.Core/Client` on these files is empty) and is unrelated to the SEP-2575 / SEP-2567 draft work in this PR. Tracking as a follow-up. 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, bothSep2243.http-{standard,invalid-tool}-headers, and CachingClientConformanceTests.RunConformanceTest_Sep2243("http-custom-headers")— pre-existing C# client bug onorigin/main(git diff origin/main..HEADonMcpClientImpl.cs/StreamableHttpClientSessionTransport.csis empty). C# client doesn't emitMcp-Param-*headers when tools declarex-mcp-headerannotations. Out of scope for this PR; tracking as a follow-up.input-required-result-tampered-state(needs HMAC-protected requestState pattern) andinput-required-result-capability-check(needs per-request capability-aware inputRequest gating). Both are advanced scenarios that would require new server-side patterns and are outside this PR's scope; annotated withSkip = "..."and rationale.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.Punted to follow-up PRs
-32001) validation: server should compare HTTPMCP-Protocol-Versionagainst body_meta.protocolVersion. Currently not validated.mcp-session-idheader: server returns400instead of silently ignoring (spec says draft is sessionless so the header should be a no-op).IsStatefulSession()gate review inMcpServerImpl.IsMrtrSupported(the existing TODO from the MRTR PR).Mcp-Param-*header emission for tools declaringx-mcp-headerannotations (the SEP-2243 finding in Track B above; pre-existing onorigin/main).#310) after the conformanceRunContextrefactor settles, (3) implement the two skipped MRTR scenarios (tampered-stateHMAC +capability-checkper-request gating), (4) file upstream issue for missingserver/discoverstandalone scenario.