Skip to content

Browser-based MCP clients blocked by missing CORS headers #198

@bettercallsaulj

Description

@bettercallsaulj

1. What's the Issue

Browser-based MCP clients (such as MCP Inspector) fail to receive responses from gopher-mcp servers because HTTP responses lack CORS (Cross-Origin Resource Sharing) headers.

When a browser makes a cross-origin request, it checks the response for CORS headers to determine if the response should be allowed. Without these headers, the browser blocks the response even though the server processed the request successfully.

Error observed:

Access to fetch at 'http://localhost:3001/mcp' from origin 'http://localhost:6274'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

This affects all HTTP responses from the server:

  • JSON-RPC responses (initialize, tools/list, etc.)
  • SSE event streams

2. How to Reproduce

Prerequisites

  • Build gopher-mcp with HTTP/SSE transport enabled
  • Run an MCP server using HTTP/SSE transport

Steps to Reproduce

  1. Start the MCP server:

    ./mcp_example_server  # or any gopher-mcp server with HTTP/SSE transport
  2. Send a request from a browser context (or simulate with curl showing headers):

    curl -s -i -X POST http://localhost:3001/mcp \
      -H "Content-Type: application/json" \
      -H "Origin: http://localhost:6274" \
      -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
  3. Expected: Response includes CORS headers:

    HTTP/1.1 200 OK
    Access-Control-Allow-Origin: *
    Access-Control-Allow-Methods: GET, POST, OPTIONS
    Access-Control-Allow-Headers: Content-Type, Authorization, Accept, Mcp-Session-Id, Mcp-Protocol-Version
    ...
    
  4. Actual (before fix): Response has no CORS headers:

    HTTP/1.1 200 OK
    Content-Type: application/json
    Content-Length: ...
    

Using MCP Inspector

  1. Open MCP Inspector at http://localhost:6274
  2. Select "Streamable HTTP" transport
  3. Enter URL: http://localhost:3001/mcp
  4. Click Connect
  5. Before fix: Browser console shows CORS error, responses are blocked

3. How to Fix It

The fix adds CORS headers to all HTTP responses in src/filter/http_codec_filter.cc.

Fix 1: Add CORS Headers to JSON Responses

In the onWrite method, add CORS headers to standard JSON responses:

// In the JSON response section (non-SSE)
response << "Cache-Control: no-cache\r\n";
// CORS headers for browser-based clients (e.g., MCP Inspector)
response << "Access-Control-Allow-Origin: *\r\n";
response << "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n";
response << "Access-Control-Allow-Headers: Content-Type, Authorization, Accept, Mcp-Session-Id, Mcp-Protocol-Version\r\n";

Fix 2: Add CORS Headers to SSE Responses

Add the same CORS headers to Server-Sent Events responses:

// In the SSE response section
response << "Cache-Control: no-cache\r\n";
response << "Connection: keep-alive\r\n";
response << "X-Accel-Buffering: no\r\n";  // Disable proxy buffering
// CORS headers for browser-based clients (e.g., MCP Inspector)
response << "Access-Control-Allow-Origin: *\r\n";
response << "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n";
response << "Access-Control-Allow-Headers: Content-Type, Authorization, Accept, Mcp-Session-Id, Mcp-Protocol-Version\r\n";

Fix 3: Skip Double HTTP Framing

Add a check to pass through responses that are already HTTP-formatted (e.g., from the routing filter):

// Check if data is already HTTP-formatted (from routing filter)
// If so, pass through without adding more HTTP framing
if (body_data.length() >= 5 &&
    body_data.compare(0, 5, "HTTP/") == 0) {
  GOPHER_LOG_DEBUG(
      "HttpCodecFilter::onWrite - data already HTTP formatted, "
      "passing through");
  return network::FilterStatus::Continue;
}

This prevents double HTTP headers when other filters (like the routing filter for OPTIONS) have already built the HTTP response.

Verification

After the fix:

$ curl -s -i -X POST http://localhost:3001/mcp \
    -H "Content-Type: application/json" \
    -H "Origin: http://localhost:6274" \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}'

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 234
Cache-Control: no-cache
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, Accept, Mcp-Session-Id, Mcp-Protocol-Version
Connection: keep-alive

{"jsonrpc":"2.0","id":1,"result":{...}}

Browser-based MCP clients can now receive and process responses from the server.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions