Initial commit: Modularized Strava MCP Server with UV and Hatchling
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
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 get_athlete_profile(ctx: Context) -> list[TextContent]:
|
||||
"""
|
||||
Get the authenticated Strava athlete's profile.
|
||||
Returns name, city, country, follower count, and other profile details.
|
||||
"""
|
||||
try:
|
||||
|
||||
await ctx.info("Fetching athlete profile...")
|
||||
|
||||
athlete = await strava.get_athlete()
|
||||
|
||||
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}.{d.month}.{d.year}"
|
||||
except Exception:
|
||||
return d_str
|
||||
|
||||
location_parts = [p for p in (athlete.get('city'), athlete.get('state'), athlete.get('country')) if p]
|
||||
location = ", ".join(location_parts) if location_parts else "N/A"
|
||||
|
||||
essential_data = {
|
||||
"id": athlete.get("id"),
|
||||
"username": athlete.get("username"),
|
||||
"name": f"{athlete.get('firstname')} {athlete.get('lastname')}".strip(),
|
||||
"location": location,
|
||||
"sex": athlete.get("sex"),
|
||||
"weight": athlete.get("weight"),
|
||||
"measurement_units": athlete.get("measurement_preference"),
|
||||
"is_premium": athlete.get("premium", False),
|
||||
"profile_medium": athlete.get("profile_medium"),
|
||||
"created_at": athlete.get("created_at"),
|
||||
"updated_at": athlete.get("updated_at"),
|
||||
"bio": athlete.get("bio"),
|
||||
"follower_count": athlete.get("follower_count"),
|
||||
"friend_count": athlete.get("friend_count"),
|
||||
}
|
||||
|
||||
markdown_summary = f"""
|
||||
👤 **Profile for {essential_data['name']}** (ID: {essential_data['id']})
|
||||
- Username: {essential_data['username'] or 'N/A'}
|
||||
- Location: {essential_data['location']}
|
||||
- Sex: {essential_data['sex'] or 'N/A'}
|
||||
- Weight: {essential_data['weight'] or 'N/A'} kg
|
||||
- Measurement Units: {essential_data['measurement_units'] or 'N/A'}
|
||||
- Strava Summit Member: {'Yes' if essential_data['is_premium'] else 'No'}
|
||||
- Profile Image (Medium): {essential_data['profile_medium'] or 'N/A'}
|
||||
- Joined Strava: {format_date(essential_data['created_at'])}
|
||||
- Last Updated: {format_date(essential_data['updated_at'])}
|
||||
""".strip()
|
||||
|
||||
return [
|
||||
TextContent(type="text", text=markdown_summary),
|
||||
TextContent(
|
||||
type="text",
|
||||
text=f"Raw Data for Analysis: {json.dumps(essential_data, indent=2)}"
|
||||
)
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error fetching athlete profile: {str(e)}"
|
||||
await ctx.error(error_msg)
|
||||
return [TextContent(type="text", text=error_msg)]
|
||||
|
||||
@mcp.tool()
|
||||
async def get_athlete_zones():
|
||||
"""
|
||||
Get the heart rate and power zones configured for the authenticated athlete.
|
||||
Returns zone boundaries for both heart rate and power (if a power meter is configured).
|
||||
"""
|
||||
try:
|
||||
return await strava.get_athlete_zones()
|
||||
except Exception as e:
|
||||
return f"Error fetching athlete zones: {str(e)}"
|
||||
|
||||
@mcp.tool()
|
||||
async def get_athlete_stats():
|
||||
"""
|
||||
Get cumulative training statistics for the authenticated Strava athlete.
|
||||
Includes all-time, year-to-date, and recent (4-week) totals for runs, rides, and swims.
|
||||
"""
|
||||
import json
|
||||
try:
|
||||
stats = await strava.get_athlete_stats()
|
||||
|
||||
def fmt_sport(s: dict) -> dict:
|
||||
return {
|
||||
"count": s.get("count", 0),
|
||||
"distance": f"{s.get('distance', 0) / 1000:.1f} km",
|
||||
"moving_time": f"{s.get('moving_time', 0) / 3600:.1f} h",
|
||||
"elevation_gain": f"{s.get('elevation_gain', 0):.0f} m",
|
||||
}
|
||||
|
||||
result = {
|
||||
"all_time": {
|
||||
"runs": fmt_sport(stats.get("all_run_totals", {})),
|
||||
"rides": fmt_sport(stats.get("all_ride_totals", {})),
|
||||
"swims": fmt_sport(stats.get("all_swim_totals", {})),
|
||||
},
|
||||
"ytd": {
|
||||
"runs": fmt_sport(stats.get("ytd_run_totals", {})),
|
||||
"rides": fmt_sport(stats.get("ytd_ride_totals", {})),
|
||||
"swims": fmt_sport(stats.get("ytd_swim_totals", {})),
|
||||
},
|
||||
"recent_4_weeks": {
|
||||
"runs": fmt_sport(stats.get("recent_run_totals", {})),
|
||||
"rides": fmt_sport(stats.get("recent_ride_totals", {})),
|
||||
"swims": fmt_sport(stats.get("recent_swim_totals", {})),
|
||||
},
|
||||
}
|
||||
return json.dumps(result, indent=2)
|
||||
except Exception as e:
|
||||
return f"Error fetching athlete stats: {str(e)}"
|
||||
Reference in New Issue
Block a user