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:
parent
0781beafa0
commit
2c5d1ed2be
2 changed files with 288 additions and 9 deletions
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue