Rework
This commit is contained in:
parent
ce5abcc217
commit
cfe4a641a6
40 changed files with 1728 additions and 1445 deletions
31
src/geoguessr_mcp/tools/__init__.py
Normal file
31
src/geoguessr_mcp/tools/__init__.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""Register all MCP tools."""
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from ..api.client import GeoguessrClient
|
||||
from ..auth.session import SessionManager
|
||||
from ..services.analysis_service import AnalysisService
|
||||
from ..services.game_service import GameService
|
||||
from ..services.profile_service import ProfileService
|
||||
from .analysis_tools import register_analysis_tools
|
||||
from .auth_tools import register_auth_tools
|
||||
from .game_tools import register_game_tools
|
||||
from .profile_tools import register_profile_tools
|
||||
|
||||
|
||||
def register_all_tools(mcp: FastMCP):
|
||||
"""Register all tools with the MCP server."""
|
||||
# Initialize dependencies
|
||||
session_manager = SessionManager()
|
||||
client = GeoguessrClient(session_manager)
|
||||
|
||||
# Initialize services
|
||||
profile_service = ProfileService(client)
|
||||
game_service = GameService(client)
|
||||
analysis_service = AnalysisService()
|
||||
|
||||
# Register tools
|
||||
register_auth_tools(mcp, session_manager)
|
||||
register_profile_tools(mcp, profile_service)
|
||||
register_game_tools(mcp, game_service)
|
||||
register_analysis_tools(mcp, analysis_service, game_service)
|
||||
124
src/geoguessr_mcp/tools/analysis_tools.py
Normal file
124
src/geoguessr_mcp/tools/analysis_tools.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
|
||||
@mcp.tool()
|
||||
async def analyze_recent_games(count: int = 10) -> dict:
|
||||
"""
|
||||
Analyze recent games and provide statistics summary.
|
||||
Fetches recent games from the activity feed and calculates aggregate statistics.
|
||||
|
||||
Args:
|
||||
count: Number of recent games to analyze (default: 10)
|
||||
"""
|
||||
async with await get_async_session() as client:
|
||||
# Get activity feed
|
||||
feed_response = await client.get(
|
||||
f"{GEOGUESSR_BASE_URL}/v4/feed/private",
|
||||
params={"count": count * 2, "page": 0}
|
||||
)
|
||||
feed_response.raise_for_status()
|
||||
feed = feed_response.json()
|
||||
|
||||
games_analyzed = []
|
||||
total_score = 0
|
||||
total_rounds = 0
|
||||
perfect_rounds = 0
|
||||
|
||||
for entry in feed.get("entries", []):
|
||||
if entry.get("type") == "PlayedGame" and len(games_analyzed) < count:
|
||||
game_token = entry.get("payload", {}).get("gameToken")
|
||||
if game_token:
|
||||
try:
|
||||
game_response = await client.get(f"{GEOGUESSR_BASE_URL}/v3/games/{game_token}")
|
||||
if game_response.status_code == 200:
|
||||
game = game_response.json()
|
||||
|
||||
game_info = {
|
||||
"token": game_token,
|
||||
"map": game.get("map", {}).get("name", "Unknown"),
|
||||
"mode": game.get("type", "Unknown"),
|
||||
"total_score": 0,
|
||||
"rounds": []
|
||||
}
|
||||
|
||||
for round_data in game.get("player", {}).get("guesses", []):
|
||||
round_score = round_data.get("roundScoreInPoints", 0)
|
||||
game_info["total_score"] += round_score
|
||||
game_info["rounds"].append({
|
||||
"score": round_score,
|
||||
"distance": round_data.get("distanceInMeters", 0),
|
||||
"time": round_data.get("time", 0)
|
||||
})
|
||||
|
||||
total_rounds += 1
|
||||
if round_score == 5000:
|
||||
perfect_rounds += 1
|
||||
|
||||
total_score += game_info["total_score"]
|
||||
games_analyzed.append(game_info)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch game {game_token}: {e}")
|
||||
|
||||
return {
|
||||
"games_analyzed": len(games_analyzed),
|
||||
"total_score": total_score,
|
||||
"average_score": total_score / len(games_analyzed) if games_analyzed else 0,
|
||||
"total_rounds": total_rounds,
|
||||
"perfect_rounds": perfect_rounds,
|
||||
"perfect_round_percentage": (perfect_rounds / total_rounds * 100) if total_rounds > 0 else 0,
|
||||
"games": games_analyzed
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_performance_summary() -> dict:
|
||||
"""
|
||||
Get a comprehensive performance summary combining profile stats,
|
||||
achievements, and season information.
|
||||
"""
|
||||
async with await get_async_session() as client:
|
||||
results = {}
|
||||
|
||||
# Get profile
|
||||
try:
|
||||
profile_response = await client.get(f"{GEOGUESSR_BASE_URL}/v3/profiles")
|
||||
profile_response.raise_for_status()
|
||||
results["profile"] = profile_response.json()
|
||||
except Exception as e:
|
||||
results["profile_error"] = str(e)
|
||||
|
||||
# Get stats
|
||||
try:
|
||||
stats_response = await client.get(f"{GEOGUESSR_BASE_URL}/v3/profiles/stats")
|
||||
stats_response.raise_for_status()
|
||||
results["stats"] = stats_response.json()
|
||||
except Exception as e:
|
||||
results["stats_error"] = str(e)
|
||||
|
||||
# Get extended stats
|
||||
try:
|
||||
extended_response = await client.get(f"{GEOGUESSR_BASE_URL}/v4/stats/me")
|
||||
extended_response.raise_for_status()
|
||||
results["extended_stats"] = extended_response.json()
|
||||
except Exception as e:
|
||||
results["extended_stats_error"] = str(e)
|
||||
|
||||
# Get season stats
|
||||
try:
|
||||
season_response = await client.get(f"{GEOGUESSR_BASE_URL}/v4/seasons/active/stats")
|
||||
season_response.raise_for_status()
|
||||
results["current_season"] = season_response.json()
|
||||
except Exception as e:
|
||||
results["season_error"] = str(e)
|
||||
|
||||
# Get achievements
|
||||
try:
|
||||
achievements_response = await client.get(f"{GEOGUESSR_BASE_URL}/v3/profiles/achievements")
|
||||
achievements_response.raise_for_status()
|
||||
achievements = achievements_response.json()
|
||||
results["achievements_summary"] = {
|
||||
"total": len(achievements) if isinstance(achievements, list) else 0,
|
||||
"achievements": achievements
|
||||
}
|
||||
except Exception as e:
|
||||
results["achievements_error"] = str(e)
|
||||
|
||||
return results
|
||||
182
src/geoguessr_mcp/tools/auth_tools.py
Normal file
182
src/geoguessr_mcp/tools/auth_tools.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
"""MCP tools for auth operations."""
|
||||
import logging
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from ..auth.session import SessionManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register_auth_tools(mcp: FastMCP, session_manager: SessionManager):
|
||||
"""Register auth-related tools."""
|
||||
|
||||
@mcp.tool()
|
||||
async def login(email: str, password: str) -> dict:
|
||||
"""
|
||||
Authenticate with Geoguessr using your email and password.
|
||||
This creates a session that will be used for all later API calls.
|
||||
|
||||
Args:
|
||||
email: Your Geoguessr account email
|
||||
password: Your Geoguessr account password
|
||||
|
||||
Returns:
|
||||
Session information including username and session token
|
||||
|
||||
Note: Your credentials are only used to get an authentication token
|
||||
from Geoguessr. They are not stored on the server.
|
||||
"""
|
||||
|
||||
try:
|
||||
session_token, session = await session_manager.login(email, password)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Successfully logged in as {session.username}",
|
||||
"username": session.username,
|
||||
"user_id": session.user_id,
|
||||
"session_token": session_token,
|
||||
"expires_at": session.expires_at.isoformat() if session.expires_at else None,
|
||||
}
|
||||
except ValueError as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"Login error: {e}")
|
||||
return {"success": False, "error": f"An unexpected error occurred: {str(e)}"}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def logout() -> dict:
|
||||
"""
|
||||
Logout from the current Geoguessr session.
|
||||
This invalidates the current session token.
|
||||
"""
|
||||
global _current_session_token
|
||||
|
||||
if _current_session_token:
|
||||
success = await session_manager.logout(_current_session_token)
|
||||
_current_session_token = None
|
||||
return {
|
||||
"success": success,
|
||||
"message": "Successfully logged out" if success else "No active session to logout",
|
||||
}
|
||||
|
||||
return {"success": False, "message": "No active session"}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def set_session_token(token: str) -> dict:
|
||||
"""
|
||||
Set an existing session token for authentication.
|
||||
Use this if you have a previously obtained session token.
|
||||
|
||||
Args:
|
||||
token: A valid session token from a previous login
|
||||
"""
|
||||
global _current_session_token
|
||||
|
||||
session = await session_manager.get_session(token)
|
||||
if session and session.is_valid():
|
||||
_current_session_token = token
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Session set for user {session.username}",
|
||||
"username": session.username,
|
||||
}
|
||||
|
||||
return {"success": False, "error": "Invalid or expired session token"}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def set_ncfa_cookie(cookie: str) -> dict:
|
||||
"""
|
||||
Directly set the _ncfa cookie for authentication.
|
||||
Use this if you've manually extracted the cookie from your browser.
|
||||
|
||||
Args:
|
||||
cookie: The _ncfa cookie value from your browser
|
||||
|
||||
Note: This sets the cookie as the default for all requests.
|
||||
"""
|
||||
global _current_session_token
|
||||
|
||||
# Validate the cookie by making a test request
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
client.cookies.set("_ncfa", cookie, domain="www.geoguessr.com")
|
||||
response = await client.get(f"{GEOGUESSR_BASE_URL}/v3/profiles")
|
||||
|
||||
if response.status_code != 200:
|
||||
return {"success": False, "error": "Invalid cookie - authentication failed"}
|
||||
|
||||
profile = response.json()
|
||||
|
||||
# Create a session from the cookie
|
||||
session = UserSession(
|
||||
ncfa_cookie=cookie,
|
||||
user_id=profile.get("id", ""),
|
||||
username=profile.get("nick", ""),
|
||||
email="manual@cookie",
|
||||
expires_at=datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=30),
|
||||
)
|
||||
|
||||
# Store as a session
|
||||
session_token = secrets.token_urlsafe(32)
|
||||
async with session_manager._lock:
|
||||
session_manager._sessions[session_token] = session
|
||||
session_manager._user_sessions[session.user_id] = session_token
|
||||
|
||||
_current_session_token = session_token
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Cookie set successfully. Authenticated as {session.username}",
|
||||
"username": session.username,
|
||||
"user_id": session.user_id,
|
||||
"session_token": session_token,
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_auth_status() -> dict:
|
||||
"""
|
||||
Check the current authentication status.
|
||||
Returns information about the current session or authentication method.
|
||||
"""
|
||||
global _current_session_token
|
||||
|
||||
# Check for active session
|
||||
if _current_session_token:
|
||||
session = await session_manager.get_session(_current_session_token)
|
||||
if session and session.is_valid():
|
||||
return {
|
||||
"authenticated": True,
|
||||
"method": "session",
|
||||
"username": session.username,
|
||||
"user_id": session.user_id,
|
||||
"expires_at": session.expires_at.isoformat() if session.expires_at else None,
|
||||
}
|
||||
|
||||
# Check for environment variable
|
||||
env_cookie = os.environ.get("GEOGUESSR_NCFA_COOKIE")
|
||||
if env_cookie:
|
||||
# Validate the environment cookie
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
client.cookies.set("_ncfa", env_cookie, domain="www.geoguessr.com")
|
||||
response = await client.get(f"{GEOGUESSR_BASE_URL}/v3/profiles")
|
||||
|
||||
if response.status_code == 200:
|
||||
profile = response.json()
|
||||
return {
|
||||
"authenticated": True,
|
||||
"method": "environment_variable",
|
||||
"username": profile.get("nick", "Unknown"),
|
||||
"user_id": profile.get("id", "Unknown"),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"authenticated": False,
|
||||
"message": "Not authenticated. Use 'login' with your GeoGuessr credentials or 'set_ncfa_cookie' with a valid cookie.",
|
||||
}
|
||||
0
src/geoguessr_mcp/tools/competitive_tools.py
Normal file
0
src/geoguessr_mcp/tools/competitive_tools.py
Normal file
0
src/geoguessr_mcp/tools/game_tools.py
Normal file
0
src/geoguessr_mcp/tools/game_tools.py
Normal file
26
src/geoguessr_mcp/tools/profile_tools.py
Normal file
26
src/geoguessr_mcp/tools/profile_tools.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"""MCP tools for profile operations."""
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from ..services.profile_service import ProfileService
|
||||
|
||||
|
||||
def register_profile_tools(mcp: FastMCP, profile_service: ProfileService):
|
||||
"""Register profile-related tools."""
|
||||
|
||||
@mcp.tool()
|
||||
async def get_my_profile(session_token: str = "") -> dict:
|
||||
"""Get the current user's profile information."""
|
||||
profile = await profile_service.get_profile(session_token if session_token else None)
|
||||
return {
|
||||
"id": profile.id,
|
||||
"nick": profile.nick,
|
||||
"email": profile.email,
|
||||
"country": profile.country,
|
||||
"level": profile.level,
|
||||
}
|
||||
|
||||
@mcp.tool()
|
||||
async def get_my_stats(session_token: str = "") -> dict:
|
||||
"""Get the current user's statistics."""
|
||||
return await profile_service.get_stats(session_token if session_token else None)
|
||||
Loading…
Add table
Add a link
Reference in a new issue