diff --git a/.env.example b/.env.example index 3907cdb..a6127a3 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,19 @@ MCP_HOST=0.0.0.0 # Port to expose the server on MCP_PORT=8000 +# ============================================================================= +# MCP Server Authentication +# ============================================================================= +# Enable authentication for MCP server access (true/false) +# When enabled, clients must provide a valid API key in the Authorization header +MCP_AUTH_ENABLED=false + +# Comma-separated list of valid API keys for MCP server access +# Example: MCP_API_KEYS=key1,key2,key3 +# Clients connect using: Authorization: Bearer YOUR_API_KEY +# Generate secure keys with: openssl rand -hex 32 +MCP_API_KEYS= + # ============================================================================= # API Monitoring Configuration # ============================================================================= @@ -57,12 +70,3 @@ REQUEST_TIMEOUT=30.0 # Maximum retry attempts for failed requests MAX_RETRIES=3 - -# ============================================================================= -# Docker Configuration (for Docker Compose deployments) -# ============================================================================= -# Your Docker Hub username (used when pulling pre-built images) -DOCKER_USERNAME=yourusername - -# Docker image tag to use (e.g., latest, v1.0.0, dev) -IMAGE_TAG=latest diff --git a/CLAUDE.md b/CLAUDE.md index 4072f83..76ba0bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,8 @@ This document provides context for AI assistants (like Claude) working on the Ge geoguessr-mcp/ ├── src/geoguessr_mcp/ │ ├── api/ # GeoGuessr API client and response handling -│ ├── auth/ # Authentication management +│ ├── auth/ # GeoGuessr authentication management +│ ├── middleware/ # MCP server authentication middleware │ ├── models/ # Data models (Profile, Stats, Games, etc.) │ ├── monitoring/ # Dynamic schema detection and API monitoring │ │ ├── endpoint/ # Endpoint monitoring logic @@ -21,6 +22,8 @@ geoguessr-mcp/ │ ├── tools/ # MCP tool definitions │ ├── config.py # Configuration and settings │ └── main.py # Application entry point +├── scripts/ # Deployment scripts +│ └── deploy.sh # Automated production deployment ├── tests/ # Unit and integration tests ├── Dockerfile # Container definition ├── docker-compose.yml # Development deployment @@ -54,9 +57,19 @@ Tools are organized by domain: Each tool returns a `DynamicResponse` which includes both the data and schema information. -### 3. Authentication Flow +### 3. Authentication Systems -The server supports three authentication methods: +The server has two authentication layers: + +#### MCP Server Authentication (Access Control) +Controls who can connect to the MCP server: +- **Bearer Token**: API key-based authentication via `Authorization` header +- **Configuration**: `MCP_AUTH_ENABLED` and `MCP_API_KEYS` environment variables +- **Middleware**: `src/geoguessr_mcp/middleware/auth.py` - Validates API keys +- **Optional**: Can be disabled for local/trusted deployments + +#### GeoGuessr API Authentication (Data Access) +The server supports three methods to access GeoGuessr's API: 1. **Environment variable**: `GEOGUESSR_NCFA_COOKIE` in .env 2. **Login tool**: Email/password authentication via MCP 3. **Manual cookie**: Direct cookie setting via tool @@ -65,6 +78,10 @@ Session state is managed in `src/geoguessr_mcp/auth/session.py`. ## Docker Deployment +### Official Docker Image + +The server is available as a pre-built image: **`nyxiumyuuki/geoguessr-mcp:latest`** + ### Build Process The Dockerfile uses a multi-stage approach: @@ -76,9 +93,19 @@ The Dockerfile uses a multi-stage approach: ### Deployment Options -1. **Local Build**: `docker compose up -d --build` -2. **Docker Hub**: Build, tag, push, then pull on VPS +1. **Pre-built Image**: `docker compose up -d` (uses nyxiumyuuki/geoguessr-mcp:latest) +2. **Local Build**: Uncomment build section in docker-compose.yml 3. **Production**: Use `docker-compose.prod.yml` with nginx reverse proxy +4. **Automated**: Use `./scripts/deploy.sh` for production deployment + +### MCP Server Authentication Configuration + +The server supports optional Bearer token authentication: +- Set `MCP_AUTH_ENABLED=true` to enable +- Set `MCP_API_KEYS=key1,key2,key3` for comma-separated keys +- Generate secure keys with `openssl rand -hex 32` +- Clients connect with `Authorization: Bearer YOUR_API_KEY` header +- Middleware is in `src/geoguessr_mcp/middleware/auth.py` ### Monitoring Configuration diff --git a/README.md b/README.md index d96f073..ab55578 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ A Model Context Protocol (MCP) server for analyzing GeoGuessr game statistics wi ## TODO -- [ ] Fix Docker username in compose files and env vars -- [ ] Add authentication to MCP server to allow access only to specific users +- [x] ~~Fix Docker username in compose files and env vars~~ +- [x] ~~Add authentication to MCP server to allow access only to specific users~~ - [ ] Fix Code Quality on tests not running - [ ] Fix Code Quality on black not formatting - [ ] Add auto monitoring for new endpoints and send notifications by email @@ -51,13 +51,28 @@ docker compose up -d --build That's it! The server is now running on port 8000. -### 3. Connect to Claude +### 3. Configure MCP Server Authentication (Optional) + +To secure your MCP server with API key authentication, edit `.env`: + +```bash +MCP_AUTH_ENABLED=true +MCP_API_KEYS=your-secure-api-key-here +``` + +Generate a secure API key: +```bash +openssl rand -hex 32 +``` + +### 4. Connect to Claude Add to your Claude Desktop configuration: -**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` +**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` +**Without Authentication:** ```json { "mcpServers": { @@ -69,9 +84,58 @@ Add to your Claude Desktop configuration: } ``` +**With Authentication:** +```json +{ + "mcpServers": { + "geoguessr": { + "type": "streamable-http", + "url": "http://YOUR_VPS_IP:8000/mcp", + "headers": { + "Authorization": "Bearer your-secure-api-key-here" + } + } + } +} +``` + ## 🔐 Authentication -The server supports multiple authentication methods: +The server supports two types of authentication: + +### MCP Server Authentication (Controls Access to the MCP Server) + +Secures who can connect to your MCP server. When enabled, clients must provide a valid API key. + +**Enable in `.env`:** +```bash +MCP_AUTH_ENABLED=true +MCP_API_KEYS=key1,key2,key3 # Comma-separated for multiple users +``` + +**Generate secure keys:** +```bash +openssl rand -hex 32 +``` + +**Configure Claude Desktop with authentication:** +```json +{ + "mcpServers": { + "geoguessr": { + "type": "streamable-http", + "url": "https://your-domain.com/mcp", + "headers": { + "Authorization": "Bearer YOUR_API_KEY" + } + } + } +} +``` + +### GeoGuessr API Authentication (Access GeoGuessr Data) + +The server also needs authentication to access GeoGuessr's API. Multiple methods supported: ### Option 1: Login via Claude (Recommended) Simply ask Claude: @@ -174,68 +238,49 @@ Claude uses explore_endpoint tool: ## 🏭 Production Deployment -### Method 1: Build and Push Docker Image +The server is available as a pre-built Docker image: **`nyxiumyuuki/geoguessr-mcp:latest`** -For VPS deployment, it's recommended to build and push your image to Docker Hub: +### Method 1: Quick Deploy with Script + +For VPS deployment with existing nginx-proxy-manager: ```bash -# 1. Build the image -docker build -t yourusername/geoguessr-mcp:latest . +# Clone repository on VPS +git clone https://github.com/NyxiumYuuki/GeoGuessrMCP.git +cd GeoGuessrMCP -# 2. Login to Docker Hub -docker login +# Configure environment +cp .env.example .env +# Edit .env with your settings: +# - GEOGUESSR_NCFA_COOKIE (for GeoGuessr API access) +# - MCP_AUTH_ENABLED=true (optional, for MCP server security) +# - MCP_API_KEYS (if authentication enabled) -# 3. Push the image -docker push yourusername/geoguessr-mcp:latest - -# 4. On your VPS, pull and run -docker pull yourusername/geoguessr-mcp:latest +# Run deployment script +./scripts/deploy.sh ``` -### Method 2: Deploy with Docker Compose on VPS +### Method 2: Manual Docker Compose Deploy #### Development/Testing Setup ```bash -# Clone repository on VPS -git clone https://github.com/yourusername/geoguessr-mcp.git -cd geoguessr-mcp - -# Create .env file -cat > .env << EOF -GEOGUESSR_NCFA_COOKIE=your_cookie_here -DOCKER_USERNAME=yourusername -IMAGE_TAG=latest -EOF - -# Deploy +# Using docker-compose.yml (development) docker compose up -d ``` -#### Production Setup with SSL (VPS with nginx-proxy-manager) +#### Production Setup with nginx-proxy-manager -If you have an existing nginx-proxy-manager setup (like with Firefly III), you can easily deploy this alongside it: - -1. **Build and push your image to Docker Hub:** ```bash -docker build -t yourusername/geoguessr-mcp:latest . -docker push yourusername/geoguessr-mcp:latest +# Using docker-compose.prod.yml (production) +docker compose -f docker-compose.prod.yml up -d ``` -2. **Deploy on VPS using the automated script:** -```bash -# On your VPS -cd /geoguessr-mcp -cp .env.production .env -# Edit .env with your DOCKER_USERNAME and GEOGUESSR_NCFA_COOKIE -./deploy.sh -``` - -3. **Configure SSL in nginx-proxy-manager:** - - Access admin panel: `http://your-vps-ip:81` - - Add Proxy Host for your domain - - Forward to: `geoguessr-mcp-server:8000` - - Enable SSL with Let's Encrypt +**Configure SSL in nginx-proxy-manager:** +- Access admin panel: `http://your-vps-ip:81` +- Add Proxy Host for your domain +- Forward to: `geoguessr-mcp-server:8000` +- Enable SSL with Let's Encrypt **📖 For detailed VPS deployment instructions, see [DEPLOYMENT.md](DEPLOYMENT.md)** @@ -245,52 +290,52 @@ If you prefer not to use Docker Compose: ```bash # Pull the image -docker pull yourusername/geoguessr-mcp:latest +docker pull nyxiumyuuki/geoguessr-mcp:latest # Create a volume for schema cache docker volume create geoguessr-schemas -# Run the container +# Run the container (without authentication) docker run -d \ --name geoguessr-mcp \ --restart unless-stopped \ -p 8000:8000 \ -e GEOGUESSR_NCFA_COOKIE=your_cookie \ + -e MCP_AUTH_ENABLED=false \ -e MONITORING_ENABLED=true \ -e MONITORING_INTERVAL_HOURS=24 \ -e LOG_LEVEL=INFO \ -v geoguessr-schemas:/app/data/schemas \ - yourusername/geoguessr-mcp:latest -``` + nyxiumyuuki/geoguessr-mcp:latest -### Building Multi-Architecture Images - -For deployment on different CPU architectures (ARM64, AMD64): - -```bash -# Enable buildx -docker buildx create --use - -# Build and push multi-arch image -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - -t yourusername/geoguessr-mcp:latest \ - --push . +# Run with MCP authentication enabled +docker run -d \ + --name geoguessr-mcp \ + --restart unless-stopped \ + -p 8000:8000 \ + -e GEOGUESSR_NCFA_COOKIE=your_cookie \ + -e MCP_AUTH_ENABLED=true \ + -e MCP_API_KEYS=your-api-key-1,your-api-key-2 \ + -e MONITORING_ENABLED=true \ + -e MONITORING_INTERVAL_HOURS=24 \ + -e LOG_LEVEL=INFO \ + -v geoguessr-schemas:/app/data/schemas \ + nyxiumyuuki/geoguessr-mcp:latest ``` ### Environment Variables | Variable | Default | Description | |----------|---------|-------------| -| `GEOGUESSR_NCFA_COOKIE` | - | Default authentication cookie | +| `GEOGUESSR_NCFA_COOKIE` | - | GeoGuessr API authentication cookie | +| `MCP_AUTH_ENABLED` | false | Enable MCP server authentication | +| `MCP_API_KEYS` | - | Comma-separated API keys for MCP access | | `MCP_PORT` | 8000 | Server port | | `MCP_TRANSPORT` | streamable-http | MCP transport protocol | | `MONITORING_ENABLED` | true | Enable API monitoring | | `MONITORING_INTERVAL_HOURS` | 24 | Monitoring check interval (runs every 24h) | | `SCHEMA_CACHE_DIR` | /app/data/schemas | Directory for schema persistence | | `LOG_LEVEL` | INFO | Logging verbosity | -| `DOCKER_USERNAME` | yourusername | Your Docker Hub username (for compose files) | -| `IMAGE_TAG` | latest | Docker image tag | ## 🧪 Development diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index fd8a914..9bf599c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -3,14 +3,8 @@ services: geoguessr-mcp: - # Option 1: Build locally (use for initial setup or development) - # build: - # context: . - # dockerfile: Dockerfile - - # Option 2: Use pre-built image from Docker Hub (recommended for production) - # Update this with your Docker Hub username - image: ${DOCKER_USERNAME:-yourusername}/geoguessr-mcp:${IMAGE_TAG:-latest} + # Use pre-built image from Docker Hub + image: nyxiumyuuki/geoguessr-mcp:latest container_name: geoguessr-mcp-server restart: unless-stopped @@ -25,13 +19,6 @@ services: volumes: - geoguessr-schemas:/app/data/schemas - healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:8000/health" ] - interval: 30s - timeout: 10s - retries: 3 - start_period: 15s - logging: driver: "json-file" options: diff --git a/docker-compose.yml b/docker-compose.yml index b8d2bdd..6737184 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,13 @@ services: geoguessr-mcp: - # Option 1: Build locally - build: - context: . - dockerfile: Dockerfile - # Option 2: Use pre-built image from Docker Hub (uncomment to use) - # image: ${DOCKER_USERNAME:-yourusername}/geoguessr-mcp:${IMAGE_TAG:-latest} + # Option 1: Build locally (for development) + # build: + # context: . + # dockerfile: Dockerfile + + # Option 2: Use pre-built image from Docker Hub (recommended) + image: nyxiumyuuki/geoguessr-mcp:latest + container_name: geoguessr-mcp-server restart: unless-stopped ports: @@ -15,12 +17,6 @@ services: volumes: # Persist schema cache between restarts - geoguessr-schemas:${SCHEMA_CACHE_DIR:-/app/data/schemas} - healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:${MCP_PORT:-8000}/health" ] - interval: 30s - timeout: 10s - retries: 3 - start_period: 15s logging: driver: "json-file" options: diff --git a/deploy.sh b/scripts/deploy.sh similarity index 70% rename from deploy.sh rename to scripts/deploy.sh index 0e6baa8..edffcd8 100755 --- a/deploy.sh +++ b/scripts/deploy.sh @@ -50,14 +50,14 @@ check_environment() { print_success "Found $COMPOSE_FILE" if [ ! -f "$ENV_FILE" ]; then - print_warning "$ENV_FILE not found. Creating from example..." - if [ -f "$ENV_EXAMPLE" ]; then - cp "$ENV_EXAMPLE" "$ENV_FILE" - print_warning "Please edit $ENV_FILE with your actual credentials!" - print_info "Required: DOCKER_USERNAME and GEOGUESSR_NCFA_COOKIE" + print_warning "$ENV_FILE not found. Creating from .env.example..." + if [ -f ".env.example" ]; then + cp ".env.example" "$ENV_FILE" + print_warning "Please edit $ENV_FILE with your actual configuration!" + print_info "Configure GEOGUESSR_NCFA_COOKIE and MCP_API_KEYS if using authentication" exit 1 else - print_error "No .env.production example found!" + print_error "No .env.example found!" exit 1 fi fi @@ -84,23 +84,25 @@ check_config() { source "$ENV_FILE" - if [ -z "$DOCKER_USERNAME" ] || [ "$DOCKER_USERNAME" == "yourusername" ]; then - print_error "DOCKER_USERNAME not configured in $ENV_FILE" - exit 1 - fi - print_success "DOCKER_USERNAME is set: $DOCKER_USERNAME" + print_info "Using Docker image: nyxiumyuuki/geoguessr-mcp:latest" - if [ -z "$GEOGUESSR_NCFA_COOKIE" ] || [ "$GEOGUESSR_NCFA_COOKIE" == "your_actual_ncfa_cookie_value_here" ]; then + if [ -z "$GEOGUESSR_NCFA_COOKIE" ]; then print_warning "GEOGUESSR_NCFA_COOKIE not configured" - print_info "Most features require authentication. You can set this later." + print_info "Most GeoGuessr features require authentication. You can set this later." else print_success "GEOGUESSR_NCFA_COOKIE is configured" fi - if [ -z "$IMAGE_TAG" ]; then - IMAGE_TAG="latest" + if [ "$MCP_AUTH_ENABLED" == "true" ]; then + if [ -z "$MCP_API_KEYS" ]; then + print_error "MCP_AUTH_ENABLED is true but MCP_API_KEYS is not set!" + print_info "Either disable authentication or configure API keys" + exit 1 + fi + print_success "MCP server authentication is ENABLED" + else + print_warning "MCP server authentication is DISABLED - server will be publicly accessible" fi - print_info "Using image tag: $IMAGE_TAG" } # Check if firefly_network exists @@ -121,7 +123,7 @@ check_network() { pull_image() { print_header "Step 4: Pull Docker Image" - print_info "Pulling image: $DOCKER_USERNAME/geoguessr-mcp:$IMAGE_TAG" + print_info "Pulling image: nyxiumyuuki/geoguessr-mcp:latest" docker compose -f "$COMPOSE_FILE" pull print_success "Image pulled successfully" } @@ -147,25 +149,16 @@ start_new() { docker compose -f "$COMPOSE_FILE" up -d print_success "Container started" - print_info "Waiting for container to be healthy..." - sleep 5 + print_info "Waiting for container to start..." + sleep 3 - # Check health - HEALTH=$(docker inspect geoguessr-mcp-server --format='{{.State.Health.Status}}' 2>/dev/null || echo "unknown") - if [ "$HEALTH" == "healthy" ]; then - print_success "Container is healthy!" - elif [ "$HEALTH" == "starting" ]; then - print_warning "Container is still starting... Check logs with:" - print_info "docker compose -f $COMPOSE_FILE logs -f" + # Check if running + if docker ps | grep -q geoguessr-mcp-server; then + print_success "Container is running" else - print_warning "Container health check not yet available. Checking if running..." - if docker ps | grep -q geoguessr-mcp-server; then - print_success "Container is running" - else - print_error "Container is not running!" - print_info "Check logs with: docker compose -f $COMPOSE_FILE logs" - exit 1 - fi + print_error "Container is not running!" + print_info "Check logs with: docker compose -f $COMPOSE_FILE logs" + exit 1 fi } @@ -191,12 +184,23 @@ show_next_steps() { echo " - Forward Port: 8000" echo " - Enable SSL with Let's Encrypt" echo "" - echo "2. Test the health endpoint:" - echo " curl https://your-domain.com/health" + echo "2. Test the MCP endpoint:" + echo " curl https://your-domain.com/mcp" + echo " (With auth): curl -H 'Authorization: Bearer YOUR_API_KEY' https://your-domain.com/mcp" echo "" echo "3. Connect Claude Desktop:" echo " Add to claude_desktop_config.json:" - echo ' "geoguessr-mcp": { "url": "https://your-domain.com" }' + echo ' {' + echo ' "mcpServers": {' + echo ' "geoguessr": {' + echo ' "type": "streamable-http",' + echo ' "url": "https://your-domain.com/mcp",' + echo ' "headers": {' + echo ' "Authorization": "Bearer YOUR_API_KEY"' + echo ' }' + echo ' }' + echo ' }' + echo ' }' echo "" print_info "Useful Commands:" @@ -204,14 +208,14 @@ show_next_steps() { echo " View logs: docker compose -f $COMPOSE_FILE logs -f" echo " Restart: docker compose -f $COMPOSE_FILE restart" echo " Stop: docker compose -f $COMPOSE_FILE down" - echo " Update: ./deploy.sh" + echo " Update: scripts/deploy.sh" echo "" print_info "Troubleshooting:" echo "" echo " Check status: docker ps | grep geoguessr-mcp" - echo " Check health: docker inspect geoguessr-mcp-server --format='{{.State.Health.Status}}'" echo " Enter container: docker exec -it geoguessr-mcp-server /bin/bash" + echo " View all logs: docker compose -f $COMPOSE_FILE logs --tail=100" echo "" print_info "For detailed documentation, see: DEPLOYMENT.md" diff --git a/src/geoguessr_mcp/config.py b/src/geoguessr_mcp/config.py index 6c0cc6d..e900e07 100644 --- a/src/geoguessr_mcp/config.py +++ b/src/geoguessr_mcp/config.py @@ -33,6 +33,14 @@ class Settings: default_factory=lambda: os.getenv("SCHEMA_CACHE_DIR", "/app/data/schemas") ) + # Authentication Configuration + MCP_AUTH_ENABLED: bool = field( + default_factory=lambda: os.getenv("MCP_AUTH_ENABLED", "false").lower() == "true" + ) + MCP_API_KEYS: Optional[str] = field( + default_factory=lambda: os.getenv("MCP_API_KEYS") + ) + # Logging Configuration LOG_LEVEL: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO")) @@ -48,6 +56,15 @@ class Settings: raise ValueError(f"Invalid port number: {self.PORT}") if self.MONITORING_INTERVAL_HOURS < 1: raise ValueError("Monitoring interval must be at least 1 hour") + if self.MCP_AUTH_ENABLED and not self.MCP_API_KEYS: + raise ValueError("MCP_AUTH_ENABLED is true but MCP_API_KEYS is not set") + + def get_api_keys(self) -> set[str]: + """Parse and return the set of valid API keys.""" + if not self.MCP_API_KEYS: + return set() + # Support comma-separated API keys + return {key.strip() for key in self.MCP_API_KEYS.split(",") if key.strip()} settings = Settings() diff --git a/src/geoguessr_mcp/main.py b/src/geoguessr_mcp/main.py index 09128cf..c6521e3 100644 --- a/src/geoguessr_mcp/main.py +++ b/src/geoguessr_mcp/main.py @@ -11,6 +11,7 @@ import sys from mcp.server.fastmcp import FastMCP from .config import settings +from .middleware import AuthenticationMiddleware from .monitoring import endpoint_monitor from .tools import register_all_tools @@ -58,6 +59,12 @@ mcp = FastMCP( # Register all tools services = register_all_tools(mcp) +# Add authentication middleware if needed +if settings.MCP_AUTH_ENABLED: + logger.info("Registering authentication middleware") + # Add middleware to the underlying ASGI app + mcp.app.add_middleware(AuthenticationMiddleware) + async def start_background_tasks(): """Start background monitoring tasks.""" @@ -79,11 +86,17 @@ def main(): f"with {settings.TRANSPORT} transport" ) + if settings.MCP_AUTH_ENABLED: + api_key_count = len(settings.get_api_keys()) + logger.info(f"MCP server authentication is ENABLED with {api_key_count} API key(s)") + else: + logger.warning("MCP server authentication is DISABLED - server is publicly accessible") + if settings.DEFAULT_NCFA_COOKIE: - logger.info("Default authentication cookie configured from environment") + logger.info("Default GeoGuessr authentication cookie configured from environment") else: logger.warning( - "No default authentication cookie set. " "Users will need to login or provide a cookie." + "No default GeoGuessr authentication cookie set. " "Users will need to login or provide a cookie." ) # Run the server diff --git a/src/geoguessr_mcp/middleware/__init__.py b/src/geoguessr_mcp/middleware/__init__.py new file mode 100644 index 0000000..eee6c55 --- /dev/null +++ b/src/geoguessr_mcp/middleware/__init__.py @@ -0,0 +1,5 @@ +"""Middleware for GeoGuessr MCP Server.""" + +from .auth import AuthenticationMiddleware + +__all__ = ["AuthenticationMiddleware"] diff --git a/src/geoguessr_mcp/middleware/auth.py b/src/geoguessr_mcp/middleware/auth.py new file mode 100644 index 0000000..ddb918b --- /dev/null +++ b/src/geoguessr_mcp/middleware/auth.py @@ -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