Skip to content

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

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

Add HttpServerSessionMode for hybrid stateful/stateless HTTP serving#1796
saicharanpardhu wants to merge 3 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

Undefined HttpServerSessionMode values are rejected through options validation rather than silently falling through to hybrid behavior.

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 is now obsolete (MCP9008)

bool Stateless can't express the third state, so it's obsoleted and reduced to a compatibility proxy over SessionMode, exactly as requested:

  • 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 were migrated off the obsolete property (the build runs with TreatWarningsAsErrors).

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. HttpMcpServerBuilderExtensionsTests verifies undefined enum values fail options validation.

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 — documents MCP9008 and rewords MCP9006.
  • 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: 611 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 and others added 3 commits August 5, 2026 12:46
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>
@saicharanpardhu
saicharanpardhu marked this pull request as ready for review August 5, 2026 21:26
```csharp
builder.Services.AddMcpServer()
.WithHttpTransport(o => o.Stateless = true)
.WithHttpTransport(o => o.SessionMode = HttpServerSessionMode.Stateless)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we've made the change to default Stateless = true, I wonder if we should update our examples/samples to just call .WithHttpTransport() and not explicitly configure the SessionMode. The reason we originally set this explicitly in all the samples is we new we were going to change the default in 2.0. What do you think @jeffhandley?

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

2 participants