CORS Fixed + black and ruff fixes

This commit is contained in:
Yûki VACHOT 2025-12-01 02:55:47 +01:00
parent 5e2f6078a1
commit 3844ffc207
30 changed files with 85 additions and 114 deletions

View file

@ -465,16 +465,6 @@ geoguessr-mcp/
└── Dockerfile
```
### MCP Inspector Tool Test
Tool to inspect MCP server and test connectivity:
Using the [Inspector](https://github.com/modelcontextprotocol/inspector) tool from the MCP project.
Via Docker:
```bash
docker run --rm -p 6274:6274 -p 6277:6277 -e HOST=0.0.0.0 ghcr.io/modelcontextprotocol/inspector:latest
```
## 🤝 Contributing
Contributions are welcome! Please:

View file

@ -4,8 +4,8 @@ GeoGuessr API Endpoints Registry.
Centralized endpoint definitions with metadata for dynamic discovery and routing.
"""
from collections.abc import Callable
from dataclasses import dataclass
from typing import Callable, Optional
from ..config import settings
@ -19,7 +19,7 @@ class EndpointInfo:
description: str = ""
auth_required: bool = True
use_game_server: bool = False
params_builder: Optional[Callable[..., dict]] = None
params_builder: Callable[..., dict] | None = None
class Endpoints:

View file

@ -10,7 +10,6 @@ Classes:
"""
import logging
from typing import Optional
import httpx
@ -45,7 +44,7 @@ class GeoGuessrClient:
async def _get_authenticated_client(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> httpx.AsyncClient:
"""
Get an authenticated HTTP client.
@ -79,9 +78,9 @@ class GeoGuessrClient:
async def request(
self,
endpoint: EndpointInfo,
session_token: Optional[str] = None,
params: Optional[dict] = None,
json_data: Optional[dict] = None,
session_token: str | None = None,
params: dict | None = None,
json_data: dict | None = None,
**kwargs,
) -> DynamicResponse:
"""
@ -154,8 +153,8 @@ class GeoGuessrClient:
async def get(
self,
endpoint: EndpointInfo,
session_token: Optional[str] = None,
params: Optional[dict] = None,
session_token: str | None = None,
params: dict | None = None,
**kwargs,
) -> DynamicResponse:
"""Make a GET request."""
@ -164,8 +163,8 @@ class GeoGuessrClient:
async def post(
self,
endpoint: EndpointInfo,
session_token: Optional[str] = None,
json_data: Optional[dict] = None,
session_token: str | None = None,
json_data: dict | None = None,
**kwargs,
) -> DynamicResponse:
"""Make a POST request."""
@ -174,9 +173,9 @@ class GeoGuessrClient:
async def get_raw(
self,
path: str,
session_token: Optional[str] = None,
session_token: str | None = None,
use_game_server: bool = False,
params: Optional[dict] = None,
params: dict | None = None,
) -> DynamicResponse:
"""
Make a raw GET request to any path.

View file

@ -7,11 +7,10 @@ where each API key can have its own GeoGuessr session.
import asyncio
import logging
from typing import Optional
from ..config import settings
from .session import SessionManager, UserSession
from .user_context import UserContext
from ..config import settings
logger = logging.getLogger(__name__)
@ -146,7 +145,7 @@ class MultiUserSessionManager:
return True
async def get_session_for_api_key(self, api_key: str) -> Optional[UserSession]:
async def get_session_for_api_key(self, api_key: str) -> UserSession | None:
"""
Get the active session for a specific API key.

View file

@ -7,12 +7,11 @@ the authenticated user making the request.
"""
from contextvars import ContextVar
from typing import Optional
from .user_context import UserContext
# Context variable to store the current user context
_current_user_context: ContextVar[Optional[UserContext]] = ContextVar(
_current_user_context: ContextVar[UserContext | None] = ContextVar(
"current_user_context", default=None
)
@ -30,7 +29,7 @@ def set_current_user_context(context: UserContext) -> None:
_current_user_context.set(context)
def get_current_user_context() -> Optional[UserContext]:
def get_current_user_context() -> UserContext | None:
"""
Get the current user context.

View file

@ -7,7 +7,6 @@ import logging
import secrets
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from typing import Optional
import httpx
@ -25,7 +24,7 @@ class UserSession:
username: str
email: str
created_at: datetime = field(default_factory=datetime.now)
expires_at: Optional[datetime] = None
expires_at: datetime | None = None
def is_valid(self) -> bool:
"""Check if the session is still valid."""
@ -37,10 +36,10 @@ class UserSession:
class SessionManager:
"""Manages user sessions for the MCP server."""
def __init__(self, default_cookie: Optional[str] = None):
def __init__(self, default_cookie: str | None = None):
self._sessions: dict[str, UserSession] = {}
self._user_sessions: dict[str, str] = {}
self._default_cookie: Optional[str] = default_cookie or settings.DEFAULT_NCFA_COOKIE
self._default_cookie: str | None = default_cookie or settings.DEFAULT_NCFA_COOKIE
self._lock = asyncio.Lock()
@staticmethod
@ -111,7 +110,7 @@ class SessionManager:
return session_token, session
@staticmethod
def _extract_ncfa_cookie(response: httpx.Response) -> Optional[str]:
def _extract_ncfa_cookie(response: httpx.Response) -> str | None:
"""Extract _ncfa cookie from response."""
# Try cookies jar first
for cookie in response.cookies.jar:
@ -160,7 +159,7 @@ class SessionManager:
return True
return False
async def get_session(self, session_token: Optional[str] = None) -> Optional[UserSession]:
async def get_session(self, session_token: str | None = None) -> UserSession | None:
"""
Get a session by token or return default if available.
@ -203,7 +202,7 @@ class SessionManager:
logger.info("Default NCFA cookie updated")
@staticmethod
async def validate_cookie(cookie: str) -> Optional[dict]:
async def validate_cookie(cookie: str) -> dict | None:
"""
Validate a cookie by making a test request.

View file

@ -6,7 +6,6 @@ is making a request and their associated GeoGuessr session.
"""
from dataclasses import dataclass
from typing import Optional
from .session import UserSession
@ -23,7 +22,7 @@ class UserContext:
api_key: str
"""The API key used to authenticate this request"""
session: Optional[UserSession] = None
session: UserSession | None = None
"""The GeoGuessr session for this user (if logged in)"""
@property
@ -41,7 +40,7 @@ class UserContext:
return f"User-{hash(self.api_key) % 10000:04d}"
@property
def ncfa_cookie(self) -> Optional[str]:
def ncfa_cookie(self) -> str | None:
"""Get the NCFA cookie for this user."""
if self.session:
return self.session.ncfa_cookie

View file

@ -2,7 +2,6 @@
import os
from dataclasses import dataclass, field
from typing import Optional
@dataclass
@ -18,7 +17,7 @@ class Settings:
GEOGUESSR_DOMAIN_NAME: str = "geoguessr.com"
GEOGUESSR_API_URL: str = "https://www.geoguessr.com/api"
GAME_SERVER_URL: str = "https://game-server.geoguessr.com/api"
DEFAULT_NCFA_COOKIE: Optional[str] = field(
DEFAULT_NCFA_COOKIE: str | None = field(
default_factory=lambda: os.getenv("GEOGUESSR_NCFA_COOKIE")
)
@ -37,7 +36,7 @@ class Settings:
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"))
MCP_API_KEYS: str | None = field(default_factory=lambda: os.getenv("MCP_API_KEYS"))
# Logging Configuration
LOG_LEVEL: str = field(default_factory=lambda: os.getenv("LOG_LEVEL", "INFO"))

View file

@ -6,7 +6,6 @@ and attaches user context for multi-user support.
"""
import logging
from typing import Optional
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
@ -26,7 +25,7 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
Authorization header with a Bearer token matching one of the configured API keys.
"""
def __init__(self, app, valid_api_keys: Optional[set[str]] = None):
def __init__(self, app, valid_api_keys: set[str] | None = None):
super().__init__(app)
self.valid_api_keys = valid_api_keys or settings.get_api_keys()
self.enabled = settings.MCP_AUTH_ENABLED

View file

@ -1,7 +1,6 @@
"""Achievement-related data models."""
from dataclasses import dataclass
from typing import Optional
@dataclass
@ -12,9 +11,9 @@ class Achievement:
name: str
description: str = ""
unlocked: bool = False
unlocked_at: Optional[str] = None
unlocked_at: str | None = None
progress: float = 0.0
icon_url: Optional[str] = None
icon_url: str | None = None
@classmethod
def from_api_response(cls, data: dict) -> "Achievement":

View file

@ -1,7 +1,6 @@
"""DailyChallenge-related data models."""
from dataclasses import dataclass, field
from typing import Optional
@dataclass
@ -13,7 +12,7 @@ class DailyChallenge:
date: str = ""
time_limit: int = 0
completed: bool = False
score: Optional[int] = None
score: int | None = None
raw_data: dict = field(default_factory=dict)
@classmethod

View file

@ -1,7 +1,6 @@
"""Game-related data models."""
from dataclasses import dataclass, field
from typing import Optional
from .round_guess import RoundGuess
@ -15,7 +14,7 @@ class Game:
mode: str
total_score: int
rounds: list[RoundGuess] = field(default_factory=list)
created_at: Optional[str] = None
created_at: str | None = None
finished: bool = False
raw_data: dict = field(default_factory=dict)

View file

@ -1,7 +1,6 @@
"""RoundGuess-related data models."""
from dataclasses import dataclass
from typing import Optional
@dataclass
@ -12,8 +11,8 @@ class RoundGuess:
score: int
distance_meters: float
time_seconds: int
lat: Optional[float] = None
lng: Optional[float] = None
lat: float | None = None
lng: float | None = None
country: str = ""
@classmethod

View file

@ -1,7 +1,6 @@
"""UserProfile-related data models."""
from dataclasses import dataclass, field
from typing import Optional
@dataclass
@ -16,7 +15,7 @@ class UserProfile:
created: str = ""
is_verified: bool = False
is_pro: bool = False
avatar_url: Optional[str] = None
avatar_url: str | None = None
raw_data: dict = field(default_factory=dict)
@classmethod

View file

@ -12,9 +12,9 @@ Classes:
"""
import asyncio
import contextlib
import logging
from datetime import UTC, datetime
from typing import Optional
import httpx
@ -119,14 +119,14 @@ class EndpointMonitor:
def __init__(
self,
registry: Optional[SchemaRegistry] = None,
ncfa_cookie: Optional[str] = None,
registry: SchemaRegistry | None = None,
ncfa_cookie: str | None = None,
):
self.registry = registry or schema_registry
self.ncfa_cookie = ncfa_cookie or settings.DEFAULT_NCFA_COOKIE
self.results: list[MonitoringResult] = []
self._running = False
self._task: Optional[asyncio.Task] = None
self._task: asyncio.Task | None = None
async def check_endpoint(
self,
@ -286,10 +286,8 @@ class EndpointMonitor:
self._running = False
if self._task:
self._task.cancel()
try:
with contextlib.suppress(asyncio.CancelledError):
await self._task
except asyncio.CancelledError:
pass
logger.info("Stopped periodic monitoring")
async def _monitoring_loop(self) -> None:

View file

@ -8,7 +8,6 @@ an endpoint, including its availability, response time, and any errors encounter
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Optional
@dataclass
@ -20,5 +19,5 @@ class MonitoringResult:
response_code: int
response_time_ms: float
schema_changed: bool
error_message: Optional[str] = None
error_message: str | None = None
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))

View file

@ -11,7 +11,7 @@ schema information.
import logging
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any, Optional
from typing import Any
from .schema_field import SchemaField
@ -29,8 +29,8 @@ class EndpointSchema:
schema_hash: str = ""
response_code: int = 200
is_available: bool = True
error_message: Optional[str] = None
sample_response: Optional[dict] = None
error_message: str | None = None
sample_response: dict | None = None
def to_dict(self) -> dict:
"""Convert to dictionary for serialization."""

View file

@ -6,7 +6,7 @@ a schema field, including its name, type, and other relevant metadata.
"""
from dataclasses import dataclass
from typing import Any, Optional
from typing import Any
@dataclass
@ -16,6 +16,6 @@ class SchemaField:
name: str
field_type: str
nullable: bool = False
nested_schema: Optional[dict] = None
nested_schema: dict | None = None
example_value: Any = None
description: str = ""

View file

@ -15,7 +15,7 @@ import logging
import tempfile
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Optional
from typing import Any
from .endpoint_schema import EndpointSchema
from .schema_detector import SchemaDetector
@ -32,7 +32,7 @@ class SchemaRegistry:
to track changes over time and adapt automatically.
"""
def __init__(self, cache_dir: Optional[str] = None):
def __init__(self, cache_dir: str | None = None):
self.cache_dir = Path(cache_dir or settings.SCHEMA_CACHE_DIR)
# Try to create the cache directory, fall back to temp if permission denied
@ -187,7 +187,7 @@ class SchemaRegistry:
)
self._save_schemas()
def get_schema(self, endpoint: str) -> Optional[EndpointSchema]:
def get_schema(self, endpoint: str) -> EndpointSchema | None:
"""Get the current schema for an endpoint."""
return self.schemas.get(endpoint)

View file

@ -7,7 +7,6 @@ dynamic data handling and LLM-friendly output formatting.
import logging
from dataclasses import dataclass, field
from typing import Optional
from .game_service import GameService
from .profile_service import ProfileService
@ -61,8 +60,8 @@ class AnalysisService:
def __init__(
self,
client: GeoGuessrClient,
game_service: Optional[GameService] = None,
profile_service: Optional[ProfileService] = None,
game_service: GameService | None = None,
profile_service: ProfileService | None = None,
):
self.client = client
self.game_service = game_service or GameService(client)
@ -155,7 +154,7 @@ class AnalysisService:
async def analyze_recent_games(
self,
count: int = 10,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> dict:
"""
Analyze recent games and provide statistics summary.
@ -181,7 +180,7 @@ class AnalysisService:
async def get_performance_summary(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> dict:
"""
Get a comprehensive performance summary.
@ -246,7 +245,7 @@ class AnalysisService:
async def get_strategy_recommendations(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> dict:
"""
Generate strategy recommendations based on performance analysis.

View file

@ -5,9 +5,8 @@ Handles game history, details, and competitive data with dynamic schema support.
"""
import logging
from typing import Optional
from ..api import Endpoints, DynamicResponse, GeoGuessrClient
from ..api import DynamicResponse, Endpoints, GeoGuessrClient
from ..models import DailyChallenge, Game, SeasonStats
logger = logging.getLogger(__name__)
@ -22,7 +21,7 @@ class GameService:
async def get_game_details(
self,
game_token: str,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> tuple[Game, DynamicResponse]:
"""
Get details for a specific game.
@ -45,7 +44,7 @@ class GameService:
async def get_unfinished_games(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> DynamicResponse:
"""Get list of unfinished games."""
return await self.client.get(Endpoints.GAMES.GET_UNFINISHED_GAMES, session_token)
@ -53,7 +52,7 @@ class GameService:
async def get_streak_game(
self,
game_token: str,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> DynamicResponse:
"""Get streak game details."""
endpoint = Endpoints.GAMES.get_streak_game(game_token)
@ -63,7 +62,7 @@ class GameService:
self,
count: int = 10,
page: int = 0,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> DynamicResponse:
"""
Get the activity feed.
@ -82,7 +81,7 @@ class GameService:
async def get_recent_games(
self,
count: int = 10,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> list[Game]:
"""
Get recent games from the activity feed.
@ -122,7 +121,7 @@ class GameService:
async def get_season_stats(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> tuple[SeasonStats, DynamicResponse]:
"""Get active season statistics."""
response = await self.client.get(
@ -138,7 +137,7 @@ class GameService:
async def get_daily_challenge(
self,
day: str = "today",
session_token: Optional[str] = None,
session_token: str | None = None,
) -> tuple[DailyChallenge, DynamicResponse]:
"""
Get daily challenge.
@ -162,7 +161,7 @@ class GameService:
async def get_battle_royale(
self,
game_id: str,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> DynamicResponse:
"""Get battle royale game details."""
endpoint = Endpoints.GAME_SERVER.get_battle_royale(game_id)
@ -171,7 +170,7 @@ class GameService:
async def get_duel(
self,
duel_id: str,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> DynamicResponse:
"""Get duel game details."""
endpoint = Endpoints.GAME_SERVER.get_duel(duel_id)
@ -179,7 +178,7 @@ class GameService:
async def get_tournaments(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> DynamicResponse:
"""Get tournament information."""
return await self.client.get(Endpoints.GAME_SERVER.GET_TOURNAMENTS, session_token)

View file

@ -6,9 +6,8 @@ dynamic schema adaptation.
"""
import logging
from typing import Optional
from ..api import DynamicResponse, GeoGuessrClient, Endpoints
from ..api import DynamicResponse, Endpoints, GeoGuessrClient
from ..models import Achievement, UserProfile, UserStats
logger = logging.getLogger(__name__)
@ -22,7 +21,7 @@ class ProfileService:
async def get_profile(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> tuple[UserProfile, DynamicResponse]:
"""
Get current user's profile.
@ -40,7 +39,7 @@ class ProfileService:
async def get_stats(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> tuple[UserStats, DynamicResponse]:
"""
Get user statistics.
@ -58,7 +57,7 @@ class ProfileService:
async def get_extended_stats(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> DynamicResponse:
"""
Get extended statistics.
@ -69,7 +68,7 @@ class ProfileService:
async def get_achievements(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> tuple[list[Achievement], DynamicResponse]:
"""
Get user achievements.
@ -96,7 +95,7 @@ class ProfileService:
async def get_public_profile(
self,
user_id: str,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> tuple[UserProfile, DynamicResponse]:
"""Get another user's public profile."""
endpoint = Endpoints.PROFILES.get_public_profile(user_id)
@ -110,14 +109,14 @@ class ProfileService:
async def get_user_maps(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> DynamicResponse:
"""Get user's custom maps."""
return await self.client.get(Endpoints.PROFILES.GET_USER_MAPS, session_token)
async def get_comprehensive_profile(
self,
session_token: Optional[str] = None,
session_token: str | None = None,
) -> dict:
"""
Get a comprehensive profile combining multiple endpoints.

View file

@ -99,7 +99,7 @@ def register_game_tools(mcp: FastMCP, game_service: GameService):
"summary": {
"total_score": sum(g.total_score for g in games),
"average_score": sum(g.total_score for g in games) / len(games) if games else 0,
"maps_played": list(set(g.map_name for g in games)),
"maps_played": list({g.map_name for g in games}),
},
}

View file

@ -9,7 +9,7 @@ from geoguessr_mcp.api import GeoGuessrClient
from geoguessr_mcp.api.dynamic_response import DynamicResponse
from geoguessr_mcp.auth import SessionManager, UserSession
from geoguessr_mcp.config import settings
from geoguessr_mcp.models import RoundGuess, Game
from geoguessr_mcp.models import Game, RoundGuess
from geoguessr_mcp.services import AnalysisService, GameService, ProfileService

View file

@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from geoguessr_mcp.api import DynamicResponse, GeoGuessrClient, EndpointInfo, Endpoints
from geoguessr_mcp.api import DynamicResponse, EndpointInfo, Endpoints, GeoGuessrClient
from geoguessr_mcp.config import settings

View file

@ -187,7 +187,7 @@ class TestAuthenticationFlow:
assert "expired_user" not in session_manager._user_sessions
@pytest.mark.asyncio
async def test_default_cookie_fallback(self, session_manager):
async def test_default_cookie_fallback(self):
"""Test falling back to default cookie when no session exists."""
# Create manager with default cookie
manager_with_default = SessionManager(default_cookie="default_test_cookie")

View file

@ -26,8 +26,8 @@ class TestMultiUserSessionManager:
@pytest.mark.asyncio
async def test_get_user_context_reuses_existing_manager(self, manager):
"""Test that getting context for existing API key reuses the same manager."""
context1 = await manager.get_user_context("existing_key")
context2 = await manager.get_user_context("existing_key")
await manager.get_user_context("existing_key")
await manager.get_user_context("existing_key")
# Should use the same manager instance
assert manager._user_managers["existing_key"] is manager._user_managers["existing_key"]
@ -36,9 +36,9 @@ class TestMultiUserSessionManager:
@pytest.mark.asyncio
async def test_multiple_api_keys_get_separate_managers(self, manager):
"""Test that different API keys get separate session managers."""
context1 = await manager.get_user_context("key1")
context2 = await manager.get_user_context("key2")
context3 = await manager.get_user_context("key3")
await manager.get_user_context("key1")
await manager.get_user_context("key2")
await manager.get_user_context("key3")
assert len(manager._user_managers) == 3
assert manager._user_managers["key1"] is not manager._user_managers["key2"]
@ -61,7 +61,7 @@ class TestMultiUserSessionManager:
assert session is None
@pytest.mark.asyncio
async def test_login_user_creates_manager_if_not_exists(self, manager):
async def test_login_user_creates_manager_if_not_exists(self):
"""Test that login_user creates a manager if it doesn't exist."""
# This test requires mocking the HTTP client for GeoGuessr API
# We'll mark it as a placeholder for now

View file

@ -1,10 +1,9 @@
"""Tests for UserContext class."""
import pytest
from datetime import UTC, datetime, timedelta
from geoguessr_mcp.auth.session import UserSession
from geoguessr_mcp.auth.user_context import UserContext
from datetime import datetime, timedelta, UTC
class TestUserContext:

View file

@ -11,7 +11,7 @@ feeds, recent games, season statistics, and daily challenges.
import pytest
from geoguessr_mcp.models import Game, SeasonStats, DailyChallenge
from geoguessr_mcp.models import DailyChallenge, Game, SeasonStats
class TestGameService:

View file

@ -9,7 +9,7 @@ operations.
import pytest
from geoguessr_mcp.models import UserProfile, UserStats, Achievement
from geoguessr_mcp.models import Achievement, UserProfile, UserStats
class TestProfileService: