Skip to content

Conversation

@pablotp
Copy link

@pablotp pablotp commented Sep 16, 2025

Add authentication support

Summary

Adds optional API key authentication to protect /sse and /mcp endpoints. Authentication is disabled by default for backward compatibility.

Changes

  • Added AuthMiddleware class for API key validation
  • Added --api-key CLI argument and MCP_PROXY_API_KEY environment variable support
  • Protected endpoints: /sse, /mcp, /messages/* (including named server paths)
  • Unprotected endpoints: /status, OPTIONS requests

Usage

# Start server with authentication
mcp-proxy --port 8080 --api-key YOUR_SECRET_KEY uvx mcp-server-fetch

# Make authenticated request
curl -H "X-API-Key: YOUR_SECRET_KEY" http://localhost:8080/sse

Testing

  • Added comprehensive test suite covering authentication scenarios. All tests pass.
  • Manual tests:
Manual verification commands

Make sure you have installed dependencies:

uv sync

Test 1: Server WITHOUT Authentication

Open Terminal 1 and start the server:

uv run mcp-proxy --port 8080 echo "Hello MCP"

Open Terminal 2 and test endpoints:

# Should return 200 (status endpoint)
curl -i http://localhost:8080/status

# Should return 200 or 405 (SSE endpoint - no auth required)
curl -i http://localhost:8080/sse

Test 2: Server WITH Authentication (CLI argument)

Open Terminal 1 and start the server with API key:

uv run mcp-proxy --port 8080 --api-key test123 echo "Hello MCP"

Open Terminal 2 and test endpoints:

# Status endpoint (should work without auth - returns 200)
curl -i http://localhost:8080/status

# SSE endpoint WITHOUT API key (should return 401)
curl -i http://localhost:8080/sse

# SSE endpoint with WRONG API key (should return 401)
curl -i -H "X-API-Key: wrong" http://localhost:8080/sse

# SSE endpoint with CORRECT API key (should return 200 or 405)
curl -i -H "X-API-Key: test123" http://localhost:8080/sse

# Test case-insensitive header (should work)
curl -i -H "x-api-key: test123" http://localhost:8080/sse

Test 3: Server WITH Authentication (Environment Variable)

Open Terminal 1 and start the server with env var:

export MCP_PROXY_API_KEY=secret123
uv run mcp-proxy --port 8080 echo "Hello MCP"

Open Terminal 2 and test:

# Without key (should return 401)
curl -i http://localhost:8080/sse

# With correct key from env (should return 200 or 405)
curl -i -H "X-API-Key: secret123" http://localhost:8080/sse

Test 4: Named Server Endpoints

Start server with named servers and authentication:

uv run mcp-proxy --port 8080 --api-key test123 --named-server test1 "echo test1" echo "default"

Test in another terminal:

# Named server SSE endpoint without key (should return 401)
curl -i http://localhost:8080/servers/test1/sse

# Named server SSE endpoint with key (should work)
curl -i -H "X-API-Key: test123" http://localhost:8080/servers/test1/sse

Expected Results

  • Without authentication: All endpoints are accessible
  • With authentication:
    • /status always accessible
    • /sse and /mcp require valid API key
    • Wrong or missing key returns 401
    • Correct key allows access
    • Headers are case-insensitive
    • Named server endpoints also protected

Related

@codecov
Copy link

codecov bot commented Sep 22, 2025

Codecov Report

❌ Patch coverage is 98.35165% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/mcp_proxy/auth.py 92.00% 1 Missing and 1 partial ⚠️
src/mcp_proxy/mcp_server.py 98.03% 0 Missing and 1 partial ⚠️

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #107      +/-   ##
==========================================
+ Coverage   97.20%   97.42%   +0.22%     
==========================================
  Files           6        8       +2     
  Lines         823      972     +149     
  Branches       30       36       +6     
==========================================
+ Hits          800      947     +147     
- Misses         15       16       +1     
- Partials        8        9       +1     
Files with missing lines Coverage Δ
tests/test_auth_simple.py 100.00% <100.00%> (ø)
tests/test_mcp_server.py 99.30% <100.00%> (+0.04%) ⬆️
src/mcp_proxy/mcp_server.py 93.91% <98.03%> (+1.12%) ⬆️
src/mcp_proxy/auth.py 92.00% <92.00%> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 9dd600c...a83d87c. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sparfenyuk
Copy link
Owner

Hey @pablotp, nice contribution! Can you resolve failures in the build checks?

bfontaine and others added 2 commits September 22, 2025 11:07
Fixes sparfenyuk#95

This adds the option `--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}`
to define the log level. If both `--debug` and `--log-level` are
provided, the former takes precedence.
@pablotp
Copy link
Author

pablotp commented Sep 22, 2025

@sparfenyuk I'm sorry for not looking more carefully into the pre-commits and test setup. I've just pushed a commit to address the issues and resolve the conflicts with main. Locally, the different steps of the pipeline work. Please trigger the GHA build at your earliest convenience.

@pablotp
Copy link
Author

pablotp commented Sep 22, 2025

@sparfenyuk it seems a file outside of my PR caused an error for the linter. I've pushed a new commit to address this: 218da4f
I hope that's the last one needed to get the build green :-)

Thanks for your prompt response!

@sparfenyuk
Copy link
Owner

@pablotp Thank you, and no worries - all good!

@sparfenyuk sparfenyuk requested a review from Copilot October 3, 2025 14:09
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds optional API key authentication to protect MCP proxy endpoints. Authentication is disabled by default for backward compatibility and can be enabled via CLI argument or environment variable.

  • Added AuthMiddleware class for API key validation with configurable protection for specific endpoints
  • Added CLI --api-key argument and MCP_PROXY_API_KEY environment variable support
  • Refactored the run_mcp_server function to improve maintainability and support middleware integration

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/mcp_proxy/auth.py New authentication middleware implementation
src/mcp_proxy/__main__.py Added CLI argument and environment variable support for API key
src/mcp_proxy/mcp_server.py Refactored server setup and integrated authentication middleware
tests/test_auth_simple.py Comprehensive test suite for authentication scenarios
tests/test_mcp_server.py Added test for authentication integration
README.md Updated documentation with authentication usage examples

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

# Check if path needs protection (/sse, /mcp, /messages, /servers/*/sse, /servers/*/mcp)
path = request.url.path
needs_auth = (
path.startswith(("/sse", "/mcp", "/messages")) or "/sse" in path or "/mcp" in path
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path protection logic is duplicated and could match unintended paths. For example, /sse in path would match /something/sse/other. Consider using a more precise pattern matching approach or regex to avoid false positives.

Copilot uses AI. Check for mistakes.
Comment on lines 242 to +244
# Setup named servers
for name, params in named_server_params.items():
logger.info(
"Setting up named server '%s': %s %s",
name,
params.command,
" ".join(params.args),
)
stdio_streams_named = await stack.enter_async_context(stdio_client(params))
session_named = await stack.enter_async_context(ClientSession(*stdio_streams_named))
proxy_named = await create_proxy_server(session_named)

instance_routes_named, http_manager_named = create_single_instance_routes(
proxy_named,
stateless_instance=mcp_settings.stateless,
)
await stack.enter_async_context(
http_manager_named.run(),
) # Manage lifespan by calling run()

# Mount these routes under /servers/<name>/
server_mount = Mount(f"/servers/{name}", routes=instance_routes_named)
all_routes.append(server_mount)
_global_status["server_instances"][name] = "configured"

if not default_server_params and not named_server_params:
logger.error("No servers configured to run.")
return

middleware: list[Middleware] = []
if mcp_settings.allow_origins:
middleware.append(
Middleware(
CORSMiddleware,
allow_origins=mcp_settings.allow_origins,
allow_methods=["*"],
allow_headers=["*"],
),
)
if named_server_params:
named_routes = await _setup_named_servers(stack, named_server_params, mcp_settings)
Copy link

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable named_server_params is checked for truthiness after being defaulted to an empty dict on line 213. This check will always be True for an empty dict. Consider checking if named_server_params: or use if len(named_server_params) > 0: for clarity.

Copilot uses AI. Check for mistakes.
@mtuckerb
Copy link

this would be so useful. what can i do to help move it along?

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.

4 participants