Add MCP server authentication and update Docker configuration

This commit implements several key improvements to the GeoGuessr MCP server:

## MCP Server Authentication
- Add Bearer token authentication for MCP server access control
- New middleware in src/geoguessr_mcp/middleware/auth.py
- Configuration via MCP_AUTH_ENABLED and MCP_API_KEYS environment variables
- Support for multiple API keys (comma-separated)
- Optional authentication - can be disabled for trusted deployments
- Clients connect using Authorization: Bearer YOUR_API_KEY header

## Docker Configuration Updates
- Update to use official pre-built image: nyxiumyuuki/geoguessr-mcp:latest
- Remove DOCKER_USERNAME and IMAGE_TAG from environment variables
- Simplify docker-compose.yml and docker-compose.prod.yml
- Remove healthcheck configuration (not necessary for the deployment)

## Deployment Improvements
- Move deploy.sh to scripts/deploy.sh for better organization
- Update deploy.sh to use official Docker image
- Add authentication validation in deployment script
- Improve deployment logging and error messages

## Documentation Updates
- Update README.md with authentication configuration examples
- Add MCP server authentication section with setup instructions
- Update environment variables table
- Simplify deployment instructions
- Update CLAUDE.md with new authentication architecture
- Add .env.example configuration for MCP authentication

## Technical Details
- Authentication middleware integrates with FastMCP's Starlette ASGI app
- Middleware validates Bearer tokens on all requests except /health
- Logs authentication attempts and failures
- Returns proper 401/403 HTTP status codes
- Validates configuration on startup to prevent misconfiguration

Resolves TODO items:
- [x] Fix Docker username in compose files and env vars
- [x] Add authentication to MCP server to allow access only to specific users
This commit is contained in:
Claude 2025-11-29 22:16:01 +00:00
parent 52d2f864a8
commit 07b1cb84b2
No known key found for this signature in database
10 changed files with 346 additions and 151 deletions

View file

@ -0,0 +1,97 @@
"""
Authentication middleware for MCP server.
Provides Bearer token authentication for HTTP-based MCP transports.
"""
import logging
from typing import Optional
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from ..config import settings
logger = logging.getLogger(__name__)
class AuthenticationMiddleware(BaseHTTPMiddleware):
"""
Middleware to validate API keys via Bearer token authentication.
When MCP_AUTH_ENABLED is true, this middleware checks for a valid
Authorization header with a Bearer token matching one of the configured API keys.
"""
def __init__(self, app, valid_api_keys: Optional[set[str]] = None):
super().__init__(app)
self.valid_api_keys = valid_api_keys or settings.get_api_keys()
self.enabled = settings.MCP_AUTH_ENABLED
if self.enabled:
if not self.valid_api_keys:
logger.warning("Authentication is enabled but no API keys are configured!")
else:
logger.info(f"Authentication enabled with {len(self.valid_api_keys)} API key(s)")
else:
logger.info("Authentication is disabled")
async def dispatch(self, request: Request, call_next) -> Response:
"""Process the request and validate authentication if enabled."""
# Skip authentication if disabled
if not self.enabled:
return await call_next(request)
# Skip authentication for health check endpoint
if request.url.path == "/health":
return await call_next(request)
# Check for Authorization header
auth_header = request.headers.get("Authorization")
if not auth_header:
logger.warning(f"Missing Authorization header from {request.client.host}")
return JSONResponse(
status_code=401,
content={
"error": "Unauthorized",
"message": "Missing Authorization header. Use 'Authorization: Bearer YOUR_API_KEY'"
},
headers={"WWW-Authenticate": "Bearer"}
)
# Parse Bearer token
parts = auth_header.split()
if len(parts) != 2 or parts[0].lower() != "bearer":
logger.warning(f"Invalid Authorization header format from {request.client.host}")
return JSONResponse(
status_code=401,
content={
"error": "Unauthorized",
"message": "Invalid Authorization header format. Use 'Authorization: Bearer YOUR_API_KEY'"
},
headers={"WWW-Authenticate": "Bearer"}
)
token = parts[1]
# Validate token against configured API keys
if token not in self.valid_api_keys:
logger.warning(f"Invalid API key attempt from {request.client.host}")
return JSONResponse(
status_code=403,
content={
"error": "Forbidden",
"message": "Invalid API key"
},
headers={"WWW-Authenticate": "Bearer"}
)
# Authentication successful
logger.debug(f"Authenticated request from {request.client.host}")
# Proceed with the request
response = await call_next(request)
return response