Choosing MCP transports in C# is a decision about process boundaries, security boundaries, and how much connection state your .NET server needs to remember. The official C# SDK gives you stdio for local child-process servers, Streamable HTTP for remote servers, and legacy SSE for compatibility.
This is a MOFU deep dive for developers who understand the basic Model Context Protocol shape and now need to decide how a C# implementation should communicate. We will stay focused on how each option behaves, where it fits, and how to migrate away from SSE safely.
As of writing, the latest stable NuGet version I verified for ModelContextProtocol, ModelContextProtocol.Core, and ModelContextProtocol.AspNetCore is 1.4.0. A 2.0.0-preview.1 package exists, but this article sticks to stable 1.4.0 APIs.
Why MCP transports in C# are a decision, not a detail
A transport is not just the pipe carrying JSON-RPC messages. It controls who starts the server, where credentials live, whether HTTP infrastructure participates, how backpressure works, and whether the server can keep session state between calls.
The C# SDK transport docs describe stdio, Streamable HTTP, and SSE as a legacy option. The protocol transport specification defines stdio and Streamable HTTP as the standard transports, with Streamable HTTP replacing HTTP+SSE. If your agent integrations also use regular .NET HTTP clients, HttpClient in C#: The Complete Guide for .NET Developers is relevant because transport choice affects timeouts, headers, diagnostics, and trust boundaries.
Here is the mental model: stdio is for a local process relationship, Streamable HTTP is for a remote service relationship, and legacy SSE is for migration.
The stdio MCP transport in C# is a child-process contract
Stdio is the most direct MCP transport in C# when your server runs beside a desktop host, IDE, CLI, or local agent runtime. The client launches the server process, writes JSON-RPC messages to stdin, and reads JSON-RPC messages from stdout. The protocol specification is strict here: stdout is protocol traffic, not a logging stream.
That changes your .NET defaults. Console logging normally writes to stdout. A stdio MCP server should send logs to stderr so the client does not parse log lines as JSON-RPC.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(options =>
{
options.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
This wiring is small, which is part of the appeal. A local host can start your executable, manage its lifetime, and terminate it when the user closes the workspace. For local developer workflows, this fits naturally. You can package the server as a .NET tool, run it from an editor, and avoid exposing an HTTP endpoint at all.
The tradeoff is that stdio does not look like a shared service. Each client process typically gets its own server process. That can be excellent for isolation, but it is not what you want when multiple clients need a common remote endpoint. If you are comparing MCP stdio vs HTTP, start with this question: should the host own the server process, or should the server live independently?
Credentials are the other big stdio decision. The SDK docs state that StdioClientTransportOptions inherits environment variables by default. That is convenient, but it can leak credentials such as API keys, tokens, proxy settings, or internal configuration into a third-party child process. For trusted first-party servers, inheritance may be acceptable. For untrusted or marketplace-style servers, it is a risk you should make explicit.
using ModelContextProtocol.Client;
var environment = StdioClientTransportOptions.GetDefaultEnvironmentVariables();
environment["MY_SERVER_API_KEY"] = myServerApiKey;
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Command = "my-mcp-server",
Arguments = ["--workspace", workspacePath],
InheritEnvironmentVariables = false,
EnvironmentVariables = environment,
ShutdownTimeout = TimeSpan.FromSeconds(10)
});
await using var client = await McpClient.CreateAsync(transport);
That example is the safer default for many MCP transports in C# evaluations: do not inherit everything. Start with the SDK's curated allowlist, then add only the values the server needs.
Stdio works best when the server is local, process-scoped, and tied to one user context. It is less suitable when you need central authentication, shared availability, browser-based access, or normal HTTP observability.
Streamable HTTP is the recommended remote MCP transport
Streamable HTTP is the recommended HTTP-based transport for remote MCP servers. In the C# SDK, server wiring comes from ModelContextProtocol.AspNetCore, WithHttpTransport(...), and MapMcp(...). This does not mean every hosting concern belongs in the transport article. Allowed hosts, authentication policies, per-session tools, Azure deployment, and production hardening deserve their own treatment. But the transport-level decision still matters because it determines stateless versus stateful behavior and how messages flow.
A minimal Streamable HTTP .NET server can be wired like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMcpServer()
.WithHttpTransport(options =>
{
options.Stateless = true;
})
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp("/mcp");
app.Run();
For most remote servers, options.Stateless = true is the starting point I would evaluate first. The 1.4.0 SDK docs recommend explicitly setting Stateless instead of relying on the current default. In stable 1.4.0, the docs describe the current default as stateful for backward compatibility, while still recommending stateless for most HTTP servers. That nuance matters. If you want your behavior to remain stable across SDK upgrades, set the option yourself.
Stateless Streamable HTTP treats each request independently. There is no Mcp-Session-Id to track, no server-side session memory, and no sticky-session requirement for scaling. That is why it fits many remote servers that expose tools as request-response operations. If a tool receives input, calls an API, queries data, returns a result, and does not need to remember the client between requests, stateless is usually the simpler model.
Stateful Streamable HTTP is still valid. Use it when the server needs server-to-client requests, unsolicited notifications, resource subscriptions, or per-client state that cannot leak across concurrent agents. In stateful mode, the server assigns an Mcp-Session-Id, and compliant clients include it on subsequent requests. The cost is operational: memory per session, restart behavior, and often session affinity if you deploy more than one instance.
This is the heart of MCP Streamable HTTP .NET decision-making. Stateless is easier to scale and reason about. Stateful enables richer bidirectional behavior. Neither is a moral victory. Pick the one your protocol behavior requires.
How Streamable HTTP clients connect and resume sessions
On the client side, HttpClientTransport connects to the MCP endpoint. The SDK supports HttpTransportMode.StreamableHttp, and the default auto-detection mode tries Streamable HTTP first before falling back to SSE for older servers. If you know the server supports Streamable HTTP, being explicit can make diagnostics easier.
using ModelContextProtocol.Client;
var transport = new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new Uri("https://mcp.example.com/mcp"),
TransportMode = HttpTransportMode.StreamableHttp,
ConnectionTimeout = TimeSpan.FromSeconds(30),
AdditionalHeaders = new Dictionary<string, string>
{
["Authorization"] = $"Bearer {accessToken}"
}
});
await using var client = await McpClient.CreateAsync(transport);
This is where normal .NET HTTP instincts still help. Think about connection timeouts, authorization headers, proxy behavior, logging, and diagnostics. If your organization standardizes on IHttpClientFactory, typed clients, and delegating handlers, IHttpClientFactory in .NET: Named Clients, Typed Clients, and DI Patterns is relevant when wrapping remote MCP clients.
Session resumption only applies when there is a stateful session to resume. The SDK docs show KnownSessionId on the transport and McpClient.ResumeSessionAsync(...) with the previous server capabilities and server info. That is useful when the client has a recoverable interruption and the server still has the session.
using ModelContextProtocol.Client;
var transport = new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new Uri("https://mcp.example.com/mcp"),
KnownSessionId = previousSessionId
});
await using var client = await McpClient.ResumeSessionAsync(
transport,
new ResumeClientSessionOptions
{
ServerCapabilities = previousServerCapabilities,
ServerInfo = previousServerInfo
});
Do not treat resumption as durable application state. It is a transport/session recovery mechanism. Long-running workflow state belongs outside the transport session.
DNS rebinding and CORS are transport-level safety concerns
The Streamable HTTP specification includes a security warning for remote and local HTTP servers: validate the Origin header, bind local servers to localhost rather than all interfaces, and require proper authentication. The C# SDK docs also call out host name validation and restrictive CORS for browser-based clients. I am intentionally keeping this at the transport level, not turning it into a full ASP.NET Core hosting checklist.
DNS rebinding matters because a browser can be tricked into reaching a local HTTP server through an attacker-controlled name. CORS matters when browser code is intentionally allowed to call your MCP endpoint, but it is not a substitute for host validation or authentication.
The decision point is simple: if the MCP transport is HTTP, it inherits HTTP security obligations. Restrict host names. Restrict browser origins when CORS is needed. Include only the headers required for your mode, such as Content-Type, Authorization, MCP-Protocol-Version, and session-related headers when you use sessions or resumability.
Legacy SSE is now a migration topic, not a new-design choice
Legacy SSE is the part of MCP transports in C# where current documentation matters most. The C# SDK 1.4.0 transport docs say new implementations should prefer Streamable HTTP. The GitHub release notes for v1.2.0 also call out a breaking behavioral change: legacy SSE endpoints are disabled by default.
The specifics are important. MapMcp() no longer maps /sse and /message by default. The opt-in property is HttpServerTransportOptions.EnableLegacySse. It is marked [Obsolete] with diagnostic MCP9004. Legacy SSE also requires stateful mode. When Stateless = true, legacy SSE endpoints are not mapped.
Why the strong guidance? Backpressure. Legacy SSE separates the request and response channels. Clients POST JSON-RPC messages to /message, and responses arrive on a long-lived GET stream at /sse. The POST endpoint returns 202 Accepted immediately after queueing, before the handler completes. That means there is no HTTP-level backpressure on handler concurrency. A client can keep posting work without waiting for prior tool calls to finish. Streamable HTTP improves this because the server holds the POST response open until the handler completes, which naturally ties request completion to connection usage.
If you must temporarily support older clients, the opt-in looks like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMcpServer()
.WithHttpTransport(options =>
{
// Legacy SSE requires stateful mode.
options.Stateless = false;
#pragma warning disable MCP9004 // EnableLegacySse is obsolete.
options.EnableLegacySse = true;
#pragma warning restore MCP9004
})
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp("/mcp");
app.Run();
I would not hide the warning suppression in a helper without a comment in the owning configuration. The obsolete diagnostic is a useful reminder: this is compatibility mode, not the direction for new servers.
Migrating from SSE to Streamable HTTP in .NET
The practical migration is smaller than many teams expect, but you need to change both the client endpoint and the server expectation.
For clients, move from the SSE path to the MCP endpoint mapped by MapMcp(). If the old client pointed at https://mcp.example.com/sse, the Streamable HTTP client should point at the root MCP endpoint such as https://mcp.example.com/mcp, depending on your route. With HttpTransportMode.AutoDetect, the SDK tries Streamable HTTP first. If you know the server supports it, set HttpTransportMode.StreamableHttp explicitly.
For servers, migrate clients first, then remove EnableLegacySse. During a transition period, you can run Streamable HTTP and legacy SSE together by setting Stateless = false and EnableLegacySse = true. Streamable HTTP is served at the MapMcp() route. Legacy SSE clients use the SSE path under that route. After the final legacy client moves, remove the opt-in and consider switching to stateless mode if you no longer need session features.
A safe migration checklist looks like this:
| Step | Change | Why it matters |
|---|---|---|
| Inventory clients | Find clients configured for /sse |
Those clients will break when legacy endpoints are disabled |
| Update client endpoint | Point to the MapMcp() route |
Streamable HTTP uses the MCP endpoint, not the old SSE endpoint |
| Prefer Streamable HTTP | Use explicit HttpTransportMode.StreamableHttp where possible |
Removes ambiguity during troubleshooting |
| Run transition mode | Keep EnableLegacySse = true only while needed |
Supports old clients without blocking new clients |
| Remove SSE opt-in | Delete EnableLegacySse after migration |
Eliminates the backpressure risk and obsolete diagnostic |
| Revisit state | Switch to Stateless = true if session features are not needed |
Simplifies scaling and removes session memory |
If you are writing docs for your own server, be explicit about the endpoint. Say whether clients should connect to /mcp, /, or another route. Do not tell new clients to connect to /sse unless you are intentionally documenting legacy compatibility.
Decision table for choosing an MCP transport
When evaluating MCP transports in C#, I would use the following table as a starting point rather than a final architecture review.
| Criterion | Choose stdio | Choose Streamable HTTP stateless | Choose Streamable HTTP stateful | Legacy SSE compatibility |
|---|---|---|---|---|
| Process model | Client should launch the server | Server should run independently | Server should run independently | Existing server already has SSE clients |
| Primary environment | IDE, desktop, CLI, local agent | Remote service, internal API, cloud-hosted endpoint | Remote service with session features | Compatibility transition only |
| Client isolation | One process per client is useful | Requests can be independent | Per-client state must be preserved | Legacy session is required |
| Unsolicited server-to-client messages | Natural through the process connection | Not available in stateless mode | Supported | Required by legacy client behavior |
| Scaling model | Not a shared HTTP service | Easier horizontal scaling | Requires session-aware operations | Requires stateful operation and carries backpressure concerns |
| Credential concern | Environment inheritance into child process | HTTP auth and headers | HTTP auth plus session protection | HTTP auth plus legacy stream behavior |
| Recommendation | Great for local tools | Default remote choice to evaluate first | Use when features require it | Migrate away |
The strongest practical advice is to avoid designing around legacy SSE for new work. If the comparison is MCP stdio vs HTTP, decide whether the server should be local and host-owned or remote and independently hosted. If HTTP wins, compare stateless Streamable HTTP against stateful Streamable HTTP based on feature requirements, not habit.
Common mistakes when comparing MCP transports in C#
The first mistake is treating stdout as a normal logging destination in stdio. That breaks the protocol stream. Send logs to stderr and keep stdout reserved for MCP messages. If you need broader observability, Logging in .NET: The Complete Developer's Guide can help, but stdio has this extra constraint.
The second mistake is ignoring environment inheritance. A third-party stdio server can receive parent credentials unless you disable inheritance or curate the environment. The third mistake is using stateful Streamable HTTP by habit. If you do not need server-to-client requests, unsolicited notifications, subscriptions, or per-client state, stateless mode is usually easier to operate.
The fourth mistake is enabling legacy SSE and forgetting why. The MCP9004 obsolete diagnostic exists to keep that decision visible. If your server still needs SSE, document which clients depend on it and what must happen before it can be removed. For streaming HTTP fundamentals outside MCP, see HttpClient Streaming in C#: HttpCompletionOption, ReadAsStreamAsync, and Server-Sent Events.
FAQ
What are MCP transports in C#?
MCP transports in C# are the communication mechanisms used by MCP clients and servers to exchange JSON-RPC messages. The official C# SDK supports stdio, Streamable HTTP, and legacy SSE. Stdio is local and process-based. Streamable HTTP is the recommended remote transport. Legacy SSE exists for compatibility with older clients.
Should I use stdio or Streamable HTTP for a local MCP server?
Use stdio when a desktop app, IDE, CLI, or local agent host should launch and own the server process. Use Streamable HTTP locally only when you need an HTTP endpoint for debugging, browser access, or remote-style behavior.
Is Streamable HTTP always stateless in .NET?
No. Streamable HTTP in .NET can be stateless or stateful. The SDK 1.4.0 docs recommend setting Stateless explicitly. Choose stateless when each request can stand alone. Choose stateful when you need server-to-client requests, unsolicited notifications, subscriptions, or session-scoped state.
Why is legacy SSE disabled by default?
Legacy SSE is disabled by default because it has a backpressure problem. The client posts work to /message, receives 202 Accepted immediately, and gets responses over a separate /sse stream. Since the POST does not wait for the handler to finish, a client can create excessive concurrent work. Streamable HTTP avoids that shape by keeping the POST response open until the request completes.
How do I migrate from SSE to Streamable HTTP in C#?
Move clients from the /sse endpoint to the MCP endpoint mapped by MapMcp(), such as /mcp. Prefer HttpTransportMode.StreamableHttp when the server supports it. If old clients still need SSE, temporarily enable EnableLegacySse = true with Stateless = false, then remove the opt-in after migration.
Does CORS make a Streamable HTTP MCP server safe?
No. CORS controls which browser origins may call the server, but it does not replace authentication or host validation. Treat CORS as one transport-level browser access control, not the whole security model.
Final guidance on MCP transport selection
For most teams, the decision tree is short. Use stdio when the MCP server is local and the host should launch it. Use Streamable HTTP when the MCP server is remote or independently hosted. Start with stateless Streamable HTTP unless your server needs session-only features. Use legacy SSE only as a temporary compatibility bridge while clients migrate.
That is the practical difference between MCP transports in C#. The wiring is small, but the operational consequences are not. Choose the transport that matches the process model first, then choose the state model that matches the protocol behavior you actually need.

