Skip to content

Conversation

@gazzadownunder
Copy link
Contributor

Summary

The mcp-proxy HTTP server does not include the WWW-Authenticate header in 401 Unauthorized responses, violating RFC 6750 requirements for OAuth 2.0 Bearer Token usage.

Impact

  • Compliance: Non-compliant with RFC 6750 Section 3
  • Developer Experience: Clients cannot discover OAuth endpoints from 401 responses
  • OAuth 2.1: Does not follow MCP OAuth 2.1 specification for resource server requirements

Expected Behavior

Per RFC 6750 Section 3, when a protected resource returns a 401 response, it MUST include the WWW-Authenticate header:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource"
Content-Type: application/json

{
  "error": {
    "code": -32000,
    "message": "Unauthorized: Missing Authorization header with Bearer token"
  },
  "id": 1,
  "jsonrpc": "2.0"
}

Current Behavior

Current 401 responses only include Content-Type header:

res.setHeader("Content-Type", "application/json");
res.writeHead(401).end(JSON.stringify({
  error: {
    code: -32000,
    message: errorMessage
  },
  id: body?.id ?? null,
  jsonrpc: "2.0"
}));

Missing: WWW-Authenticate header ❌

Proposed Solution

Add support for passing OAuth configuration to startHTTPServer and include the WWW-Authenticate header in all 401 responses:

// Add oauth parameter to startHTTPServer options
export const startHTTPServer = async <T extends ServerLike>({
  // ... existing parameters
  oauth?: {
    protectedResource?: {
      resource?: string;
    };
  };
  // ...
}) => {
  // Helper function to construct WWW-Authenticate header
  const getWWWAuthenticateHeader = (req: http.IncomingMessage): string => {
    if (oauth?.protectedResource?.resource) {
      return `Bearer resource_metadata="${oauth.protectedResource.resource}/.well-known/oauth-protected-resource"`;
    }
    // Fallback: construct from request
    const protocol = req.headers["x-forwarded-proto"] || "http";
    const host = req.headers.host || "localhost";
    return `Bearer resource_metadata="${protocol}://${host}/.well-known/oauth-protected-resource"`;
  };

  // In handleStreamRequest, when returning 401:
  const wwwAuthHeader = getWWWAuthenticateHeader(req);
  res.setHeader("Content-Type", "application/json");
  res.setHeader("WWW-Authenticate", wwwAuthHeader);
  res.writeHead(401).end(JSON.stringify({
    error: {
      code: -32000,
      message: errorMessage
    },
    id: body?.id ?? null,
    jsonrpc: "2.0"
  }));
};

Locations Requiring Changes

  1. Stateless authentication check (lines ~165-205 in current implementation)

    • Add WWW-Authenticate header when authentication fails
    • Add WWW-Authenticate header when authentication throws error
  2. Session validation errors (other 401 scenarios)

    • Any other 401 responses should include the header

Testing

Once implemented, verify with:

curl -v -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

Expected response headers:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="http://localhost:3000/.well-known/oauth-protected-resource"
Content-Type: application/json

Benefits

  1. RFC 6750 compliant - Follows OAuth 2.0 Bearer Token standard
  2. Better DX - Clients can discover OAuth endpoints from 401 responses
  3. MCP OAuth 2.1 - Aligns with MCP specification requirements
  4. Backward compatible - Adding headers doesn't break existing clients

References

@punkpeye punkpeye merged commit f30804f into punkpeye:main Oct 12, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants