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
2 changed files with 288 additions and 9 deletions

View file

@ -97,6 +97,138 @@ public class MapMcpStreamableHttpTests(ITestOutputHelper outputHelper) : MapMcpT
Assert.Equal("AutoDetectTestServer", mcpClient.ServerInfo.Name);
}
[Fact]
public async Task BrowserPreflight_AllowsConfiguredOrigin_AndRequiredHeaders()
{
Builder.Services.AddCors(options =>
{
options.AddPolicy("BrowserClient", policy =>
{
policy.WithOrigins("http://localhost:5173")
.WithMethods("GET", "POST", "DELETE")
.WithHeaders("Content-Type", "Authorization", "MCP-Protocol-Version", "Mcp-Session-Id")
.WithExposedHeaders("Mcp-Session-Id");
});
});
Builder.Services.AddMcpServer().WithHttpTransport(ConfigureStateless);
await using var app = Builder.Build();
app.UseCors();
app.MapMcp().RequireCors("BrowserClient");
await app.StartAsync(TestContext.Current.CancellationToken);
using var request = new HttpRequestMessage(HttpMethod.Options, "http://localhost:5000/");
request.Headers.Add("Origin", "http://localhost:5173");
request.Headers.Add("Access-Control-Request-Method", "POST");
request.Headers.Add("Access-Control-Request-Headers", "content-type,authorization,mcp-protocol-version,mcp-session-id");
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.Equal("http://localhost:5173", Assert.Single(response.Headers.GetValues("Access-Control-Allow-Origin")));
var allowHeaders = string.Join(",", response.Headers.GetValues("Access-Control-Allow-Headers"));
Assert.Contains("content-type", allowHeaders, StringComparison.OrdinalIgnoreCase);
Assert.Contains("authorization", allowHeaders, StringComparison.OrdinalIgnoreCase);
Assert.Contains("mcp-protocol-version", allowHeaders, StringComparison.OrdinalIgnoreCase);
Assert.Contains("mcp-session-id", allowHeaders, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BrowserPreflight_DoesNotCorsApprove_DisallowedOrigin()
{
Builder.Services.AddCors(options =>
{
options.AddPolicy("BrowserClient", policy =>
{
policy.WithOrigins("http://localhost:5173")
.WithMethods("POST")
.WithHeaders("Content-Type", "MCP-Protocol-Version");
});
});
Builder.Services.AddMcpServer().WithHttpTransport(ConfigureStateless);
await using var app = Builder.Build();
app.UseCors();
app.MapMcp().RequireCors("BrowserClient");
await app.StartAsync(TestContext.Current.CancellationToken);
using var request = new HttpRequestMessage(HttpMethod.Options, "http://localhost:5000/");
// CORS matches the browser Origin exactly. "localhost" and "127.0.0.1" both
// resolve to loopback, but they are different origins and do not match.
request.Headers.Add("Origin", "http://127.0.0.1:5173");
request.Headers.Add("Access-Control-Request-Method", "POST");
request.Headers.Add("Access-Control-Request-Headers", "content-type,mcp-protocol-version");
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
// ASP.NET Core's CORS middleware commonly answers the preflight with 204 even when
// the origin is not approved. The browser treats the request as disallowed because
// the Access-Control-Allow-* approval headers are omitted from the response.
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.False(response.Headers.Contains("Access-Control-Allow-Origin"));
Assert.False(response.Headers.Contains("Access-Control-Allow-Headers"));
}
[Fact]
public async Task InitializeResponse_ExposesMcpSessionId_ForBrowserClients()
{
Builder.Services.AddCors(options =>
{
options.AddPolicy("BrowserClient", policy =>
{
policy.WithOrigins("http://localhost:5173")
.WithMethods("POST")
.WithHeaders("Content-Type", "MCP-Protocol-Version")
.WithExposedHeaders("Mcp-Session-Id");
});
});
Builder.Services.AddMcpServer(options =>
{
options.ServerInfo = new()
{
Name = "CorsSessionServer",
Version = "1.0.0",
};
}).WithHttpTransport(ConfigureStateless);
await using var app = Builder.Build();
app.UseCors();
app.MapMcp().RequireCors("BrowserClient");
await app.StartAsync(TestContext.Current.CancellationToken);
const string initializeRequest = """
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"browser-client","version":"1.0.0"}}}
""";
using var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/")
{
Content = new StringContent(initializeRequest, System.Text.Encoding.UTF8, "application/json")
};
request.Headers.Add("Origin", "http://localhost:5173");
request.Headers.Accept.ParseAdd("application/json");
request.Headers.Accept.ParseAdd("text/event-stream");
using var response = await HttpClient.SendAsync(request, TestContext.Current.CancellationToken);
Assert.True(response.IsSuccessStatusCode);
Assert.Equal("http://localhost:5173", Assert.Single(response.Headers.GetValues("Access-Control-Allow-Origin")));
var exposedHeaders = string.Join(",", response.Headers.GetValues("Access-Control-Expose-Headers"));
Assert.Contains("Mcp-Session-Id", exposedHeaders, StringComparison.OrdinalIgnoreCase);
if (!Stateless)
{
Assert.True(response.Headers.Contains("Mcp-Session-Id"));
}
}
[Fact]
public async Task SseEndpoints_AreDisabledByDefault_InStatefulMode()
{

View file

@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using ModelContextProtocol.Tests.Utils;
@ -109,6 +110,9 @@ public class ServerConformanceTests(ConformanceServerFixture fixture, ITestOutpu
public async Task RunPendingConformanceTest_JsonSchema202012()
{
Assert.SkipWhen(!NodeHelpers.IsNodeInstalled(), "Node.js is not installed. Skipping conformance tests.");
Assert.SkipWhen(
RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
"Pending Node-based conformance scenario is unstable on Windows due to a libuv shutdown assertion.");
var result = await RunConformanceTestsAsync($"server --url {fixture.ServerUrl} --scenario json-schema-2020-12");
@ -120,6 +124,9 @@ public class ServerConformanceTests(ConformanceServerFixture fixture, ITestOutpu
public async Task RunPendingConformanceTest_ServerSsePolling()
{
Assert.SkipWhen(!NodeHelpers.IsNodeInstalled(), "Node.js is not installed. Skipping conformance tests.");
Assert.SkipWhen(
RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
"Pending Node-based conformance scenario is unstable on Windows due to a libuv shutdown assertion.");
var result = await RunConformanceTestsAsync($"server --url {fixture.ServerUrl} --scenario server-sse-polling");