Init Commit
This commit is contained in:
commit
ce5abcc217
19 changed files with 2526 additions and 0 deletions
53
.devcontainer/Dockerfile.dev
Normal file
53
.devcontainer/Dockerfile.dev
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Development Dockerfile for GeoGuessr MCP Server
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# Prevent Python from writing pyc files and buffering stdout/stderr
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
ssh \
|
||||||
|
sudo \
|
||||||
|
vim \
|
||||||
|
nano \
|
||||||
|
htop \
|
||||||
|
procps \
|
||||||
|
net-tools \
|
||||||
|
iputils-ping \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create non-root user for development
|
||||||
|
ARG USERNAME=vscode
|
||||||
|
ARG USER_UID=1000
|
||||||
|
ARG USER_GID=$USER_UID
|
||||||
|
RUN groupadd --gid $USER_GID $USERNAME \
|
||||||
|
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
|
||||||
|
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
|
||||||
|
&& chmod 0440 /etc/sudoers.d/$USERNAME
|
||||||
|
|
||||||
|
# Install uv for fast package management
|
||||||
|
RUN pip install --no-cache-dir uv
|
||||||
|
|
||||||
|
# Set up workspace
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
# Copy pyproject.toml and poetry.lock (or similar) first for layer caching
|
||||||
|
COPY pyproject.toml README.md ./
|
||||||
|
|
||||||
|
# Install project and dev dependencies using uv
|
||||||
|
RUN uv pip install --system --no-cache --upgrade pip && \
|
||||||
|
uv pip install --system --no-cache -e ".[dev]"
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER $USERNAME
|
||||||
|
|
||||||
|
# Set Python path
|
||||||
|
ENV PYTHONPATH=/workspace
|
||||||
|
|
||||||
|
# Default command
|
||||||
|
CMD ["sleep", "infinity"]
|
||||||
56
.devcontainer/devcontainer.json
Normal file
56
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"name": "GeoGuessr MCP Server Dev",
|
||||||
|
"dockerComposeFile": "docker-compose.dev.yml",
|
||||||
|
"service": "dev",
|
||||||
|
"workspaceFolder": "/workspace",
|
||||||
|
|
||||||
|
// Features to install
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configure tool-specific properties
|
||||||
|
"customizations": {
|
||||||
|
// VS Code settings (also works for some PyCharm features)
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
|
"ms-python.black-formatter",
|
||||||
|
"charliermarsh.ruff",
|
||||||
|
"tamasfe.even-better-toml",
|
||||||
|
"redhat.vscode-yaml"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
||||||
|
"python.formatting.provider": "black",
|
||||||
|
"python.linting.enabled": true,
|
||||||
|
"python.linting.ruffEnabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ports to forward
|
||||||
|
"forwardPorts": [8000],
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
"containerEnv": {
|
||||||
|
"PYTHONDONTWRITEBYTECODE": "1",
|
||||||
|
"PYTHONUNBUFFERED": "1"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Run commands after container is created
|
||||||
|
"postCreateCommand": "pip install -e '.[dev]' && pre-commit install || true",
|
||||||
|
|
||||||
|
// Run commands when container starts
|
||||||
|
"postStartCommand": "echo 'Dev container ready! Run: python src/server.py'",
|
||||||
|
|
||||||
|
// Mount the .env file if it exists
|
||||||
|
"mounts": [
|
||||||
|
"source=${localWorkspaceFolder}/.env,target=/workspace/.env,type=bind,consistency=cached"
|
||||||
|
],
|
||||||
|
|
||||||
|
// User configuration
|
||||||
|
"remoteUser": "vscode"
|
||||||
|
}
|
||||||
46
.devcontainer/docker-compose.dev.yml
Normal file
46
.devcontainer/docker-compose.dev.yml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
dev:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: .devcontainer/Dockerfile.dev
|
||||||
|
volumes:
|
||||||
|
# Mount the workspace
|
||||||
|
- ..:/workspace:cached
|
||||||
|
# Persist VS Code extensions
|
||||||
|
- vscode-extensions:/home/vscode/.vscode-server/extensions
|
||||||
|
# Persist pip cache
|
||||||
|
- pip-cache:/home/vscode/.cache/pip
|
||||||
|
# Docker socket for Docker-in-Docker (optional)
|
||||||
|
# - /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
# Keep container running
|
||||||
|
command: sleep infinity
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
environment:
|
||||||
|
- PYTHONDONTWRITEBYTECODE=1
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
# Load from .env file
|
||||||
|
- GEOGUESSR_NCFA_COOKIE=${GEOGUESSR_NCFA_COOKIE:-}
|
||||||
|
- MCP_TRANSPORT=streamable-http
|
||||||
|
- MCP_HOST=0.0.0.0
|
||||||
|
- MCP_PORT=8000
|
||||||
|
|
||||||
|
# Ports
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
- "5678:5678" # debugpy port
|
||||||
|
|
||||||
|
# Network
|
||||||
|
networks:
|
||||||
|
- dev-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
vscode-extensions:
|
||||||
|
pip-cache:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dev-network:
|
||||||
|
name: geoguessr-mcp-dev
|
||||||
36
.env.example
Normal file
36
.env.example
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# GeoGuessr MCP Server Configuration
|
||||||
|
# Copy this file to .env and fill in your values
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# REQUIRED: GeoGuessr Authentication
|
||||||
|
# =============================================================================
|
||||||
|
# Your GeoGuessr _ncfa cookie for API authentication
|
||||||
|
#
|
||||||
|
# How to get your _ncfa cookie:
|
||||||
|
# 1. Log in to GeoGuessr in your browser
|
||||||
|
# 2. Open Developer Tools (F12 or Ctrl+Shift+I)
|
||||||
|
# 3. Go to the "Application" or "Storage" tab
|
||||||
|
# 4. Under "Cookies", find www.geoguessr.com
|
||||||
|
# 5. Look for the cookie named "_ncfa"
|
||||||
|
# 6. Copy its value and paste it below
|
||||||
|
#
|
||||||
|
# IMPORTANT: Keep this secret! Anyone with this cookie can access your account.
|
||||||
|
# The cookie typically expires after some time, so you may need to update it periodically.
|
||||||
|
|
||||||
|
GEOGUESSR_NCFA_COOKIE=your_ncfa_cookie_value_here
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MCP Server Configuration
|
||||||
|
# =============================================================================
|
||||||
|
# Transport protocol: "streamable-http" (recommended) or "sse" (legacy)
|
||||||
|
MCP_TRANSPORT=streamable-http
|
||||||
|
|
||||||
|
# Port to expose the server on
|
||||||
|
MCP_PORT=8000
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Optional: API Key Authentication (recommended for production)
|
||||||
|
# =============================================================================
|
||||||
|
# If you want to require API key authentication for accessing the MCP server
|
||||||
|
# Uncomment and set a secure API key
|
||||||
|
# API_KEYS=your-secure-api-key-here,another-api-key-if-needed
|
||||||
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Environment files (contain secrets!)
|
||||||
|
.env
|
||||||
|
*.env.local
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# SSL certificates (if stored locally)
|
||||||
|
ssl/
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
*.key
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
12
.idea/GeoguessrStatsMCP.iml
generated
Normal file
12
.idea/GeoguessrStatsMCP.iml
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/tests" isTestSource="true" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.13 (GeoguessrStatsMCP)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/GeoguessrStatsMCP.iml" filepath="$PROJECT_DIR$/.idea/GeoguessrStatsMCP.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
387
DEVELOPMENT.md
Normal file
387
DEVELOPMENT.md
Normal file
|
|
@ -0,0 +1,387 @@
|
||||||
|
# Development Guide: GeoGuessr MCP Server
|
||||||
|
|
||||||
|
This guide covers multiple ways to develop the GeoGuessr MCP Server using PyCharm, including remote development options.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Option 1: PyCharm Remote Development (Gateway)](#option-1-pycharm-remote-development-gateway)
|
||||||
|
2. [Option 2: PyCharm with Docker Interpreter](#option-2-pycharm-with-docker-interpreter)
|
||||||
|
3. [Option 3: Dev Containers](#option-3-dev-containers)
|
||||||
|
4. [Option 4: SSH Remote Interpreter](#option-4-ssh-remote-interpreter)
|
||||||
|
5. [Running Tests](#running-tests)
|
||||||
|
6. [Debugging](#debugging)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 1: PyCharm Remote Development (Gateway)
|
||||||
|
|
||||||
|
**Best for:** Full IDE experience on a remote server with PyCharm Professional.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- PyCharm Professional 2023.2+ or JetBrains Gateway
|
||||||
|
- SSH access to your VPS
|
||||||
|
|
||||||
|
### Setup Steps
|
||||||
|
|
||||||
|
1. **Install JetBrains Gateway** (or use PyCharm Professional's remote dev feature)
|
||||||
|
- Download from: https://www.jetbrains.com/remote-development/gateway/
|
||||||
|
|
||||||
|
2. **Connect to your VPS**
|
||||||
|
```
|
||||||
|
Gateway → New Connection → SSH
|
||||||
|
Host: your-vps-ip
|
||||||
|
User: your-username
|
||||||
|
Authentication: Key pair or password
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Clone the project on the server**
|
||||||
|
```bash
|
||||||
|
ssh user@your-vps
|
||||||
|
git clone https://github.com/yourusername/geoguessr-mcp.git
|
||||||
|
cd geoguessr-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Open in Gateway**
|
||||||
|
- Select IDE: PyCharm Professional
|
||||||
|
- Project directory: `/home/user/geoguessr-mcp`
|
||||||
|
- Click "Download and Start IDE"
|
||||||
|
|
||||||
|
5. **Configure the environment**
|
||||||
|
- Gateway will install a remote IDE backend
|
||||||
|
- Set up your Python interpreter (see below)
|
||||||
|
- Copy `.env.example` to `.env` and add your cookie
|
||||||
|
|
||||||
|
### Configure Python Interpreter in Gateway
|
||||||
|
|
||||||
|
1. File → Settings → Project → Python Interpreter
|
||||||
|
2. Add Interpreter → Add Local Interpreter
|
||||||
|
3. Select "Virtualenv Environment" → New
|
||||||
|
4. Location: `/home/user/geoguessr-mcp/.venv`
|
||||||
|
5. Base interpreter: `/usr/bin/python3.12`
|
||||||
|
6. Click OK
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 2: PyCharm with Docker Interpreter
|
||||||
|
|
||||||
|
**Best for:** Consistent dev environment, running PyCharm locally with Docker.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- PyCharm Professional
|
||||||
|
- Docker installed locally
|
||||||
|
- Docker Compose installed
|
||||||
|
|
||||||
|
### Setup Steps
|
||||||
|
|
||||||
|
1. **Open the project locally in PyCharm**
|
||||||
|
|
||||||
|
2. **Build the dev container**
|
||||||
|
```bash
|
||||||
|
cd geoguessr-mcp
|
||||||
|
docker compose -f .devcontainer/docker-compose.dev.yml build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure Docker Interpreter**
|
||||||
|
|
||||||
|
a. File → Settings → Project → Python Interpreter
|
||||||
|
|
||||||
|
b. Click the gear icon → Add
|
||||||
|
|
||||||
|
c. Select "Docker Compose"
|
||||||
|
|
||||||
|
d. Configuration file: `.devcontainer/docker-compose.dev.yml`
|
||||||
|
|
||||||
|
e. Service: `dev`
|
||||||
|
|
||||||
|
f. Python interpreter: `/usr/local/bin/python`
|
||||||
|
|
||||||
|
g. Click OK
|
||||||
|
|
||||||
|
4. **Configure Environment Variables**
|
||||||
|
|
||||||
|
a. Run → Edit Configurations
|
||||||
|
|
||||||
|
b. Select your run configuration
|
||||||
|
|
||||||
|
c. Environment variables: Add `GEOGUESSR_NCFA_COOKIE=your_cookie`
|
||||||
|
|
||||||
|
Or create a `.env` file:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Run/Debug**
|
||||||
|
- Use the run configurations provided in `.idea/runConfigurations/`
|
||||||
|
|
||||||
|
### Docker Compose Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start dev container
|
||||||
|
docker compose -f .devcontainer/docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose -f .devcontainer/docker-compose.dev.yml logs -f
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
docker compose -f .devcontainer/docker-compose.dev.yml down
|
||||||
|
|
||||||
|
# Rebuild
|
||||||
|
docker compose -f .devcontainer/docker-compose.dev.yml build --no-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 3: Dev Containers
|
||||||
|
|
||||||
|
**Best for:** VS Code users or PyCharm 2024.1+ with Dev Containers support.
|
||||||
|
|
||||||
|
### Using VS Code
|
||||||
|
|
||||||
|
1. Install the "Dev Containers" extension
|
||||||
|
|
||||||
|
2. Open the project folder
|
||||||
|
|
||||||
|
3. Click "Reopen in Container" when prompted (or F1 → "Dev Containers: Reopen in Container")
|
||||||
|
|
||||||
|
4. VS Code will build and connect to the dev container
|
||||||
|
|
||||||
|
### Using PyCharm (2024.1+)
|
||||||
|
|
||||||
|
PyCharm 2024.1+ has experimental Dev Containers support:
|
||||||
|
|
||||||
|
1. File → Remote Development → Dev Containers
|
||||||
|
|
||||||
|
2. Select the `.devcontainer/devcontainer.json` file
|
||||||
|
|
||||||
|
3. PyCharm will build and connect to the container
|
||||||
|
|
||||||
|
### Manual Container Development
|
||||||
|
|
||||||
|
If your IDE doesn't support dev containers natively:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the dev container
|
||||||
|
docker compose -f .devcontainer/docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# Exec into the container
|
||||||
|
docker exec -it geoguessr-mcp-dev-1 bash
|
||||||
|
|
||||||
|
# Inside container: run the server
|
||||||
|
cd /workspace
|
||||||
|
python server.py
|
||||||
|
|
||||||
|
# Inside container: run tests
|
||||||
|
pytest -v
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Option 4: SSH Remote Interpreter
|
||||||
|
|
||||||
|
**Best for:** PyCharm Professional users who want to run code directly on VPS.
|
||||||
|
|
||||||
|
### Setup Steps
|
||||||
|
|
||||||
|
1. **Prepare your VPS**
|
||||||
|
```bash
|
||||||
|
ssh user@your-vps
|
||||||
|
|
||||||
|
# Install Python 3.12
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3.12 python3.12-venv python3.12-dev
|
||||||
|
|
||||||
|
# Clone the project
|
||||||
|
git clone https://github.com/yourusername/geoguessr-mcp.git
|
||||||
|
cd geoguessr-mcp
|
||||||
|
|
||||||
|
# Create virtual environment
|
||||||
|
python3.12 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt -r requirements-dev.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure PyCharm SSH Interpreter**
|
||||||
|
|
||||||
|
a. File → Settings → Project → Python Interpreter
|
||||||
|
|
||||||
|
b. Click gear → Add → SSH Interpreter
|
||||||
|
|
||||||
|
c. New server configuration:
|
||||||
|
- Host: your-vps-ip
|
||||||
|
- Username: your-username
|
||||||
|
- Authentication: Key pair
|
||||||
|
|
||||||
|
d. Next → Interpreter path: `/home/user/geoguessr-mcp/.venv/bin/python`
|
||||||
|
|
||||||
|
e. Sync folders:
|
||||||
|
- Local: Your local project path
|
||||||
|
- Remote: `/home/user/geoguessr-mcp`
|
||||||
|
|
||||||
|
3. **Configure deployment**
|
||||||
|
|
||||||
|
a. Tools → Deployment → Configuration
|
||||||
|
|
||||||
|
b. Add SFTP server
|
||||||
|
|
||||||
|
c. Enable automatic upload (Tools → Deployment → Automatic Upload)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### From PyCharm
|
||||||
|
- Use the "Run Tests" configuration
|
||||||
|
- Or right-click on `tests/` → Run 'pytest in tests'
|
||||||
|
|
||||||
|
### From Command Line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
pytest -v
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
pytest -v --cov=. --cov-report=html
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
pytest tests/test_server.py -v
|
||||||
|
|
||||||
|
# Run only unit tests (skip integration)
|
||||||
|
pytest -v -m "not integration"
|
||||||
|
|
||||||
|
# Run integration tests (requires real cookie)
|
||||||
|
GEOGUESSR_NCFA_COOKIE=your_real_cookie pytest -v -m integration
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### PyCharm Debugger
|
||||||
|
|
||||||
|
1. Set breakpoints by clicking in the gutter
|
||||||
|
|
||||||
|
2. Run → Debug 'Debug MCP Server'
|
||||||
|
|
||||||
|
3. Use the debugger panel to:
|
||||||
|
- Step through code (F8)
|
||||||
|
- Step into functions (F7)
|
||||||
|
- Evaluate expressions
|
||||||
|
- View variables
|
||||||
|
|
||||||
|
### Remote Debugging (debugpy)
|
||||||
|
|
||||||
|
For debugging in Docker or remote environments:
|
||||||
|
|
||||||
|
1. **Modify server.py** to enable debugpy:
|
||||||
|
```python
|
||||||
|
import debugpy
|
||||||
|
debugpy.listen(("0.0.0.0", 5678))
|
||||||
|
print("Waiting for debugger...")
|
||||||
|
debugpy.wait_for_client()
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure PyCharm Remote Debug**
|
||||||
|
|
||||||
|
a. Run → Edit Configurations → Add → Python Debug Server
|
||||||
|
|
||||||
|
b. IDE host name: `localhost`
|
||||||
|
|
||||||
|
c. Port: `5678`
|
||||||
|
|
||||||
|
d. Path mappings:
|
||||||
|
- Local: `/path/to/local/project`
|
||||||
|
- Remote: `/workspace`
|
||||||
|
|
||||||
|
3. **Start debugging**
|
||||||
|
- Start the server (it will wait for debugger)
|
||||||
|
- Run the "Python Debug Server" configuration in PyCharm
|
||||||
|
- Server will continue execution
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
The server uses Python's logging module. Increase verbosity:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In server.py, change:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via environment:
|
||||||
|
```bash
|
||||||
|
export LOG_LEVEL=DEBUG
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Quality
|
||||||
|
|
||||||
|
### Format Code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Black
|
||||||
|
black .
|
||||||
|
|
||||||
|
# Ruff (lint + fix)
|
||||||
|
ruff check --fix .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Checking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mypy server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-commit Hooks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install hooks
|
||||||
|
pre-commit install
|
||||||
|
|
||||||
|
# Run manually
|
||||||
|
pre-commit run --all-files
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
geoguessr-mcp/
|
||||||
|
├── .devcontainer/ # Dev container configuration
|
||||||
|
│ ├── devcontainer.json
|
||||||
|
│ ├── docker-compose.dev.yml
|
||||||
|
│ └── Dockerfile.dev
|
||||||
|
├── .idea/ # PyCharm settings
|
||||||
|
│ └── runConfigurations/
|
||||||
|
├── tests/ # Test files
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── test_server.py
|
||||||
|
├── server.py # Main MCP server
|
||||||
|
├── requirements.txt # Production dependencies
|
||||||
|
├── requirements-dev.txt # Development dependencies
|
||||||
|
├── pyproject.toml # Project configuration
|
||||||
|
├── Dockerfile # Production Dockerfile
|
||||||
|
├── docker-compose.yml # Production compose
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
1. **Hot Reload**: Use `watchfiles` for auto-restart during development:
|
||||||
|
```bash
|
||||||
|
pip install watchfiles
|
||||||
|
watchfiles "python server.py" .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Test MCP Connection**: Use the test script:
|
||||||
|
```bash
|
||||||
|
python test_server.py http://localhost:8000/mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Environment Variables**: Always use `.env` file locally, never commit secrets
|
||||||
|
|
||||||
|
4. **Cookie Expiration**: GeoGuessr cookies expire - if tests start failing, get a fresh cookie
|
||||||
36
Dockerfile
Normal file
36
Dockerfile
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# GeoGuessr MCP Server Dockerfile
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install uv for faster package installation
|
||||||
|
RUN pip install --no-cache-dir uv
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN uv pip install --system --no-cache -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY server.py .
|
||||||
|
|
||||||
|
# Expose the port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8000/health || exit 1
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
CMD ["python", "server.py"]
|
||||||
335
README.md
Normal file
335
README.md
Normal file
|
|
@ -0,0 +1,335 @@
|
||||||
|
# GeoGuessr MCP Server
|
||||||
|
|
||||||
|
A Model Context Protocol (MCP) server for analyzing your GeoGuessr account data. This server allows Claude AI to access and analyze your GeoGuessr statistics, game history, achievements, and more.
|
||||||
|
|
||||||
|
## Authentication Options
|
||||||
|
|
||||||
|
The server supports **three authentication methods**:
|
||||||
|
|
||||||
|
1. **Login with credentials** (Recommended) - Use the `login` tool with your GeoGuessr email and password
|
||||||
|
2. **Manual cookie** - Use the `set_ncfa_cookie` tool with a cookie extracted from your browser
|
||||||
|
3. **Environment variable** - Set `GEOGUESSR_NCFA_COOKIE` for server-wide default authentication
|
||||||
|
|
||||||
|
### Using Login (Easiest)
|
||||||
|
|
||||||
|
Simply ask Claude to login:
|
||||||
|
```
|
||||||
|
"Login to GeoGuessr with email: myemail@example.com and password: mypassword"
|
||||||
|
```
|
||||||
|
|
||||||
|
The server will authenticate with GeoGuessr and create a session. Your credentials are only used once to obtain an authentication token - they are **not stored** on the server.
|
||||||
|
|
||||||
|
### Security Notes
|
||||||
|
|
||||||
|
- Credentials are sent directly to GeoGuessr's official API over HTTPS
|
||||||
|
- Session tokens are stored in memory only (lost on server restart)
|
||||||
|
- For persistent authentication, use the environment variable method
|
||||||
|
- Always use HTTPS in production to protect credentials in transit
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Authentication Tools
|
||||||
|
- **login** - Authenticate with email/password
|
||||||
|
- **logout** - End the current session
|
||||||
|
- **set_ncfa_cookie** - Set authentication cookie manually
|
||||||
|
- **set_session_token** - Restore a previous session
|
||||||
|
- **get_auth_status** - Check current authentication status
|
||||||
|
|
||||||
|
### Profile & Stats
|
||||||
|
- **get_my_profile** - Get your user profile information
|
||||||
|
- **get_my_stats** - Get detailed statistics displayed on your profile
|
||||||
|
- **get_extended_stats** - Get additional stats not shown on profile
|
||||||
|
- **get_my_achievements** - Get all your achievements
|
||||||
|
- **get_my_trophies** - Get your trophy collection
|
||||||
|
- **get_trophy_case** - Get your displayed trophy case
|
||||||
|
|
||||||
|
### Games & Activity
|
||||||
|
- **get_activity_feed** - Get recent activity (games, achievements, etc.)
|
||||||
|
- **get_game_details** - Get detailed info about a specific game
|
||||||
|
- **get_unfinished_games** - Get games you haven't completed
|
||||||
|
- **get_streak_game** - Get country streak game details
|
||||||
|
- **analyze_recent_games** - Analyze your recent games with statistics
|
||||||
|
|
||||||
|
### Competitive Modes
|
||||||
|
- **get_battle_royale_game** - Get Battle Royale game stats
|
||||||
|
- **get_duel_game** - Get duel game information
|
||||||
|
- **get_game_lobby** - Get lobby info with player stats
|
||||||
|
- **get_current_season_stats** - Get current season statistics
|
||||||
|
- **get_season_game_info** - Get season info for specific game modes
|
||||||
|
- **get_tournaments** - Get tournament information
|
||||||
|
|
||||||
|
### Challenges
|
||||||
|
- **get_daily_challenge** - Get today's or previous daily challenges
|
||||||
|
- **get_challenge_details** - Get details about a specific challenge
|
||||||
|
|
||||||
|
### Social
|
||||||
|
- **get_friends** - Get your friends list
|
||||||
|
- **get_friends_summary** - Get friends with requests and recommendations
|
||||||
|
- **get_notifications** - Get your notifications
|
||||||
|
- **search_user** - Search for other players
|
||||||
|
|
||||||
|
### Maps
|
||||||
|
- **get_my_maps** - Get maps you've created
|
||||||
|
- **get_liked_maps** - Get maps you've liked
|
||||||
|
- **get_map_info** - Get info about any map
|
||||||
|
- **get_popular_maps** - Get popular/featured/official maps
|
||||||
|
- **get_personalized_maps** - Get map recommendations
|
||||||
|
- **get_map_scores** - Get high scores for a map
|
||||||
|
|
||||||
|
### Explorer & Progress
|
||||||
|
- **get_explorer_progress** - Get your explorer mode progress
|
||||||
|
- **get_objectives** - Get current objectives
|
||||||
|
- **get_unclaimed_objectives** - Get rewards to claim
|
||||||
|
- **get_unclaimed_badges** - Get badges to claim
|
||||||
|
|
||||||
|
### Analysis
|
||||||
|
- **get_performance_summary** - Get comprehensive performance overview
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose installed on your VPS
|
||||||
|
- A GeoGuessr account (Pro subscription recommended for full API access)
|
||||||
|
- Your GeoGuessr `_ncfa` authentication cookie
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Clone or copy the files to your VPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/geoguessr-mcp
|
||||||
|
cd ~/geoguessr-mcp
|
||||||
|
# Copy all the files here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build and run (no configuration required!)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! The server now supports login via credentials, so you don't need to configure anything upfront.
|
||||||
|
|
||||||
|
### 3. (Optional) Configure default authentication
|
||||||
|
|
||||||
|
If you want server-wide default authentication without logging in each time:
|
||||||
|
|
||||||
|
#### Option A: Get your GeoGuessr authentication cookie
|
||||||
|
|
||||||
|
1. Log in to [GeoGuessr](https://www.geoguessr.com) in your browser
|
||||||
|
2. Open Developer Tools (F12 or Ctrl+Shift+I)
|
||||||
|
3. Go to the **Application** tab (Chrome) or **Storage** tab (Firefox)
|
||||||
|
4. Under **Cookies**, find `www.geoguessr.com`
|
||||||
|
5. Look for the cookie named `_ncfa`
|
||||||
|
6. Copy its value
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env # Add your cookie
|
||||||
|
```
|
||||||
|
|
||||||
|
```env
|
||||||
|
GEOGUESSR_NCFA_COOKIE=your_actual_cookie_value_here
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option B: Just use login when connected
|
||||||
|
|
||||||
|
When connected to Claude, simply say:
|
||||||
|
> "Login to GeoGuessr with my email and password"
|
||||||
|
|
||||||
|
Claude will prompt you for credentials and authenticate.
|
||||||
|
|
||||||
|
### 4. Restart if you added environment variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Verify it's running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see:
|
||||||
|
```
|
||||||
|
Starting GeoGuessr MCP Server on 0.0.0.0:8000 with streamable-http transport
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connecting to Claude
|
||||||
|
|
||||||
|
### Claude Desktop (macOS/Windows)
|
||||||
|
|
||||||
|
Add to your Claude Desktop configuration file:
|
||||||
|
|
||||||
|
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"geoguessr": {
|
||||||
|
"type": "streamable-http",
|
||||||
|
"url": "http://YOUR_VPS_IP:8000/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VS Code with Copilot
|
||||||
|
|
||||||
|
1. Open VS Code
|
||||||
|
2. Run the command: `MCP: Add Server`
|
||||||
|
3. Choose "HTTP (Streamable HTTP)" as the transport
|
||||||
|
4. Enter URL: `http://YOUR_VPS_IP:8000/mcp`
|
||||||
|
|
||||||
|
### Claude.ai (if MCP support is enabled)
|
||||||
|
|
||||||
|
Add the server through Claude.ai's integrations settings with:
|
||||||
|
- URL: `http://YOUR_VPS_IP:8000/mcp`
|
||||||
|
- Transport: Streamable HTTP
|
||||||
|
|
||||||
|
## Using with Claude
|
||||||
|
|
||||||
|
Once connected, you can ask Claude questions like:
|
||||||
|
|
||||||
|
**First, authenticate (if not using environment variable):**
|
||||||
|
- "Login to GeoGuessr with email: myemail@example.com password: mypassword"
|
||||||
|
- "Check my GeoGuessr authentication status"
|
||||||
|
|
||||||
|
**Then analyze your data:**
|
||||||
|
- "Show me my GeoGuessr profile and stats"
|
||||||
|
- "Analyze my last 10 games and tell me how I'm doing"
|
||||||
|
- "What achievements have I unlocked?"
|
||||||
|
- "How am I doing in the current competitive season?"
|
||||||
|
- "Show me my activity feed from the last week"
|
||||||
|
- "What maps have I liked?"
|
||||||
|
- "Search for a player named [username]"
|
||||||
|
- "Get the details of my last Battle Royale game"
|
||||||
|
|
||||||
|
**When done (optional):**
|
||||||
|
- "Logout from GeoGuessr"
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Adding SSL with Nginx (Recommended)
|
||||||
|
|
||||||
|
For production, you should add SSL. Create an `nginx.conf`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
upstream mcp_server {
|
||||||
|
server geoguessr-mcp:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://mcp_server;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding API Key Authentication
|
||||||
|
|
||||||
|
For additional security, you can add API key authentication. Modify the server.py to check for an `x-api-key` header.
|
||||||
|
|
||||||
|
### Firewall Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Allow only specific IPs or use a firewall
|
||||||
|
sudo ufw allow from YOUR_IP to any port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "GEOGUESSR_NCFA_COOKIE environment variable not set"
|
||||||
|
|
||||||
|
Make sure your `.env` file exists and contains the cookie:
|
||||||
|
```bash
|
||||||
|
cat .env | grep GEOGUESSR
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cookie expired
|
||||||
|
|
||||||
|
GeoGuessr cookies expire periodically. If API calls start failing, get a fresh cookie from your browser.
|
||||||
|
|
||||||
|
### Connection refused
|
||||||
|
|
||||||
|
1. Check if the container is running: `docker compose ps`
|
||||||
|
2. Check logs: `docker compose logs geoguessr-mcp`
|
||||||
|
3. Verify the port is exposed: `docker port geoguessr-mcp-server`
|
||||||
|
|
||||||
|
### API rate limiting
|
||||||
|
|
||||||
|
GeoGuessr may rate-limit excessive API calls. Space out requests if you're hitting limits.
|
||||||
|
|
||||||
|
## Cookie Security
|
||||||
|
|
||||||
|
⚠️ **Important Security Notes:**
|
||||||
|
|
||||||
|
1. The `_ncfa` cookie provides full access to your GeoGuessr account
|
||||||
|
2. Never share your `.env` file or commit it to version control
|
||||||
|
3. Consider running the server on a private network
|
||||||
|
4. Use SSL in production
|
||||||
|
5. Regularly rotate your cookie
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
To run locally without Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create virtual environment
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # or `venv\Scripts\activate` on Windows
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Set environment variable
|
||||||
|
export GEOGUESSR_NCFA_COOKIE="your_cookie_here"
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
The server uses the unofficial GeoGuessr API. Key endpoints:
|
||||||
|
|
||||||
|
- `https://www.geoguessr.com/api/v3/` - Main API (v3)
|
||||||
|
- `https://www.geoguessr.com/api/v4/` - Newer API (v4)
|
||||||
|
- `https://game-server.geoguessr.com/api/` - Game server API
|
||||||
|
|
||||||
|
Note: This is an unofficial API and may change without notice.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - Feel free to modify and distribute.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This project is not affiliated with, endorsed by, or connected to GeoGuessr AB. Use at your own risk and in accordance with GeoGuessr's Terms of Service.
|
||||||
0
docker-compose.prod.yml
Normal file
0
docker-compose.prod.yml
Normal file
48
docker-compose.yml
Normal file
48
docker-compose.yml
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
geoguessr-mcp:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: geoguessr-mcp-server
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${MCP_PORT:-8000}:8000"
|
||||||
|
environment:
|
||||||
|
# Required: Your GeoGuessr _ncfa cookie for authentication
|
||||||
|
- GEOGUESSR_NCFA_COOKIE=${GEOGUESSR_NCFA_COOKIE}
|
||||||
|
# MCP Server configuration
|
||||||
|
- MCP_TRANSPORT=${MCP_TRANSPORT:-streamable-http}
|
||||||
|
- MCP_HOST=0.0.0.0
|
||||||
|
- MCP_PORT=8000
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
# Optional: Nginx reverse proxy with SSL (recommended for production)
|
||||||
|
# Uncomment and configure if you want SSL termination
|
||||||
|
# nginx:
|
||||||
|
# image: nginx:alpine
|
||||||
|
# container_name: geoguessr-mcp-nginx
|
||||||
|
# restart: unless-stopped
|
||||||
|
# ports:
|
||||||
|
# - "80:80"
|
||||||
|
# - "443:443"
|
||||||
|
# volumes:
|
||||||
|
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
# - ./ssl:/etc/nginx/ssl:ro
|
||||||
|
# depends_on:
|
||||||
|
# - geoguessr-mcp
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: geoguessr-mcp-network
|
||||||
162
pyproject.toml
Normal file
162
pyproject.toml
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
[project]
|
||||||
|
name = "geoguessr-mcp"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "MCP server for analyzing GeoGuessr account data"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
authors = [
|
||||||
|
{name = "Yûki VACHOT", email = "yuki.vachot@datasingularity.fr"}
|
||||||
|
]
|
||||||
|
keywords = ["mcp", "geoguessr", "claude", "ai", "model-context-protocol"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"mcp[cli]>=1.4.0",
|
||||||
|
"httpx>=0.27.0",
|
||||||
|
"uvicorn>=0.30.0",
|
||||||
|
"starlette>=0.38.0",
|
||||||
|
"python-dotenv>=1.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
"pytest-asyncio>=0.23.0",
|
||||||
|
"pytest-cov>=4.1.0",
|
||||||
|
"pytest-mock>=3.12.0",
|
||||||
|
"respx>=0.20.0",
|
||||||
|
"black>=24.0.0",
|
||||||
|
"ruff>=0.3.0",
|
||||||
|
"mypy>=1.8.0",
|
||||||
|
"pre-commit>=3.6.0",
|
||||||
|
"debugpy>=1.8.0",
|
||||||
|
"ipython>=8.0.0",
|
||||||
|
"rich>=13.0.0"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
geoguessr-mcp = "server:main"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/yourusername/geoguessr-mcp"
|
||||||
|
Repository = "https://github.com/yourusername/geoguessr-mcp"
|
||||||
|
Issues = "https://github.com/yourusername/geoguessr-mcp/issues"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["."]
|
||||||
|
|
||||||
|
# Black configuration
|
||||||
|
[tool.black]
|
||||||
|
line-length = 100
|
||||||
|
target-version = ['py310', 'py311', 'py312', 'py313']
|
||||||
|
include = '\.pyi?$'
|
||||||
|
exclude = '''
|
||||||
|
/(
|
||||||
|
\.eggs
|
||||||
|
| \.git
|
||||||
|
| \.hatch
|
||||||
|
| \.mypy_cache
|
||||||
|
| \.tox
|
||||||
|
| \.venv
|
||||||
|
| _build
|
||||||
|
| buck-out
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
)/
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Ruff configuration
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py313"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"F", # Pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"ARG", # flake8-unused-arguments
|
||||||
|
"SIM", # flake8-simplify
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E501", # line too long (handled by black)
|
||||||
|
"B008", # do not perform function calls in argument defaults
|
||||||
|
"C901", # too complex
|
||||||
|
"ARG001", # unused function argument
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"tests/*" = ["ARG001", "S101"]
|
||||||
|
|
||||||
|
# Pytest configuration
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "8.0"
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py", "*_test.py"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = [
|
||||||
|
"-v",
|
||||||
|
"--tb=short",
|
||||||
|
"--strict-markers",
|
||||||
|
]
|
||||||
|
markers = [
|
||||||
|
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
||||||
|
"integration: marks tests as integration tests",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Coverage configuration
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["."]
|
||||||
|
omit = [
|
||||||
|
"tests/*",
|
||||||
|
".venv/*",
|
||||||
|
"*.pyc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"def __repr__",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
"if __name__ == .__main__.:",
|
||||||
|
"if TYPE_CHECKING:",
|
||||||
|
]
|
||||||
|
|
||||||
|
# MyPy configuration
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.13"
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
warn_redundant_casts = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
warn_no_return = true
|
||||||
|
follow_imports = "silent"
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "tests.*"
|
||||||
|
disallow_untyped_defs = false
|
||||||
1089
src/server.py
Normal file
1089
src/server.py
Normal file
File diff suppressed because it is too large
Load diff
0
src/tests/__init__.py
Normal file
0
src/tests/__init__.py
Normal file
201
src/tests/test_server.py
Normal file
201
src/tests/test_server.py
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
"""
|
||||||
|
Tests for GeoGuessr MCP Server
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
# Mock the environment variable before importing server
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_env(monkeypatch):
|
||||||
|
"""Set up environment variables for testing."""
|
||||||
|
monkeypatch.setenv("GEOGUESSR_NCFA_COOKIE", "test_cookie_value")
|
||||||
|
|
||||||
|
|
||||||
|
class TestProfileTools:
|
||||||
|
"""Tests for profile-related tools."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_my_profile_success(self):
|
||||||
|
"""Test successful profile retrieval."""
|
||||||
|
from server import get_my_profile
|
||||||
|
|
||||||
|
mock_response = {
|
||||||
|
"id": "test-user-id",
|
||||||
|
"nick": "TestPlayer",
|
||||||
|
"country": "US",
|
||||||
|
"level": 50,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("server.get_async_session") as mock_session:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
|
||||||
|
mock_http_response = MagicMock()
|
||||||
|
mock_http_response.json.return_value = mock_response
|
||||||
|
mock_http_response.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
mock_client.get = AsyncMock(return_value=mock_http_response)
|
||||||
|
mock_session.return_value = mock_client
|
||||||
|
|
||||||
|
result = await get_my_profile()
|
||||||
|
|
||||||
|
assert result["nick"] == "TestPlayer"
|
||||||
|
assert result["id"] == "test-user-id"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_my_stats_success(self):
|
||||||
|
"""Test successful stats retrieval."""
|
||||||
|
from server import get_my_stats
|
||||||
|
|
||||||
|
mock_response = {
|
||||||
|
"gamesPlayed": 100,
|
||||||
|
"averageScore": 4500,
|
||||||
|
"highScore": 5000,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("server.get_async_session") as mock_session:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
|
||||||
|
mock_http_response = MagicMock()
|
||||||
|
mock_http_response.json.return_value = mock_response
|
||||||
|
mock_http_response.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
mock_client.get = AsyncMock(return_value=mock_http_response)
|
||||||
|
mock_session.return_value = mock_client
|
||||||
|
|
||||||
|
result = await get_my_stats()
|
||||||
|
|
||||||
|
assert result["gamesPlayed"] == 100
|
||||||
|
assert result["averageScore"] == 4500
|
||||||
|
|
||||||
|
|
||||||
|
class TestGameTools:
|
||||||
|
"""Tests for game-related tools."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_game_details_success(self):
|
||||||
|
"""Test successful game details retrieval."""
|
||||||
|
from server import get_game_details
|
||||||
|
|
||||||
|
mock_response = {
|
||||||
|
"token": "ABC123",
|
||||||
|
"type": "standard",
|
||||||
|
"map": {"name": "World"},
|
||||||
|
"player": {
|
||||||
|
"guesses": [
|
||||||
|
{"roundScoreInPoints": 5000, "distanceInMeters": 0},
|
||||||
|
{"roundScoreInPoints": 4500, "distanceInMeters": 100},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("server.get_async_session") as mock_session:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
|
||||||
|
mock_http_response = MagicMock()
|
||||||
|
mock_http_response.json.return_value = mock_response
|
||||||
|
mock_http_response.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
mock_client.get = AsyncMock(return_value=mock_http_response)
|
||||||
|
mock_session.return_value = mock_client
|
||||||
|
|
||||||
|
result = await get_game_details("ABC123")
|
||||||
|
|
||||||
|
assert result["token"] == "ABC123"
|
||||||
|
assert result["map"]["name"] == "World"
|
||||||
|
assert len(result["player"]["guesses"]) == 2
|
||||||
|
|
||||||
|
|
||||||
|
class TestAnalysisTools:
|
||||||
|
"""Tests for analysis tools."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_analyze_recent_games_empty(self):
|
||||||
|
"""Test analysis with no games in feed."""
|
||||||
|
from server import analyze_recent_games
|
||||||
|
|
||||||
|
mock_feed_response = {"entries": []}
|
||||||
|
|
||||||
|
with patch("server.get_async_session") as mock_session:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
|
||||||
|
mock_http_response = MagicMock()
|
||||||
|
mock_http_response.json.return_value = mock_feed_response
|
||||||
|
mock_http_response.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
mock_client.get = AsyncMock(return_value=mock_http_response)
|
||||||
|
mock_session.return_value = mock_client
|
||||||
|
|
||||||
|
result = await analyze_recent_games(count=5)
|
||||||
|
|
||||||
|
assert result["games_analyzed"] == 0
|
||||||
|
assert result["total_score"] == 0
|
||||||
|
assert result["games"] == []
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuthentication:
|
||||||
|
"""Tests for authentication handling."""
|
||||||
|
|
||||||
|
def test_get_ncfa_cookie_missing(self, monkeypatch):
|
||||||
|
"""Test error when cookie is not set."""
|
||||||
|
monkeypatch.delenv("GEOGUESSR_NCFA_COOKIE", raising=False)
|
||||||
|
|
||||||
|
from server import get_ncfa_cookie
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="GEOGUESSR_NCFA_COOKIE"):
|
||||||
|
get_ncfa_cookie()
|
||||||
|
|
||||||
|
def test_get_ncfa_cookie_present(self, monkeypatch):
|
||||||
|
"""Test cookie retrieval when set."""
|
||||||
|
monkeypatch.setenv("GEOGUESSR_NCFA_COOKIE", "my_test_cookie")
|
||||||
|
|
||||||
|
from server import get_ncfa_cookie
|
||||||
|
|
||||||
|
cookie = get_ncfa_cookie()
|
||||||
|
assert cookie == "my_test_cookie"
|
||||||
|
|
||||||
|
|
||||||
|
# Integration tests (marked to skip by default)
|
||||||
|
@pytest.mark.integration
|
||||||
|
class TestIntegration:
|
||||||
|
"""Integration tests that require a real GeoGuessr cookie."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_real_profile_fetch(self):
|
||||||
|
"""Test fetching real profile data."""
|
||||||
|
import os
|
||||||
|
if not os.environ.get("GEOGUESSR_NCFA_COOKIE") or \
|
||||||
|
os.environ.get("GEOGUESSR_NCFA_COOKIE") == "test_cookie_value":
|
||||||
|
pytest.skip("Real NCFA cookie not configured")
|
||||||
|
|
||||||
|
from server import get_my_profile
|
||||||
|
|
||||||
|
result = await get_my_profile()
|
||||||
|
assert "nick" in result
|
||||||
|
assert "id" in result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
"""Run tests automatically when script is executed directly."""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Run pytest with verbose output and show print statements
|
||||||
|
exit_code = pytest.main([
|
||||||
|
__file__,
|
||||||
|
"-v", # Verbose output
|
||||||
|
"-s", # Show print statements
|
||||||
|
"--tb=short", # Shorter traceback format
|
||||||
|
"-m", "not integration", # Skip integration tests by default
|
||||||
|
])
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue