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:
parent
52d2f864a8
commit
07b1cb84b2
10 changed files with 346 additions and 151 deletions
97
src/geoguessr_mcp/middleware/auth.py
Normal file
97
src/geoguessr_mcp/middleware/auth.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue