import json from mcp.server.fastmcp import FastMCP from mcp.types import TextContent, Annotations, EmbeddedResource, TextResourceContents from strava_mcp_server.strava_client import StravaClient def _resource(uri: str, data) -> EmbeddedResource: return EmbeddedResource( type="resource", resource=TextResourceContents( uri=uri, mimeType="application/json", text=json.dumps(data, indent=2) ), annotations=Annotations(audience=["assistant"]), ) def _user_text(text: str) -> TextContent: return TextContent( type="text", text=text, annotations=Annotations(audience=["user"]) ) def register(mcp: FastMCP, strava: StravaClient) -> None: @mcp.tool() async def list_athlete_clubs(): """ List clubs that the authenticated athlete belongs to. Returns club name, sport type, member count, and city. """ try: clubs = await strava.get_athlete_clubs() data = [ { "id": c.get("id"), "name": c.get("name"), "sport_type": c.get("sport_type"), "city": c.get("city"), "country": c.get("country"), "member_count": c.get("member_count"), "private": c.get("private", False), } for c in clubs ] if not data: md = "### 🏘️ Keine Clubs gefunden." else: md = f"### 🏘️ Clubs ({len(data)})\n" md += "| Name | Sport | Mitglieder | Ort |\n" md += "|------|-------|------------|-----|\n" for c in data: loc = ", ".join(filter(None, [c["city"], c["country"]])) or "N/A" md += f"| {c['name']} | {c['sport_type'] or 'N/A'} | {c['member_count']} | {loc} |\n" return [_user_text(md.strip()), _resource("internal://clubs/list", data)] except Exception as e: return [TextContent(type="text", text=f"Error fetching clubs: {str(e)}")] @mcp.tool() async def get_club(club_id: int): """ Get detailed information about a specific Strava club. :param club_id: The numeric ID of the club. """ try: return await strava.get_club(club_id) except Exception as e: return f"Error fetching club: {str(e)}" @mcp.tool() async def get_club_activities(club_id: int, limit: int = 30): """ List recent activities uploaded by members of a specific club. :param club_id: The numeric ID of the club. :param limit: Number of activities to return (default 30). """ try: activities = await strava.get_club_activities(club_id, per_page=limit) data = [ { "name": a.get("name"), "sport_type": a.get("sport_type") or a.get("type"), "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", "athlete": f"{a.get('athlete', {}).get('firstname')} {a.get('athlete', {}).get('lastname')}", } for a in activities ] if not data: md = "### 🚴 Keine Club-Aktivitäten gefunden." else: md = f"### 🚴 Club-Aktivitäten ({len(data)})\n" md += "| Athlet | Sport | Name | Distanz | Zeit |\n" md += "|--------|-------|------|---------|------|\n" for a in data: md += f"| {a['athlete']} | {a['sport_type']} | {a['name']} | {a['distance']} | {a['moving_time']} |\n" return [ _user_text(md.strip()), _resource(f"internal://clubs/{club_id}/activities", data), ] except Exception as e: return [ TextContent( type="text", text=f"Error fetching club activities: {str(e)}" ) ] @mcp.tool() async def get_club_members(club_id: int, limit: int = 30): """ List members of a specific Strava club. :param club_id: The numeric ID of the club. :param limit: Number of members to return (default 30). """ try: members = await strava.get_club_members(club_id, per_page=limit) data = [ { "id": m.get("id"), "name": f"{m.get('firstname')} {m.get('lastname')}", "city": m.get("city"), "country": m.get("country"), } for m in members ] if not data: md = "### 👥 Keine Mitglieder gefunden." else: md = f"### 👥 Mitglieder ({len(data)})\n" md += "| Name | Ort |\n|------|-----|\n" for m in data: loc = ", ".join(filter(None, [m["city"], m["country"]])) or "N/A" md += f"| {m['name']} | {loc} |\n" return [ _user_text(md.strip()), _resource(f"internal://clubs/{club_id}/members", data), ] except Exception as e: return [ TextContent(type="text", text=f"Error fetching club members: {str(e)}") ]