The MCP C# SDK hybrid sessions option solves an awkward upgrade boundary: some clients still use the 2025-11-25
initialize handshake and depend on sessions, while clients on 2026-07-28
expect every HTTP request to stand alone. I want both groups to reach one ASP.NET Core endpoint without making modern clients downgrade or stripping useful behavior from legacy clients.
The stable C# SDK 2.2.0 release added exactly that path with HttpServerSessionMode.StatefulForInitializeClients
. The release notes describe it as hybrid stateful/stateless serving, and the official session-mode guide spells out the per-request behavior.
The 2026-07-28
MCP revision removed the initialize handshake and Mcp-Session-Id
from its wire format. Client identity, capabilities, and protocol version travel with each request instead. The final specification announcement explains why the core moved toward request/response statelessness.
That creates a migration choice for an existing server.
With HttpServerSessionMode.Stateful
, initialize-era clients receive full sessions. A modern request is refused so a dual-path client can fall back to the older handshake. Compatibility is preserved, but the client does not use the new protocol natively.
With HttpServerSessionMode.Stateless
, every request is independent. That is the right default for servers that do not need session state, unsolicited notifications, resource subscriptions, or older server-to-client flows. It may be too abrupt when deployed clients still rely on those features.
Hybrid mode makes the decision from the incoming request instead of applying one choice to the endpoint.
The server configuration is deliberately small:
builder.Services
.AddMcpServer()
.WithHttpTransport(options =>
{
options.SessionMode =
HttpServerSessionMode.StatefulForInitializeClients;
})
.WithTools<DemoTools>();
app.MapMcp("/mcp");
An initialize-era client sends an initialize
request with protocolVersion: "2025-11-25"
. The server returns Mcp-Session-Id
, and that client must send the value on its later requests.
A 2026-07-28
client sends server/discover
or another operation with its modern metadata. It does not receive a session ID:
{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "hybrid-probe",
"version": "1.0.0"
}
}
}
}
For Streamable HTTP, the request also carries MCP-Protocol-Version: 2026-07-28
and the routing header Mcp-Method: server/discover
. Tool calls add Mcp-Name
. Those headers do not create a session; they let the transport route and validate a self-describing request.
Configuration alone is easy to regress. I prefer a transport-level check that exercises the real SDK handler while staying offline.
The complete sample uses Microsoft.AspNetCore.TestHost
, so it opens no port and calls no model. Its verifier runs these checks against the same /mcp
route:
Mcp-Session-Id
.echo
tool call succeeds and remains stateless.DELETE
returns 405 Method Not Allowed
.DELETE
closes its session successfully.The assertion that matters is not just a 200
response. Each side must get the correct session semantics:
AssertNoSession(modernToolCall, "modern tool call");
string sessionId = GetRequiredSessionId(
legacyInitialize,
"legacy initialize");
Equal(
sessionId,
GetRequiredSessionId(legacyToolCall, "legacy tool call"));
Run the verifier with:
dotnet restore .\McpHybridSessions.csproj
dotnet build .\McpHybridSessions.csproj -c Release --no-restore
dotnet run --project .\McpHybridSessions.csproj -c Release --no-build
The merged implementation and validation record are also in the pull request.
Hybrid mode is a bridge, not a new universal default. If every supported client speaks 2026-07-28
and the server needs no session-only behavior, choose Stateless
. It is simpler to scale because requests can land on any instance without affinity.
The modern half of a hybrid endpoint is still stateless. It cannot receive unsolicited notifications or use resource subscriptions, and it does not gain per-client isolation. Use the newer multi-round-trip mechanism where it fits rather than assuming hybrid mode restores sessions for modern requests.
The legacy half still has the operational costs of sessions. Session memory lives on the server, restarts discard it, and multiple instances may need affinity or a deliberate migration design. Authentication and authorization are separate concerns; a session ID is not proof of identity.
I would keep this regression test until the last initialize-era client is retired, then change the server and test together to the explicit stateless mode.
Are you keeping a legacy session path during your 2026-07-28
migration, or can your server go fully stateless?
Happy coding!