import json from mcp.server.fastmcp import FastMCP, Context from mcp.types import TextContent from strava_mcp_server.strava_client import StravaClient def register(mcp: FastMCP, strava: StravaClient) -> None: @mcp.tool() async def list_activities( ctx: Context, limit: int = 10, page: int = 1, before: int | None = None, after: int | None = None, ) -> list[TextContent]: """ List recent Strava activities for the authenticated user. :param limit: Number of activities to return per page (default 10, max 200). :param page: Page number for pagination (default 1). :param before: Unix timestamp — only return activities before this time. :param after: Unix timestamp — only return activities after this time. Example: 1704067200 = 2024-01-01 00:00:00 UTC. """ try: await ctx.info(f"Fetching activities (Page {page}, Limit {limit})...") activities = await strava.list_activities( limit=min(limit, 200), page=page, before=before, after=after, ) from datetime import datetime def format_date(d_str): if not d_str: return "N/A" try: d = datetime.fromisoformat(d_str.replace('Z', '+00:00')) return f"{d.day:02d}.{d.month:02d}.{d.year} {d.hour:02d}:{d.minute:02d}" except Exception: return d_str essential_data = [] for a in activities: essential_data.append({ "id": a["id"], "name": a["name"], "sport_type": a.get("sport_type") or a.get("type"), "start_date": format_date(a.get("start_date")), "distance": f"{a.get('distance', 0) / 1000:.2f} km", "moving_time": f"{a.get('moving_time', 0) / 60:.1f} min", "total_elevation_gain": f"{a.get('total_elevation_gain', 0):.0f} m", "average_heartrate": a.get("average_heartrate"), "gear_id": a.get("gear_id"), }) if not essential_data: markdown_summary = "### 📭 Keine Aktivitäten in diesem Zeitraum gefunden." else: markdown_summary = f"### 🚴 Aktivitäten (Seite {page})\n" markdown_summary += "| Datum | Sport | Name | Distanz | Zeit | Höhenmeter | Ø HR |\n" markdown_summary += "|-------|-------|------|---------|------|------------|------|\n" for a in essential_data: hr = f"{a['average_heartrate']:.0f} bpm" if a['average_heartrate'] else "-" markdown_summary += f"| {a['start_date']} | {a['sport_type']} | {a['name']} | {a['distance']} | {a['moving_time']} | {a['total_elevation_gain']} | {hr} |\n" return [ TextContent(type="text", text=markdown_summary.strip()), TextContent( type="text", text=f"Raw Data for Analysis: {json.dumps(essential_data, indent=2)}" ) ] except Exception as e: error_msg = f"Error listing activities: {str(e)}" await ctx.error(error_msg) return [TextContent(type="text", text=error_msg)] @mcp.tool() async def get_activity_details(activity_id: int): """ Get detailed information about a specific Strava activity by its ID. :param activity_id: The numeric ID of the Strava activity. Returns full activity data including segment efforts with string IDs safe for follow-up calls. """ try: activity = await strava.get_activity(activity_id) if "segment_efforts" in activity: for effort in activity["segment_efforts"]: if "id" in effort: effort["id"] = str(effort["id"]) segment = effort.get("segment") if segment and "id" in segment: segment["id"] = str(segment["id"]) if "id" in activity: activity["id"] = str(activity["id"]) return activity except Exception as e: return f"Error fetching activity details: {str(e)}" @mcp.tool() async def get_activity_comments(activity_id: int, limit: int = 30): """ List comments on a specific Strava activity. :param activity_id: The numeric ID of the Strava activity. :param limit: Number of comments to return (default 30). """ try: comments = await strava.get_activity_comments(activity_id, per_page=limit) return [ { "id": c.get("id"), "text": c.get("text"), "athlete": f"{c.get('athlete', {}).get('firstname')} {c.get('athlete', {}).get('lastname')}", "created_at": c.get("created_at"), } for c in comments ] except Exception as e: return f"Error fetching comments: {str(e)}" @mcp.tool() async def get_activity_kudoers(activity_id: int, limit: int = 30): """ List athletes who gave kudos on a specific Strava activity. :param activity_id: The numeric ID of the Strava activity. :param limit: Number of kudoers to return (default 30). """ try: kudoers = await strava.get_activity_kudoers(activity_id, per_page=limit) return [ { "id": k.get("id"), "name": f"{k.get('firstname')} {k.get('lastname')}", "city": k.get("city"), "country": k.get("country"), } for k in kudoers ] except Exception as e: return f"Error fetching kudoers: {str(e)}" @mcp.tool() async def get_laps_by_activity_id(activity_id: int): """ List all laps for a specific Strava activity. :param activity_id: The numeric ID of the Strava activity. Returns lap number, name, distance, moving time, average speed, and elevation gain. """ try: laps = await strava.get_activity_laps(activity_id) return [ { "lap_index": lap.get("lap_index"), "name": lap.get("name"), "distance": f"{lap.get('distance', 0) / 1000:.3f} km", "moving_time": f"{lap.get('moving_time', 0) / 60:.1f} min", "elapsed_time": f"{lap.get('elapsed_time', 0) / 60:.1f} min", "average_speed": f"{lap.get('average_speed', 0) * 3.6:.2f} km/h", "max_speed": f"{lap.get('max_speed', 0) * 3.6:.2f} km/h", "elevation_gain": f"{lap.get('total_elevation_gain', 0):.0f} m", "average_heartrate": lap.get("average_heartrate"), "max_heartrate": lap.get("max_heartrate"), } for lap in laps ] except Exception as e: return f"Error fetching laps: {str(e)}" @mcp.tool() async def get_zones_by_activity_id(activity_id: int): """ Get heart rate and power zones distribution for a specific Strava activity. :param activity_id: The numeric ID of the Strava activity. Returns zone type, score, and time spent in each zone bucket. """ try: zones = await strava.get_activity_zones(activity_id) result = [] for zone in zones: buckets = [ { "zone": i + 1, "min": b.get("min", 0), "max": b.get("max", -1), "time_in_zone": f"{b.get('time', 0) / 60:.1f} min", } for i, b in enumerate(zone.get("distribution_buckets", [])) ] result.append({ "type": zone.get("type", "unknown"), "sensor_based": zone.get("sensor_based", False), "score": zone.get("score"), "custom_zones": zone.get("custom_zones", False), "points": zone.get("points"), "distribution_buckets": buckets, }) return result except Exception as e: return f"Error fetching activity zones: {str(e)}" @mcp.tool() async def get_activity_streams( activity_id: int, keys: str = "time,distance,heartrate,altitude,velocity_smooth,cadence,watts", ): """ Get raw data streams for a specific Strava activity. :param activity_id: The numeric ID of the Strava activity. :param keys: Comma-separated list of stream types to fetch. Available: time, distance, latlng, altitude, velocity_smooth, heartrate, cadence, watts, temp, moving, grade_smooth. Returns one data array per requested stream type. """ try: key_list = [k.strip() for k in keys.split(",") if k.strip()] return await strava.get_activity_streams(activity_id, key_list) except Exception as e: return f"Error fetching activity streams: {str(e)}"