Subchapter 143.12
references/transport-http.mdMarkdown8 KBView on GitHub
Streamable HTTP is the modern remote transport. A single endpoint accepts JSON-RPC over HTTP POST and (optionally) streams responses back as Server-Sent Events when the server has more than one message to send.
SSE-only is deprecated. The legacy “HTTP+SSE” transport (separate POST endpoint + GET SSE endpoint) is gone from new clients. Use Streamable HTTP. Only enable legacy SSE (EnableLegacySse = true) if you must support a known-old client, and document why.
Stateless = true).For local single-user scenarios, STDIO is simpler.
dotnet new web -n MyHttpServer -f net10.0
cd MyHttpServer
dotnet add package ModelContextProtocol.AspNetCore --version 2.2.0// Program.cs
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMcpServer()
// Since 2.x, Stateless defaults to true: each request is independent,
// no Mcp-Session-Id tracking, no SSE session endpoints — ready for
// horizontal scaling without sticky sessions.
.WithHttpTransport()
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp(); // mounts the MCP endpoints at "/"
// app.MapMcp("/mcp"); // or under a path prefix
app.Run("http://localhost:3001");
[McpServerToolType]
public static class EchoTool
{
[McpServerTool, Description("Echoes the message back to the client.")]
public static string Echo(string message) => $"hello {message}";
}v2 breaking change:
HttpServerTransportOptions.Statelessnow defaults totrue(it defaulted tofalseon 1.x). A server upgraded to 2.x without touching options stops creating sessions and stops exposing SSE endpoints. SetStateless = falseexplicitly to restore the legacy behavior.
| Mode | options.Stateless | Behaviour | Use when |
|---|---|---|---|
| Stateless | true (default since 2.x) | No Mcp-Session-Id. Each POST is independent. Serves the current (2026-07-28) revision. | Horizontal scaling, simple tool servers, current-protocol clients. |
| Stateful | false | Server assigns and tracks Mcp-Session-Id. Long-lived session. Down-level compatibility mode: the server refuses the 2026-07-28 revision so dual-path clients fall back to an initialize-capable revision (2025-11-25 or earlier). | Legacy ElicitAsync/sampling/roots paths, pushed log notifications, clients that haven't adopted 2026-07-28. Requires session affinity at the load balancer. |
Rule: on the current (2026-07-28) protocol there are no HTTP sessions — “ask the user something mid-tool” uses the multi-round-trip pattern (throw InputRequiredException, handle the retried call; see elicitation.md), which works in both session modes and both revisions. Set Stateless = false only for the legacy paths — ElicitAsync, the deprecated SampleAsync/RequestRootsAsync, or pushed log/notification messages — and be aware it pins HTTP clients to a down-level, initialize-capable revision. On the stateless default those legacy calls fail at runtime with no channel to deliver them on.
MapMcp(pattern = "") creates a route group at pattern and maps:
Accept header and whether multiple messages need to flow back.Default pattern is the root (/). To put MCP under /mcp/v1:
app.MapMcp("/mcp/v1");Match this on the client side (Endpoint = new Uri("https://host/mcp/v1")).
server/discover method to learn capabilities instead of the legacy initialize handshake. The SDK answers both and falls back automatically for down-level peers (2025-11-25 and earlier) — you don’t write any code for this, but don’t be surprised to see server/discover in traffic captures.Mcp-Method header (e.g. tools/call), and named invocations (tools/call, prompts/get, resources/read) additionally carry Mcp-Name (e.g. the tool name), so gateways and rate limiters can route/throttle per tool without parsing JSON bodies. Don’t require Mcp-Name globally at the gateway — discovery and list requests legitimately omit it.When you need to vary server behaviour per HTTP request (auth, tenant, headers), use the ConfigureSessionOptions callback:
builder.Services
.AddMcpServer()
.WithHttpTransport(options =>
{
options.ConfigureSessionOptions = async (httpContext, mcpOptions, ct) =>
{
var tenantId = httpContext.Request.Headers["X-Tenant"].ToString();
mcpOptions.ServerInstructions = $"Tenant: {tenantId}";
// mutate any McpServerOptions fields per-session
};
});Inside a tool, you can also inject IHttpContextAccessor if AddHttpContextAccessor() is registered. See the AspNetCoreMcpPerSessionTools sample (opens in a new tab).
The MCP endpoint is just an ASP.NET Core endpoint — apply standard middleware:
builder.Services
.AddAuthentication("Bearer")
.AddJwtBearer(/* configure */);
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapMcp().RequireAuthorization(); // protect the endpointFor OAuth flows where the MCP server is the resource server, follow the MCP authorization spec (opens in a new tab). The ProtectedMcpServer sample (opens in a new tab) shows a working setup with discovery endpoints.
For machine-to-machine, an API key middleware is fine:
app.Use(async (ctx, next) =>
{
if (ctx.Request.Headers["X-Api-Key"] != Configuration["ApiKey"])
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
await next();
});builder.Services.AddCors(o => o.AddDefaultPolicy(p =>
p.WithOrigins("https://my-host.example.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()));
// ...
app.UseCors();
app.MapMcp();Add the standard ASP.NET Core probes; the MCP endpoint shouldn’t be the liveness check.
builder.Services.AddHealthChecks();
// ...
app.MapHealthChecks("/healthz");The SDK emits OpenTelemetry traces (Activity per tool call) and metrics. Wire them up if the user has an OTel pipeline:
builder.Services
.AddOpenTelemetry()
.WithTracing(t => t.AddSource("ModelContextProtocol").AddOtlpExporter())
.WithMetrics(m => m.AddMeter("ModelContextProtocol").AddOtlpExporter());proxy_buffering off;. Without this, streaming responses are batched into one slow blob.builder.Services
.AddMcpServer()
.WithHttpTransport(options =>
{
options.EnableLegacySse = true;
#pragma warning disable MCP9004
options.Stateless = false; // SSE requires stateful mode
#pragma warning restore MCP9004
})
.WithToolsFromAssembly();Only do this if the user has a documented client that hasn’t migrated. New deployments should not enable it.