DEV Community

Cover image for MCP C# SDK 2.0 is stable: HTTP is stateless by default, and 10 changes can break your upgrade
Piekwerk
Piekwerk

Posted on

MCP C# SDK 2.0 is stable: HTTP is stateless by default, and 10 changes can break your upgrade

Microsoft announced the stable 2.0 line of the official MCP C# SDK this week, and InfoQ covered the announcement on October 26. The short version: the SDK now implements the 2026-07-28 spec revision, HTTP transport is stateless by default, and the upgrade carries ten numbered breaking changes. I read the v2.0.0 release notes on GitHub before planning anything, because half of these surface as build warnings rather than runtime errors, and you want to know which is which before a deploy window.

What shipped, and when

The package history matters if you tested a preview and moved on. The v2.0.0 tag landed on GitHub on July 28, the same day the spec revision was cut. Three minor releases followed: 2.1 on August 5, 2.2 on August 13, and 2.3 on October 20. What is new this week is Microsoft's announcement of the stable line, so the code you shelved in summer has moved three times since. The revision itself is the largest protocol change since MCP launched: the initialize handshake and the Mcp-Session-Id header are gone, protocol version and capabilities travel with every request, and cross-call state is now your problem, not the protocol's.

Sessions are opt-in now

HttpServerTransportOptions.Stateless defaults to true in 2.0. A stateless server creates no transport sessions, does not expose the standalone SSE GET/DELETE endpoints, and does not support unsolicited server-to-client requests. Any instance can serve any request, which is the whole point: round-robin load balancers and horizontal scaling work without a shared session store. If your server genuinely needs the old behavior, you opt back in and accept the warning:

// HttpServerTransportOptions: stateful is opt-in since 2.0
transportOptions.Stateless = false;
Enter fullscreen mode Exit fullscreen mode

Stateful-only options now raise MCP9006 and apply only to down-level initialize-handshake connections. Before you flip that flag, check what the session was actually carrying. In most servers I have seen it held elicitation state and nothing else, and that has a better replacement now.

Discovery replaces the handshake

Clients probe server/discover first and fall back to the legacy initialize handshake when a server is older. On the wire, a tools/call request now carries Mcp-Method and Mcp-Name headers, and the [McpHeader] attribute promotes a tool parameter to an Mcp-Param-* header. That is SEP-2243 standardization: your load balancer and gateway can route on headers without parsing the body. The body stays authoritative, and a mismatched header makes the server return a HeaderMismatch error, so a misconfigured proxy fails loud instead of routing wrong. The legacy SSE endpoints raise MCP9004 warnings on the way out.

Elicitation moved into MRTR

Statelessness removes the channel servers used to ask the client for input mid-call. The replacement is Multi Round-Trip Requests. A tool throws InputRequiredException carrying one or more input requests, built with InputRequest.ForElicitation, ForSampling or ForRootsList, plus an opaque requestState. The client collects the answers and re-issues the call with inputResponses; McpClient handles that automatically when handlers are registered. Two edges to know: on stateful sessions with 2025-11-25 or earlier clients, the SDK bridges to the old server-initiated requests, and on stateless down-level connections the request surfaces as an McpException. Plan that error path before you rely on mid-tool questions.

Deprecated APIs and the Tasks package move

Roots, sampling and logging now produce MCP9005 warnings, because the 2026-07-28 spec deprecated those surfaces (SEP-2577). Down-level connections can keep using them, and you can suppress MCP9005 temporarily while you plan the migration. Tasks are a bigger deal: the v1.4.x implementation moved out of the core into a dedicated package with no API or wire compatibility with the experimental versions in 1.3 and 1.4. That is a code change, not a recompile:

dotnet add package ModelContextProtocol.Extensions.Tasks
Enter fullscreen mode Exit fullscreen mode

Then register with WithTasks(..) and replace the RequestMethods.Tasks* constants with TasksProtocol members. If your server exposed long-running work through the old experimental Tasks API, budget real migration time here, this was the item the SDK team called out as incompatible rather than merely obsoleted.

OAuth checks that now fail closed

Four authorization changes, all in the direction of rejecting instead of tolerating. Issuer validation follows RFC 9207 and RFC 8414, so inconsistent authorization-server metadata now fails instead of slipping through; fix the metadata rather than bypassing the check. Authorization fails when the server metadata does not advertise S256 in code_challenge_methods_supported, so PKCE support is a hard requirement. Dynamic client registration sends an inferred application_type, and servers that validate the request shape must accept it, or you set DynamicClientRegistrationOptions.ApplicationType explicitly. And AuthorizationRedirectDelegate now raises MCP9007; migrate to ClientOAuthOptions.AuthorizationCallbackHandler so callbacks can return the code, state, and issuer. One behavioral extra: a repeated insufficient_scope challenge that adds no new scopes throws McpException instead of retrying indefinitely, which turns one class of silent loop into a visible failure.

The quiet breaks that hit tests first

Three changes that rarely reach the release notes headline. Tools with UseStructuredContent = true and a non-object return type now emit the raw value, structuredContent: 72, instead of wrapping it as { "result": 72 }, so clients reading a result property break. Deserializing a Tool payload without inputSchema throws JsonException instead of silently defaulting the schema; an empty {} is enough, which matters for proxies and test fixtures that hand-build tool JSON. And explicit SSE connections now surface the underlying HttpRequestException or TimeoutException instead of wrapping everything in IOException, so error handling that matched on the old wrapper stops firing without any error of its own.

A five-minute upgrade check

dotnet build 2>&1 | grep -E 'MCP90(04|05|06|07)'
Enter fullscreen mode Exit fullscreen mode

Every hit maps to a section above. Then grep the codebase for RequestMethods.Tasks, AuthorizationRedirectDelegate, and any handler matching on IOException("Failed to connect transport."). The warnings tell you where the spec moved; the exceptions are the ones that reach production.

If you run Claude Code against your own MCP servers, the client side of this revision landed earlier and I probed it then: Claude Code 2.1.292 asks your MCP servers for 2026-07-28. The server side in .NET is this SDK. Same revision, two upgrade paths, and both are worth closing before a client and a server disagree about whether a session exists.

Top comments (0)