Skip to content

HttpRequest connecting to the /sse endpoint hits (number of tools + 1) times while building a custom auth middleware #316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
justinyoo opened this issue Apr 16, 2025 · 2 comments
Labels
bug Something isn't working

Comments

@justinyoo
Copy link

Describe the bug
A clear and concise description of what the bug is.

I was adding a middleware that checks the request header whether an API key exists or not. Here's the code that I wrote:

https://github.com/microsoft/mcp-dotnet-samples/blob/f2131feb5ae79fe4491e20dee4f17089ff5b2096/youtube-subtitles-extractor-containerapp/src/McpYouTubeSubtitlesExtractor.ContainerApp/Middlewares/ApiKeyValidationMiddleware.cs#L13-L40

When I added this middleware, I found a strange behaviour that:

  • The HttpRequest instance hits the middleware multiple times (actually number of tools + 1 times) while establishing connection.
  • It occurred the same through both MCP inspector and GitHub Copilot Agent mode.
  • The first hit has got the API key from the request header.
  • But the consecutive hits didn't recognised the header value, which is null.
  • Every time I hit the "List Tools" and each tool, it keeps returning null value from the request header.
  • It works on local machine anyway and a docker container, regardless of the error.
  • It doesn't work on Azure Container Apps when it's deployed because of this error.

To Reproduce
Steps to reproduce the behavior:

  1. Clone this repo: https://github.com/microsoft/mcp-dotnet-samples
  2. Go to the youtube-subtitles-extractor-containerapp directory.
  3. Follow the instruction to run the MCP server app either on local machine or local container.
  4. Connect to this MCP server to the MCP inspector or VS Code with the endpoint of http://0.0.0.0:5202/sse?code=abcde and see it hits 3 times (because this server has 2 tools) - the first one has the key but the rest doesn't have it.

Expected behavior
A clear and concise description of what you expected to happen.

  • It shouldn't hit multiple times while establishing connections.
  • If it has to hit multiple times, every request should preserve the request header and querystring.

Logs
If applicable, add logs to help explain your problem.

Additional context
Add any other context about the problem here.

If it's not the proper approach to add custom authentication logic, which will be the best one?

@justinyoo justinyoo added the bug Something isn't working label Apr 16, 2025
@dogdie233
Copy link
Contributor

LLM translation was used

Quick Explanation

Only the initial request will use the URL you provided. For example, if you enter http://127.0.0.1:5202/sse?code=114514 in the MCP Inspector's SSE URL field, only the SSE endpoint will receive the code parameter.

Root Cause Analysis

Through browser packet inspection and comparison with the Python SDK, I've analyzed the SSE communication protocol:

When using http://127.0.0.1:5202/sse?code=114514 as the URL:

  1. Upon connection, the browser initiates a request to this URL, which is why your middleware captures the first request containing the code.

Behavior branches:

  • If your middleware returns a non-200 status code, the MCP Inspector will send a request to <host>/.well-known/oauth-authorization-server. This path isn't currently handled in the codebase, so we'll set this scenario aside for now.
  • If your middleware returns 200, the MCP server will:
    • Generate a sessionId to identify the client connection
    • Return a new URL containing this sessionId to redirect subsequent requests

In this case, the server responds with:

event: endpoint  
data: /message?sessionId=0b9b5b6b-9082-4fc2-83af-1184b17db357  

This causes all subsequent client requests to be sent to http://127.0.0.1:5202/message?sessionId=0b9b5b6b-9082-4fc2-83af-1184b17db357 (The MCP server will immediately follow this with a serverinfo response).

The client then makes two POST requests to the message endpoint (with the sessionId):

  1. initialize method
  2. notifications/initialized method

This explains why you observe 3 requests total - not tool_count + 1, but a fixed sequence of 3 requests.

Recommended Solution

I suggest modifying the middleware to:

  1. Only validate the code parameter when accessing the SSE endpoint, since successful validation here triggers sessionId creation (which implicitly authenticates subsequent requests)
  2. Access the sessionId via context.Request.Query when needed for later requests

This approach maintains security while accommodating the protocol's expected flow.

@halter73
Copy link
Contributor

Generally we'd advise against putting an API key in the query string because the query string tends to get logged a lot. We'd encourage you to configure your MCP client to set a request header with the API key. If you're working with the dotnet client, you can use SseClientTransportOptions.AdditionalHeaders, and that should be sent with all request for a given session.

As for why ?code=abcde isn't getting appended to all MCP requests when you configure the client to initially connect using that query string, that's because that query string isn't reflected in the event: endpoint\ndata: messages?sessionId={sessionId} event used to implement HTTP with SSE

If you absolutely must control the query string, you can work at a lower layer with the SseResponseStreamTransport which is what MapMcp uses intternally t attach its session id. You can see sample code demonstrating how to use SseResponseStreamTransport in this test:

private static void MapAbsoluteEndpointUriMcp(IEndpointRouteBuilder endpoints)
{
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
var optionsSnapshot = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();
var routeGroup = endpoints.MapGroup("");
SseResponseStreamTransport? session = null;
routeGroup.MapGet("/sse", async context =>
{
var response = context.Response;
var requestAborted = context.RequestAborted;
response.Headers.ContentType = "text/event-stream";
await using var transport = new SseResponseStreamTransport(response.Body, "http://localhost/message");
session = transport;
try
{
var transportTask = transport.RunAsync(cancellationToken: requestAborted);
await using var server = McpServerFactory.Create(transport, optionsSnapshot.Value, loggerFactory, endpoints.ServiceProvider);
try
{
await server.RunAsync(requestAborted);
}
finally
{
await transport.DisposeAsync();
await transportTask;
}
}
catch (OperationCanceledException) when (requestAborted.IsCancellationRequested)
{
// RequestAborted always triggers when the client disconnects before a complete response body is written,
// but this is how SSE connections are typically closed.
}
});
routeGroup.MapPost("/message", async context =>
{
if (session is null)
{
await Results.BadRequest("Session not started.").ExecuteAsync(context);
return;
}
var message = (JsonRpcMessage?)await context.Request.ReadFromJsonAsync(McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)), context.RequestAborted);
if (message is null)
{
await Results.BadRequest("No message in request body.").ExecuteAsync(context);
return;
}
await session.OnMessageReceivedAsync(message, context.RequestAborted);
context.Response.StatusCode = StatusCodes.Status202Accepted;
await context.Response.WriteAsync("Accepted");
});
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants