The MCP C# SDK 2.x Release Train: What Changed Since 2.0 and What Breaks When You Upgrade
What changed between MCP C# SDK 2.0 and 2.3: stateless HTTP by default, Tasks as an extension, nullable ServerInfo and stricter OAuth, with an upgrade checklist tested against a real server.
By Ajith Joseph · · Updated · 4 min read · intermediate
The official MCP C# SDK went to 2.0 in late July, and three minor releases followed between August and October. If your .NET MCP server still pins a 1.x package, or you moved to 2.0 and stopped reading release notes, the changes between 2.0 and 2.3 are worth an hour.
This post collects them in one place, with the order I would check things when upgrading. It builds on my earlier write-up of the 2026-07-28 specification, so I will not repeat the protocol story.
The short version
- 2.0 (July 28) implements the 2026-07-28 revision of the spec and moves the SDK from Apache 2.0 to the MIT license.
- 2.1 makes
HttpServerTransportOptions.Statelessdefault totrue, and replaces the old experimental Tasks support with a separate extension package. - 2.2 adds a hybrid mode, so clients speaking the 2025-11-25 and 2026-07-28 revisions can share one endpoint.
- 2.3 makes
McpClient.ServerInfonullable, a source-breaking change, and tightens OAuth behaviour.
Stateless by default
This is the change most likely to alter behaviour without a compile error. With stateless HTTP there is no session id and no initialize handshake. Each request names its protocol version and capabilities, and any server instance can answer it.
I confirmed this against a small server on SDK 2.3. A request with no session header and no handshake succeeds as long as it carries the routing and metadata the new revision requires. In my test, a bare tools/list was rejected until I added an Mcp-Method header and put both the protocol version and clientCapabilities under _meta. The response had no Mcp-Session-Id header at all.
What that means for your code:
- Anything that stored state per session breaks quietly. If a tool read a value set by an earlier call in the same session, it will not find it. Move that state to a database or cache keyed by something you control.
- Server-initiated requests change. On a stateless connection, calls that need to ask the client something surface as an
McpException. Older stateful clients are bridged to the old behaviour. - You can still opt back in by setting
Statelesstofalse, which is the right call while you migrate state out.
One more detail from the InfoQ coverage: modern Streamable HTTP responses wait for the first protocol message before sending headers. A handler that is silent for a long time may need progress output or a longer HTTP timeout.
Tasks moved to their own package
If you used the experimental task support from 2.0, it is gone from the core package. The replacement, ModelContextProtocol.Extensions.Tasks, is a dedicated extension with no API or wire compatibility with the old implementation. Migration is three steps: add the package reference, import its namespace, and register it on the server builder with WithTasks(...). I walk through the new API in a separate post on long-running tools.
ServerInfo can be null
In 2.3, McpClient.ServerInfo is nullable because a server may omit optional identity metadata from server/discover. The compiler will point at every place you read it. The release notes also warn against using self-reported identity for security decisions, and I agree: a server telling you its name proves nothing.
OAuth got stricter
For HTTP servers behind OAuth, the 2.x line adds issuer validation per RFC 9207, requires the advertised PKCE support to include S256, and infers an application type during dynamic client registration. If a client of yours worked against a permissive authorization server in testing, rerun it against your real one.
An upgrade checklist
- Align package versions.
ModelContextProtocol,ModelContextProtocol.AspNetCoreand the extension packages depend on each other at exact versions. Mixing 2.3 and 2.4 gives a NuGet version conflict (NU1107). Update them together. - Build and read the new warnings, especially around
ServerInfo. - Search for per-session state in your tools, resources and filters.
- Decide on hybrid or stateless, and test with a real client.
- Rerun your OAuth flow end to end.
- Send a raw request with
curland confirm there is no session header. It takes two minutes and shows what your server really does.
What I could not verify
The SDK documentation pages I found were inconsistent about the default. Some still describe stateful sessions as the default, which looks like pre-2.0 text, while Microsoft's July 28 announcement says Stateless is true. I trusted the announcement and my own test.