Document and demonstrate allowed hosts and CORS policy guidance (#1554)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Stephen Halter <halter73@gmail.com>
This commit is contained in:
Jeff Handley 2026-05-01 19:03:30 -07:00 • committed by GitHub
commit 2c5d1ed2be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -133,6 +133,64 @@ app.Run();
By default, the HTTP transport uses **stateful sessions** — the server assigns an `Mcp-Session-Id` to each client and tracks session state in memory. For most servers, **stateless mode is recommended** instead. It simplifies deployment, enables horizontal scaling without session affinity, and avoids issues with clients that don't send the `Mcp-Session-Id` header. We recommend setting `Stateless` explicitly (rather than relying on the current default) for [forward compatibility](xref:stateless#forward-and-backward-compatibility). See [Sessions](xref:stateless) for a detailed guide on when to use stateless vs. stateful mode, configure session options, and understand [cancellation and disposal](xref:stateless#cancellation-and-disposal) behavior during shutdown.
#### Host name validation
For local HTTP servers, keep the set of accepted host names limited to loopback values. This helps protect against DNS rebinding, where a browser reaches a local server through an attacker-controlled DNS name while sending that DNS name in the HTTP `Host` header. ASP.NET Core's Kestrel server doesn't validate `Host` headers by default, so configure `AllowedHosts` with known host names rather than `"*"`. This also avoids reflecting untrusted host names through ASP.NET Core features such as absolute URL generation. See [Host filtering with ASP.NET Core Kestrel web server | Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/host-filtering) and [URL generation concepts | Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/routing#url-generation-concepts).
```json
// appsettings.Development.json
{
"AllowedHosts": "localhost;127.0.0.1;[::1]"
}
```
For production servers, configure `AllowedHosts` to the exact public host names for the deployment. If Kestrel is behind a reverse proxy or load balancer, validate the host name at the layer that receives or forwards the client `Host` header. ASP.NET Core's Host Filtering Middleware is appropriate when Kestrel is public-facing or the `Host` header is directly forwarded; Forwarded Headers Middleware has its own `AllowedHosts` option for cases where the proxy doesn't preserve the original `Host` header. See [Host filtering with ASP.NET Core Kestrel web server | Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/host-filtering) and [Configure ASP.NET Core to work with proxy servers and load balancers | Microsoft Learn](https://learn.microsoft.com/aspnet/core/host-and-deploy/proxy-load-balancer).
If you intentionally expose the server through another host name, such as a tunnel, container host, reverse proxy, or deployed domain, add that exact host name to `AllowedHosts` instead of using `"*"`.
#### Browser cross-origin access
**Only** enable CORS if you intentionally want browser-based cross-origin access to this server.
CORS is not a substitute for host name validation. When browser-based cross-origin access is required, limit which browser origins can call the MCP endpoint by using the most restrictive ASP.NET Core CORS policy possible. See [Enable Cross-Origin Requests (CORS) in ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/aspnet/core/security/cors).
For a **stateless** browser client, a narrowly scoped CORS policy usually only needs the headers the browser would otherwise preflight: `Content-Type` for JSON, `Authorization` when the endpoint is protected, and `MCP-Protocol-Version`. If you enable sessions or resumability, also allow `Mcp-Session-Id` and `Last-Event-ID`, and expose `Mcp-Session-Id` on responses so browser code can read it. `Accept` normally doesn't need to be listed because browsers can already send it without extra CORS configuration.
_In this sample below, the MCP server will allow browser calls from `localhost:5173` where a web application is making the request. In production, this allowed origin list would be configured to the trusted web application domains._
```json
// appsettings.Development.json
{
"Mcp": {
"AllowedOrigins": [
"http://localhost:5173"
]
}
}
```
```csharp
var allowedOrigins = builder.Configuration.GetSection("Mcp:AllowedOrigins").Get<string[]>() ?? ["http://localhost:5173"];
builder.Services.AddCors(options =>
{
options.AddPolicy("McpBrowserClient", policy =>
{
policy.WithOrigins(allowedOrigins)
// Add GET for standalone/resumable SSE streams and DELETE for stateful session termination.
.WithMethods("POST", "GET", "DELETE")
.WithHeaders("Content-Type", "Authorization", "MCP-Protocol-Version", "Mcp-Session-Id")
.WithExposedHeaders("Mcp-Session-Id");
});
});
var app = builder.Build();
app.UseCors();
app.MapMcp("/mcp").RequireCors("McpBrowserClient");
```
#### How messages flow
In Streamable HTTP, client requests arrive as HTTP POST requests. The server holds each POST response body open as an SSE stream and writes the JSON-RPC response — plus any intermediate messages like progress notifications or server-to-client requests — back through it. This provides natural HTTP-level backpressure: each POST holds its connection until the handler completes.