Skip to content

Add HttpServerSessionMode for hybrid stateful/stateless HTTP serving - #1796

Open
saicharanpardhu wants to merge 4 commits into
modelcontextprotocol:mainfrom
saicharanpardhu:hybrid-session-mode
Open

Add HttpServerSessionMode for hybrid stateful/stateless HTTP serving#1796
saicharanpardhu wants to merge 4 commits into
modelcontextprotocol:mainfrom
saicharanpardhu:hybrid-session-mode

Conversation

@saicharanpardhu

@saicharanpardhu saicharanpardhu commented Aug 5, 2026

Copy link
Copy Markdown

Closes #1777.

Implements the design @halter73 described, finishing the work from the draft in saicharanpardhu#1.

Problem

Starting with 2026-07-28, Streamable HTTP has no sessions (SEP-2567 removed Mcp-Session-Id, SEP-2575 removed the initialize handshake). Today an ASP.NET Core server must pick one era for the whole endpoint:

  • Stateless = true — no sessions for anyone, so legacy clients lose unsolicited notifications, resource subscriptions, and server-to-client requests.
  • Stateless = false2026-07-28 requests are refused with -32022 UnsupportedProtocolVersion to force a downgrade, so servers can't adopt the new revision until every client has migrated (or is willing to downgrade).

Change

New three-value HttpServerSessionMode on HttpServerTransportOptions:

builder.Services.AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.SessionMode = HttpServerSessionMode.StatefulForInitializeClients;
    });
Mode initialize clients (2025-11-25 and earlier) 2026-07-28 and later clients
Stateless (default) Served statelessly Served statelessly
Stateful Full session Refused with -32022 so dual-path clients downgrade
StatefulForInitializeClients Full session Served statelessly on the same endpoint

In hybrid mode the effective mode is decided per request, so lifetimes follow the request rather than the endpoint:

  • 2026-07-28 requests resolve from HttpContext.RequestServices with request scoping disabled; ConfigureSessionOptions and RunSessionHandler run per request.
  • initialize-handshake sessions resolve from the application provider with per-request scoping; ConfigureSessionOptions and RunSessionHandler run once per session.
  • GET/DELETE stay mapped, legacy SSE stays permitted, and idle tracking keeps running — but only legacy sessions can use them; 2026-07-28 GET/DELETE get 405.

Stateless remains a convenience proxy

bool Stateless remains the convenient way to select the two most common modes, while SessionMode exposes the hybrid value:

  • Stateless = trueSessionMode = Stateless; Stateless = falseSessionMode = Stateful
  • reading Stateless returns true only for SessionMode == Stateless (hybrid reads as false)
  • assigning both does not throw — they're the same underlying value, so the last assignment wins

Internal call sites, samples, docs samples, and tests use SessionMode explicitly so their intended behavior is clear; existing applications can continue using the bool without an obsolete warning.

Tests

July2026ProtocolHybridSessionModeTests covers the pre-merge checklist from the issue:

Test Verifies
ModernAndLegacyClients_ShareOneEndpoint_AndModernDoesNotDowngrade Both eras work against one app instance; the default modern client negotiates 2026-07-28 instead of downgrading
LegacyClient_OnHybridServer_StillSupportsServerToClientElicitation Legacy sessions keep server-to-client requests
ModernRequests_UseRequestScopedServices_WhileLegacySessionsUseApplicationServices Modern requests use stateless DI lifetimes; legacy sessions stay stateful
ConfigureSessionOptions_RunsPerRequestForModernClients_AndOncePerSessionForLegacyClients ConfigureSessionOptions lifetimes match the per-request mode
RunSessionHandler_RunsPerRequestForModernClients_AndOncePerSessionForLegacyClients RunSessionHandler follows the same per-request vs. per-session lifetime
ModernPost_DoesNotMintSessionId_WhileLegacyInitializeDoes No session ID for 2026-07-28
ModernPost_IgnoresMcpSessionIdHeader A stray Mcp-Session-Id doesn't attach a modern request to a session
LegacyGetAndDelete_RemainAvailable_WhileModernGetAndDeleteReturn405 Legacy GET/DELETE still work while modern ones return 405

HttpServerTransportOptionsTests covers the StatelessSessionMode proxy semantics, including that assigning both doesn't throw and the last assignment wins.

Docs

  • docs/concepts/stateless/stateless.md — new "Hybrid mode (sessions for initialize clients only)" section, SessionMode in the property reference, and all samples/prose migrated.
  • docs/concepts/mrtr/mrtr.md, elicitation.md, sampling.md, roots.md — updated the statements that a stateful HTTP endpoint always refuses 2026-07-28; hybrid mode serves it statelessly (so those clients use MRTR while legacy sessions keep the initialize-era flows).
  • docs/list-of-diagnostics.md — rewords MCP9006 for per-request hybrid behavior.
  • Complete ASP.NET Core snippets import ModelContextProtocol.AspNetCore, and references to “both session modes” now distinguish the three SessionMode configurations from the two effective request behaviors.

Validation

  • dotnet build — 0 warnings, 0 errors
  • dotnet test — all targets pass:
    • Analyzers: 57 passed
    • ASP.NET Core: 610 passed, 30 skipped on each of .NET 8, .NET 9, and .NET 10
    • Core: 2355 passed, 6 skipped on each of .NET 8, .NET 9, and .NET 10; 2054 passed, 282 skipped on .NET Framework 4.7.2

Note

This pull request description and the accompanying changes were drafted with GitHub Copilot.

@saicharanpardhu
saicharanpardhu marked this pull request as ready for review August 5, 2026 21:26
Comment thread docs/concepts/completions/completions.md
Comment thread src/ModelContextProtocol.AspNetCore/HttpServerTransportOptionsValidator.cs Outdated
Comment thread src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs Outdated
saicharanpardhu and others added 4 commits August 10, 2026 14:49
Introduces `HttpServerSessionMode` so a single Streamable HTTP endpoint can
serve `initialize`-handshake clients with full sessions while serving
`2026-07-28` and later clients statelessly.

- Add `HttpServerSessionMode { Stateless, Stateful, StatefulForInitializeClients }`
  and `HttpServerTransportOptions.SessionMode` (defaults to `Stateless`).
- Obsolete `HttpServerTransportOptions.Stateless` (MCP9008) and keep it as a
  compatibility proxy over `SessionMode`: `true` maps to `Stateless`, `false`
  maps to `Stateful`, hybrid reads as `false`. Assigning both is allowed and the
  last assignment wins.
- Decide the effective mode per request in `StreamableHttpHandler`:
  `2026-07-28` requests are refused only in `Stateful` mode, and
  `StartNewSessionAsync`/`CreateSessionAsync` now take an explicit
  `serveStatelessly` flag so DI and callback lifetimes follow the request.
- Keep GET/DELETE endpoints mapped, legacy SSE permitted, and idle tracking
  running in hybrid mode.

Fixes modelcontextprotocol#1777

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reject undefined HttpServerSessionMode values through options validation,
cover RunSessionHandler lifetimes in hybrid mode, and repair documentation
examples and terminology for the three configuration values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the HttpServerTransportOptions validator, its DI registration, and the
undefined-enum regression test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Retain the bool as shorthand for the Stateless and Stateful session modes,
while SessionMode remains available for selecting hybrid behavior. Remove the
MCP9008 diagnostic and related obsolete documentation.

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.

Support hybrid MCP server serving both stateful legacy clients and session less 2026-07-28 clients simultaneously

3 participants