Strava MCP Server

by yorrickjansen
Verified
import logging from collections.abc import AsyncIterator from contextlib import asynccontextmanager from typing import Any, cast from fastapi import FastAPI from mcp.server.fastmcp import Context, FastMCP from strava_mcp.config import StravaSettings from strava_mcp.service import StravaService # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(server: FastMCP) -> AsyncIterator[dict[str, Any]]: """Set up and tear down the Strava service for the MCP server. Args: server: The FastMCP server instance Yields: The lifespan context containing the Strava service """ # Load settings from environment variables try: # Let StravaSettings load values directly from env vars settings = StravaSettings( client_id="", # Will be loaded from environment variables client_secret="", # Will be loaded from environment variables base_url="https://www.strava.com/api/v3", # Default value ) if not settings.client_id: raise ValueError("STRAVA_CLIENT_ID environment variable is not set") if not settings.client_secret: raise ValueError("STRAVA_CLIENT_SECRET environment variable is not set") logger.info("Loaded Strava API settings") except Exception as e: logger.error(f"Failed to load Strava API settings: {str(e)}") raise # FastMCP extends FastAPI, so we can safely cast it for type checking fastapi_app = cast(FastAPI, server) # Initialize the Strava service with the FastAPI app service = StravaService(settings, fastapi_app) logger.info("Initialized Strava service") # Set up authentication routes and initialize await service.initialize() logger.info("Service initialization completed") try: yield {"service": service} finally: # Clean up resources await service.close() logger.info("Closed Strava service") # Create the MCP server mcp = FastMCP( "Strava", description="MCP server for interacting with the Strava API", lifespan=lifespan, client_id="", # Will be loaded from environment variables client_secret="", # Will be loaded from environment variables base_url="", # Will be loaded from environment variables ) @mcp.tool() async def get_user_activities( ctx: Context, before: int | None = None, after: int | None = None, page: int = 1, per_page: int = 30, ) -> list[dict]: """Get the authenticated user's activities. Args: ctx: The MCP request context before: An epoch timestamp for filtering activities before a certain time after: An epoch timestamp for filtering activities after a certain time page: Page number per_page: Number of items per page Returns: List of activities """ try: # Safely access service from context if not ctx.request_context.lifespan_context: raise ValueError("Lifespan context not available") # Cast service to StravaService to satisfy type checker service = cast(StravaService, ctx.request_context.lifespan_context.get("service")) if not service: raise ValueError("Service not available in context") activities = await service.get_activities(before, after, page, per_page) return [activity.model_dump() for activity in activities] except Exception as e: logger.error(f"Error in get_user_activities tool: {str(e)}") raise @mcp.tool() async def get_activity( ctx: Context, activity_id: int, include_all_efforts: bool = False, ) -> dict: """Get details of a specific activity. Args: ctx: The MCP request context activity_id: The ID of the activity include_all_efforts: Whether to include all segment efforts Returns: The activity details """ try: # Safely access service from context if not ctx.request_context.lifespan_context: raise ValueError("Lifespan context not available") # Cast service to StravaService to satisfy type checker service = cast(StravaService, ctx.request_context.lifespan_context.get("service")) if not service: raise ValueError("Service not available in context") activity = await service.get_activity(activity_id, include_all_efforts) return activity.model_dump() except Exception as e: logger.error(f"Error in get_activity tool: {str(e)}") raise @mcp.tool() async def get_activity_segments( ctx: Context, activity_id: int, ) -> list[dict]: """Get the segments of a specific activity. Args: ctx: The MCP request context activity_id: The ID of the activity Returns: List of segment efforts for the activity """ try: # Safely access service from context if not ctx.request_context.lifespan_context: raise ValueError("Lifespan context not available") # Cast service to StravaService to satisfy type checker service = cast(StravaService, ctx.request_context.lifespan_context.get("service")) if not service: raise ValueError("Service not available in context") segments = await service.get_activity_segments(activity_id) return [segment.model_dump() for segment in segments] except Exception as e: logger.error(f"Error in get_activity_segments tool: {str(e)}") raise