Fixing CORS Errors In Mcp-proxy 5.9.0+

by SLV Team 39 views

Hey everyone! Let's dive into a common issue encountered with mcp-proxy versions 5.9.0 and later: CORS (Cross-Origin Resource Sharing) errors. This can be a real headache, especially when your web applications rely on MCP (Model Context Protocol) servers. But don't worry, we'll break it down and explore how to fix it.

Understanding the CORS Issue

So, what's the deal? In a nutshell, CORS is a security mechanism browsers use to restrict web pages from making requests to a different domain than the one which served the web page. This is a crucial security feature, but it can sometimes get in the way when you're working with APIs and different services.

The Problem with mcp-proxy 5.9.0+

The mcp-proxy versions 5.9.0 and above have introduced more restrictive CORS headers. This means that if your browser-based MCP clients send requests with custom headers, they might get blocked. Think of it like a strict bouncer at a club – only specific headers are allowed in! This wasn't an issue in earlier versions like mcp-proxy@5.5.x, which were more permissive. When upgrading from a version like mcp-proxy@5.5.x to mcp-proxy@5.9.0+, you might suddenly find that your browser-based MCP clients start failing with CORS errors. These errors often pop up during the notifications/initialized request or other subsequent MCP operations, which can halt your application's functionality.

Diving into the Root Cause

The heart of the problem lies in the difference in how CORS headers are configured between different mcp-proxy versions. Let's compare:

mcp-proxy@5.5.6 (The Good Old Days - Working Fine):

res.setHeader("Access-Control-Allow-Headers", "*");

In this version, the Access-Control-Allow-Headers is set to *. This is like saying, "Hey, allow any headers!" It's very flexible but can be seen as less secure in some contexts.

mcp-proxy@5.9.0+ (The Stricter Regime - Broken):

res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id");

Here, the Access-Control-Allow-Headers is limited to a specific list: Content-Type, Authorization, Accept, Mcp-Session-Id, and Last-Event-Id. This means that if your request includes any other custom headers, the browser will block it due to CORS policy. It’s like the bouncer having a very strict list of acceptable IDs.

The Impact of Restrictive Headers

These restrictive headers can have a significant impact:

  • Browser-based MCP clients can't connect: Your web applications simply can't talk to your MCP servers.
  • Custom headers are rejected: This is a big deal because many payment protocols and other extensions rely on custom headers. If these are rejected, those features will break.
  • Limited use cases: The list of allowed headers might not cover all legitimate scenarios. You might find yourself needing to add a header that isn't on the list.

How to Reproduce the CORS Issue

Okay, let's get practical. Here's how you can reproduce this issue to see it in action:

  1. Set up an MCP server using mcp-proxy@5.9.0:

    First, you'll need to create an MCP server using the problematic version of mcp-proxy. Here’s some TypeScript code to get you started:

    import { startHTTPServer } from 'mcp-proxy';
    
    const server = await startHTTPServer({
      port: 8080,
      streamEndpoint: '/mcp',
      createServer: async (req) => {
        // Your MCP server logic goes here
      }
    });
    

    This code snippet imports the startHTTPServer function from the mcp-proxy library and sets up a basic HTTP server. The server listens on port 8080 and has a stream endpoint at /mcp. You'll need to add your own MCP server logic inside the createServer function.

  2. Access the server from a browser with custom headers:

    Next, you'll want to access this server from a web browser while including a custom header in your request. This is where the CORS issue will manifest itself. Here's some JavaScript code that demonstrates this:

    fetch('http://localhost:8080/mcp', {
      method: 'POST',
      headers: {
        'Origin': 'https://app.example.com',
        'Content-Type': 'application/json',
        'X-Custom-Header': 'value' // This is the header that will be rejected
      },
      body: JSON.stringify({
        jsonrpc: '2.0',
        id: 1,
        method: 'initialize',
        params: {}
      })
    });
    

    In this code, we're using the fetch API to make a POST request to the MCP server. Notice the headers object: it includes a custom header called X-Custom-Header. This is the header that will trigger the CORS error because it's not in the whitelist of allowed headers in mcp-proxy 5.9.0+.

  3. Observe the CORS error in the browser console:

    When you run this code in a browser, you should see a CORS error in the console. The error message will look something like this:

    Access to fetch at 'http://localhost:8080/mcp' from origin 'https://app.example.com'
    has been blocked by CORS policy: Request header field x-custom-header is not allowed
    by Access-Control-Allow-Headers in preflight response.
    

    This error message is your confirmation that the CORS issue is present. It clearly states that the X-Custom-Header is not allowed by the Access-Control-Allow-Headers policy.

Expected vs. Actual Behavior

Expected Behavior: The server should accept all headers, just like it did in version 5.5.x. This is the ideal scenario because it provides the most flexibility and avoids breaking existing functionality.

Actual Behavior: The server only accepts a limited set of headers: Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id. This restrictive behavior is the root cause of the CORS issue.

Environment Details

It's helpful to know the environment in which this issue was observed:

  • mcp-proxy version: 5.9.0
  • Node.js version: 23.3.0
  • Browsers: Chrome 141, Edge 141
  • Operating System: macOS 24.1.0

This information can be useful for others who are trying to reproduce or debug the issue.

Test Case

To ensure this issue is properly addressed, a test case was added to document the expected behavior. Here's the TypeScript code for the test:

test('CORS headers are properly set', async () => {
  const preflightResponse = await fetch(serverUrl, {
    method: 'OPTIONS',
    headers: {
      'Origin': 'https://test-app.nuwa.dev',
      'Access-Control-Request-Method': 'POST',
      'Access-Control-Request-Headers': 'Content-Type, Mcp-Session-Id, Authorization',
    },
  });

  const corsHeaders: Record<string, string> = {};
  preflightResponse.headers.forEach((value, key) => {
    if (key.toLowerCase().startsWith('access-control')) {
      corsHeaders[key] = value;
    }
  });

  // Expected: "*"
  // Actual in 5.9.0: "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id"
  expect(corsHeaders['access-control-allow-headers']).toBe('*');
});

This test sends an OPTIONS request (a preflight request used in CORS) to the server and checks the access-control-allow-headers in the response. The expectation is that it should be *, allowing all headers. In mcp-proxy 5.9.0+, the actual value is the limited list of headers, causing the test to fail.

Workaround (Temporary Fix)

If you're facing this issue and need a quick fix, a workaround is to pin your dependency to a previous version of mcp-proxy that doesn't have this restrictive behavior. For example:

{
  "dependencies": {
    "mcp-proxy": "^5.5.4"
  }
}

This tells your project to use version 5.5.4 of mcp-proxy, which should avoid the CORS issue. However, this is just a temporary solution. You'll want a proper fix in a future version.

Proposed Solutions (The Real Fixes)

Now, let's talk about how to solve this problem for good. There are a few options:

Option 1: Revert to Permissive Headers (Recommended)

The simplest and often best solution is to go back to the permissive CORS configuration by setting Access-Control-Allow-Headers to *:

res.setHeader("Access-Control-Allow-Headers", "*");

This gives you maximum flexibility and ensures that any custom headers are allowed. It's the easiest way to avoid breaking existing functionality and accommodate future extensions to the MCP protocol.

Option 2: Make CORS Configuration Optional

Another approach is to add a configuration option that allows users to customize the CORS headers. This gives developers more control over their security settings while still providing flexibility. Here's an example of how this might look:

startHTTPServer({
  port: 8080,
  streamEndpoint: '/mcp',
  cors: {
    allowHeaders: '*',  // or array of specific headers
    exposeHeaders: 'mcp-session-id'
  }
});

In this example, a cors option is added to the startHTTPServer function. This option allows developers to specify the allowHeaders (which can be * or a list of specific headers) and exposeHeaders.

Option 3: Extend the Header Whitelist

If you absolutely need to stick with whitelisting specific headers, you need to make sure the list is comprehensive enough to cover all common use cases. This includes:

  • Payment protocol headers (e.g., X-Payment-*)
  • Custom authentication headers
  • Tracing/debugging headers
  • Consider using a wildcard pattern or prefix matching to simplify the configuration.

However, this approach can be error-prone because you need to anticipate all possible headers that might be used. It's generally better to either use * or provide a configuration option.

Additional Context and Why This Matters

This CORS issue isn't just a minor inconvenience; it can have real-world consequences:

  • Production deployments are affected: If you're using payment-enabled MCP servers, this issue can break your payment processing.
  • Restrictive headers break things: While security is important, overly restrictive headers can break legitimate use cases and make it harder to extend the MCP protocol.
  • MCP spec doesn't mandate restrictions: The MCP specification itself doesn't require specific header restrictions, so there's no strong reason to enforce them.
  • Permissive CORS is common in development: Many HTTP/SSE servers default to permissive CORS in development mode to avoid these kinds of issues.

References and Further Reading

If you want to dive deeper into this topic, here are some helpful resources:

Related Files (For Developers)

If you're working on fixing this issue, these files might be relevant:

  • Test case: nuwa-kit/typescript/packages/payment-kit/test/e2e/McpPaymentKit.e2e.test.ts
  • Package configuration: nuwa-kit/typescript/packages/payment-kit/package.json

Conclusion

So, there you have it – a comprehensive look at the CORS issue in mcp-proxy 5.9.0+. We've covered what the problem is, how to reproduce it, why it's happening, and several ways to fix it. Whether you choose to revert to permissive headers, make CORS configurable, or extend the header whitelist, the key is to ensure that your MCP servers can communicate effectively with your browser-based clients. Happy coding, guys!