Files
strava-mcp-server/src/strava_mcp_server/tools/clubs.py
T

141 lines
5.5 KiB
Python

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)}")
]