# MCP C# SDK Hybrid Sessions: Serve Old and New Clients on One Endpoint

> Source: <https://dev.to/ssukhpinder/mcp-c-sdk-hybrid-sessions-serve-old-and-new-clients-on-one-endpoint-47hp>
> Published: 2026-08-19 18:47:30+00:00

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](https://github.com/modelcontextprotocol/csharp-sdk/releases/tag/v2.2.0) describe it as hybrid stateful/stateless serving, and the [official session-mode guide](https://csharp.sdk.modelcontextprotocol.io/v2/concepts/stateless/stateless.html#hybrid-mode-sessions-for-initialize-clients-only) 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](https://blog.modelcontextprotocol.io/posts/2026-07-28/) 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:

``` js
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](https://github.com/ssukhpinder/dev-to-code-samples/tree/main/036-mcp-hybrid-sessions) 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](https://github.com/ssukhpinder/dev-to-code-samples/pull/24).

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!
