style: refactor codebase to adhere to PEP 8 formatting standards throughout all source files

This commit is contained in:
2026-05-12 23:55:58 +02:00
parent bcc11cb07e
commit 8e9e4c01d4
17 changed files with 443 additions and 171 deletions
+2
View File
@@ -3,6 +3,7 @@ MCP Tool definitions for the Strava MCP Server.
Register all tools by calling register_tools(mcp, strava).
"""
from mcp.server.fastmcp import FastMCP
from strava_mcp_server.strava_client import StravaClient
@@ -15,6 +16,7 @@ from . import segment_efforts
from . import gear
from . import prompts
def register_tools(mcp: FastMCP, strava: StravaClient) -> None:
"""Register all available tools and prompts."""
athlete.register(mcp, strava)
+95 -30
View File
@@ -2,7 +2,12 @@ import json
from mcp.server.fastmcp import FastMCP, Context
from mcp.types import TextContent, Annotations, EmbeddedResource, TextResourceContents
from strava_mcp_server.strava_client import StravaClient
from strava_mcp_server.utils import parse_iso_to_unix, format_date_iso, format_date_human
from strava_mcp_server.utils import (
parse_iso_to_unix,
format_date_iso,
format_date_human,
)
def _resource(uri: str, data) -> EmbeddedResource:
"""Helper: return an assistant-facing EmbeddedResource with application/json."""
@@ -16,9 +21,13 @@ def _resource(uri: str, data) -> EmbeddedResource:
annotations=Annotations(audience=["assistant"]),
)
def _user_text(text: str) -> TextContent:
"""Helper: return a user-facing TextContent."""
return TextContent(type="text", text=text, annotations=Annotations(audience=["user"]))
return TextContent(
type="text", text=text, annotations=Annotations(audience=["user"])
)
def register(mcp: FastMCP, strava: StravaClient) -> None:
@mcp.tool()
@@ -49,27 +58,39 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
essential_data = []
for a in activities:
start_date_raw = a.get("start_date")
essential_data.append({
"id": a["id"],
"name": a["name"],
"sport_type": a.get("sport_type") or a.get("type"),
"start_date": format_date_iso(start_date_raw),
"start_date_local": format_date_human(start_date_raw),
"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"),
})
essential_data.append(
{
"id": a["id"],
"name": a["name"],
"sport_type": a.get("sport_type") or a.get("type"),
"start_date": format_date_iso(start_date_raw),
"start_date_local": format_date_human(start_date_raw),
"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."
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"
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 "-"
hr = (
f"{a['average_heartrate']:.0f} bpm"
if a["average_heartrate"]
else "-"
)
markdown_summary += f"| {a['start_date_local']} | {a['sport_type']} | {a['name']} | {a['distance']} | {a['moving_time']} | {a['total_elevation_gain']} | {hr} |\n"
return [
@@ -106,10 +127,26 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
dist = f"{activity.get('distance', 0) / 1000:.2f} km"
time = f"{activity.get('moving_time', 0) / 60:.1f} min"
elev = f"{activity.get('total_elevation_gain', 0):.0f} m"
avg_hr = f"{activity.get('average_heartrate', 0):.0f} bpm" if activity.get("average_heartrate") else "N/A"
max_hr = f"{activity.get('max_heartrate', 0):.0f} bpm" if activity.get("max_heartrate") else "N/A"
avg_spd = f"{activity.get('average_speed', 0) * 3.6:.1f} km/h" if activity.get("average_speed") else "N/A"
avg_w = f"{activity.get('average_watts', 0):.0f} W" if activity.get("average_watts") else "N/A"
avg_hr = (
f"{activity.get('average_heartrate', 0):.0f} bpm"
if activity.get("average_heartrate")
else "N/A"
)
max_hr = (
f"{activity.get('max_heartrate', 0):.0f} bpm"
if activity.get("max_heartrate")
else "N/A"
)
avg_spd = (
f"{activity.get('average_speed', 0) * 3.6:.1f} km/h"
if activity.get("average_speed")
else "N/A"
)
avg_w = (
f"{activity.get('average_watts', 0):.0f} W"
if activity.get("average_watts")
else "N/A"
)
gear = activity.get("gear_id") or "N/A"
n_efforts = len(activity.get("segment_efforts", []))
@@ -133,7 +170,11 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
_resource(f"internal://activities/{activity_id}", activity),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching activity details: {str(e)}")]
return [
TextContent(
type="text", text=f"Error fetching activity details: {str(e)}"
)
]
@mcp.tool()
async def get_activity_comments(activity_id: int, limit: int = 30):
@@ -159,7 +200,10 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
md = f"### 💬 Kommentare ({len(data)})\n"
for c in data:
md += f"- **{c['athlete']}** ({format_date_human(c['created_at'])}): {c['text']}\n"
return [_user_text(md.strip()), _resource(f"internal://activities/{activity_id}/comments", data)]
return [
_user_text(md.strip()),
_resource(f"internal://activities/{activity_id}/comments", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching comments: {str(e)}")]
@@ -189,7 +233,10 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
for k in data:
loc = ", ".join(filter(None, [k["city"], k["country"]])) or "N/A"
md += f"| {k['name']} | {loc} |\n"
return [_user_text(md.strip()), _resource(f"internal://activities/{activity_id}/kudoers", data)]
return [
_user_text(md.strip()),
_resource(f"internal://activities/{activity_id}/kudoers", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching kudoers: {str(e)}")]
@@ -224,9 +271,16 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
md += "| # | Distanz | Zeit | Ø Speed | Ø HR |\n"
md += "|---|---------|------|---------|------|\n"
for lap in data:
hr = f"{lap['average_heartrate']:.0f} bpm" if lap['average_heartrate'] else "-"
hr = (
f"{lap['average_heartrate']:.0f} bpm"
if lap["average_heartrate"]
else "-"
)
md += f"| {lap['lap_index']} | {lap['distance']} | {lap['moving_time']} | {lap['average_speed']} | {hr} |\n"
return [_user_text(md.strip()), _resource(f"internal://activities/{activity_id}/laps", data)]
return [
_user_text(md.strip()),
_resource(f"internal://activities/{activity_id}/laps", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching laps: {str(e)}")]
@@ -260,15 +314,26 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
"distribution_buckets": buckets,
}
data.append(zone_data)
label = "Herzfrequenz" if zone_data["type"] == "heartrate" else "Leistung (Power)"
label = (
"Herzfrequenz"
if zone_data["type"] == "heartrate"
else "Leistung (Power)"
)
md += f"#### {label}\n| Zone | Bereich | Zeit |\n|------|---------|------|\n"
for b in buckets:
max_val = "max" if b["max"] == -1 else str(b["max"])
md += f"| {b['zone']} | {b['min']} {max_val} | {b['time_in_zone']} |\n"
md += "\n"
return [_user_text(md.strip()), _resource(f"internal://activities/{activity_id}/zones", data)]
return [
_user_text(md.strip()),
_resource(f"internal://activities/{activity_id}/zones", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching activity zones: {str(e)}")]
return [
TextContent(
type="text", text=f"Error fetching activity zones: {str(e)}"
)
]
@mcp.tool()
async def get_activity_streams(
+41 -33
View File
@@ -4,6 +4,7 @@ from mcp.types import TextContent, Annotations, EmbeddedResource, TextResourceCo
from strava_mcp_server.strava_client import StravaClient
from strava_mcp_server.utils import format_date_iso, format_date_human
def register(mcp: FastMCP, strava: StravaClient) -> None:
@mcp.tool()
async def get_athlete_profile(ctx: Context):
@@ -12,12 +13,19 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
Returns name, city, country, follower count, and other profile details.
"""
try:
await ctx.info("Fetching athlete profile...")
athlete = await strava.get_athlete()
location_parts = [p for p in (athlete.get('city'), athlete.get('state'), athlete.get('country')) if p]
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 = {
@@ -38,33 +46,33 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
}
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_human(essential_data['created_at'])}
- Last Updated: {format_date_human(essential_data['updated_at'])}
👤 **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_human(essential_data["created_at"])}
- Last Updated: {format_date_human(essential_data["updated_at"])}
""".strip()
return [
TextContent(
type="text",
type="text",
text=markdown_summary,
annotations=Annotations(audience=["user"])
annotations=Annotations(audience=["user"]),
),
EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri="internal://athlete/profile",
mimeType="application/json",
text=json.dumps(essential_data, indent=2)
text=json.dumps(essential_data, indent=2),
),
annotations=Annotations(audience=["assistant"])
)
annotations=Annotations(audience=["assistant"]),
),
]
except Exception as e:
@@ -81,9 +89,9 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
try:
await ctx.info("Fetching athlete zones...")
zones = await strava.get_athlete_zones()
markdown_summary = "### 💓 Trainingszonen\n\n"
# Heart Rate Zones
hr_zones = zones.get("heart_rate", {}).get("zones", [])
if hr_zones:
@@ -91,9 +99,9 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
markdown_summary += "| Zone | Bereich (bpm) |\n"
markdown_summary += "|------|---------------|\n"
for i, z in enumerate(hr_zones):
markdown_summary += f"| {i+1} | {z.get('min')} - {z.get('max') if z.get('max') != -1 else 'max'} |\n"
markdown_summary += f"| {i + 1} | {z.get('min')} - {z.get('max') if z.get('max') != -1 else 'max'} |\n"
markdown_summary += "\n"
# Power Zones
power_zones = zones.get("power", {}).get("zones", [])
if power_zones:
@@ -101,26 +109,26 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
markdown_summary += "| Zone | Bereich (W) |\n"
markdown_summary += "|------|-------------|\n"
for i, z in enumerate(power_zones):
markdown_summary += f"| {i+1} | {z.get('min')} - {z.get('max') if z.get('max') != -1 else 'max'} |\n"
markdown_summary += f"| {i + 1} | {z.get('min')} - {z.get('max') if z.get('max') != -1 else 'max'} |\n"
if not hr_zones and not power_zones:
markdown_summary = "⚠️ Keine Trainingszonen konfiguriert."
return [
TextContent(
type="text",
type="text",
text=markdown_summary.strip(),
annotations=Annotations(audience=["user"])
annotations=Annotations(audience=["user"]),
),
EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri="internal://athlete/zones",
mimeType="application/json",
text=json.dumps(zones, indent=2)
text=json.dumps(zones, indent=2),
),
annotations=Annotations(audience=["assistant"])
)
annotations=Annotations(audience=["assistant"]),
),
]
except Exception as e:
error_msg = f"Error fetching athlete zones: {str(e)}"
@@ -163,7 +171,7 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
}
markdown_summary = "### 📈 Trainingsstatistiken\n\n"
def create_table(title: str, data: dict):
tbl = f"#### {title}\n"
tbl += "| Sport | Aktivitäten | Distanz | Zeit | Höhenmeter |\n"
@@ -179,19 +187,19 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
return [
TextContent(
type="text",
type="text",
text=markdown_summary.strip(),
annotations=Annotations(audience=["user"])
annotations=Annotations(audience=["user"]),
),
EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri="internal://athlete/stats",
mimeType="application/json",
text=json.dumps(stats, indent=2)
text=json.dumps(stats, indent=2),
),
annotations=Annotations(audience=["assistant"])
)
annotations=Annotations(audience=["assistant"]),
),
]
except Exception as e:
error_msg = f"Error fetching athlete stats: {str(e)}"
+23 -6
View File
@@ -7,12 +7,17 @@ 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)),
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"]))
return TextContent(
type="text", text=text, annotations=Annotations(audience=["user"])
)
def register(mcp: FastMCP, strava: StravaClient) -> None:
@@ -88,9 +93,16 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
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)]
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)}")]
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):
@@ -118,6 +130,11 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
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)]
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)}")]
return [
TextContent(type="text", text=f"Error fetching club members: {str(e)}")
]
+21 -10
View File
@@ -7,12 +7,17 @@ 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)),
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"]))
return TextContent(
type="text", text=text, annotations=Annotations(audience=["user"])
)
def register(mcp: FastMCP, strava: StravaClient) -> None:
@@ -38,18 +43,24 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
"primary": g.get("primary", False),
"retired": g.get("retired", False),
}
brand_model = " ".join(filter(None, [data["brand_name"], data["model_name"]])) or "N/A"
md = f"""### 🚲 Ausrüstung: {data['name'] or gear_id}
brand_model = (
" ".join(filter(None, [data["brand_name"], data["model_name"]]))
or "N/A"
)
md = f"""### 🚲 Ausrüstung: {data["name"] or gear_id}
| Feld | Wert |
|------|------|
| Marke / Modell | {brand_model} |
| Spitzname | {data['nickname'] or 'N/A'} |
| Typ | {data['type'] or 'N/A'} |
| Gesamt-Distanz | {data['distance']} |
| Primär | {'✅ Ja' if data['primary'] else 'Nein'} |
| Im Ruhestand | {'🛑 Ja' if data['retired'] else 'Nein'} |"""
| Spitzname | {data["nickname"] or "N/A"} |
| Typ | {data["type"] or "N/A"} |
| Gesamt-Distanz | {data["distance"]} |
| Primär | {"✅ Ja" if data["primary"] else "Nein"} |
| Im Ruhestand | {"🛑 Ja" if data["retired"] else "Nein"} |"""
if data["description"]:
md += f"\n\n_{data['description']}_"
return [_user_text(md.strip()), _resource(f"internal://gear/{gear_id}", data)]
return [
_user_text(md.strip()),
_resource(f"internal://gear/{gear_id}", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching gear: {str(e)}")]
+6 -2
View File
@@ -1,6 +1,7 @@
from mcp.server.fastmcp import FastMCP
from strava_mcp_server.strava_client import StravaClient
def register(mcp: FastMCP, strava: StravaClient) -> None:
@mcp.prompt()
def analyze_activity(activity_id: str) -> str:
@@ -30,8 +31,11 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
Fetches recent activities and athlete stats to produce a summary report.
"""
from datetime import datetime, timedelta, timezone
after_dt = (datetime.now(timezone.utc) - timedelta(weeks=weeks)).replace(hour=0, minute=0, second=0, microsecond=0)
after_iso = after_dt.isoformat().replace('+00:00', 'Z')
after_dt = (datetime.now(timezone.utc) - timedelta(weeks=weeks)).replace(
hour=0, minute=0, second=0, microsecond=0
)
after_iso = after_dt.isoformat().replace("+00:00", "Z")
return (
f"Please summarize my Strava training for the last {weeks} weeks.\n\n"
f"Use list_activities with after='{after_iso}' (ISO 8601) "
+28 -12
View File
@@ -7,12 +7,17 @@ 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)),
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"]))
return TextContent(
type="text", text=text, annotations=Annotations(audience=["user"])
)
def register(mcp: FastMCP, strava: StravaClient) -> None:
@@ -30,7 +35,11 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
"id": str(r.get("id")),
"name": r.get("name"),
"description": r.get("description") or "",
"type": "Run" if r.get("type") == 1 else "Ride" if r.get("type") == 2 else "Other",
"type": "Run"
if r.get("type") == 1
else "Ride"
if r.get("type") == 2
else "Other",
"sub_type": r.get("sub_type"),
"distance": f"{r.get('distance', 0) / 1000:.2f} km",
"elevation_gain": f"{r.get('elevation_gain', 0):.0f} m",
@@ -66,7 +75,11 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
"id": str(r.get("id")),
"name": r.get("name"),
"description": r.get("description") or "",
"type": "Run" if r.get("type") == 1 else "Ride" if r.get("type") == 2 else "Other",
"type": "Run"
if r.get("type") == 1
else "Ride"
if r.get("type") == 2
else "Other",
"distance": f"{r.get('distance', 0) / 1000:.2f} km",
"elevation_gain": f"{r.get('elevation_gain', 0):.0f} m",
"estimated_moving_time": f"{r.get('estimated_moving_time', 0) / 60:.0f} min",
@@ -83,19 +96,22 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
],
}
n_seg = len(data["segments"])
md = f"""### 🗺️ Route: {data['name']}
md = f"""### 🗺️ Route: {data["name"]}
| Feld | Wert |
|------|------|
| Typ | {data['type']} |
| Distanz | {data['distance']} |
| Höhenmeter | {data['elevation_gain']} |
| Geschätzte Dauer | {data['estimated_moving_time']} |
| Typ | {data["type"]} |
| Distanz | {data["distance"]} |
| Höhenmeter | {data["elevation_gain"]} |
| Geschätzte Dauer | {data["estimated_moving_time"]} |
| Segmente | {n_seg} |
| Favorit | {'⭐ Ja' if data['starred'] else 'Nein'} |
| Privat | {'🔒 Ja' if data['private'] else 'Nein'} |"""
| Favorit | {"⭐ Ja" if data["starred"] else "Nein"} |
| Privat | {"🔒 Ja" if data["private"] else "Nein"} |"""
if data["description"]:
md += f"\n\n_{data['description']}_"
return [_user_text(md.strip()), _resource(f"internal://routes/{route_id}", data)]
return [
_user_text(md.strip()),
_resource(f"internal://routes/{route_id}", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching route: {str(e)}")]
+35 -12
View File
@@ -8,12 +8,17 @@ from strava_mcp_server.utils import format_date_iso, format_date_human
def _resource(uri: str, data) -> EmbeddedResource:
return EmbeddedResource(
type="resource",
resource=TextResourceContents(uri=uri, mimeType="application/json", text=json.dumps(data, indent=2)),
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"]))
return TextContent(
type="text", text=text, annotations=Annotations(audience=["user"])
)
def register(mcp: FastMCP, strava: StravaClient) -> None:
@@ -43,21 +48,32 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
pr = f"#{data['pr_rank']}" if data["pr_rank"] else "N/A"
kom = f"#{data['kom_rank']}" if data["kom_rank"] else "N/A"
w = f"{data['average_watts']:.0f} W" if data["average_watts"] else "N/A"
hr = f"{data['average_heartrate']:.0f} bpm" if data["average_heartrate"] else "N/A"
md = f"""### 🏅 Segment-Effort: {data['name']}
hr = (
f"{data['average_heartrate']:.0f} bpm"
if data["average_heartrate"]
else "N/A"
)
md = f"""### 🏅 Segment-Effort: {data["name"]}
| Feld | Wert |
|------|------|
| Datum | {format_date_human(data['start_date'])} |
| Distanz | {data['distance']} |
| Zeit (gesamt) | {data['elapsed_time']} |
| Fahrzeit | {data['moving_time']} |
| Datum | {format_date_human(data["start_date"])} |
| Distanz | {data["distance"]} |
| Zeit (gesamt) | {data["elapsed_time"]} |
| Fahrzeit | {data["moving_time"]} |
| Ø Leistung | {w} |
| Ø Herzfrequenz | {hr} |
| PR-Rang | {pr} |
| KOM-Rang | {kom} |"""
return [_user_text(md.strip()), _resource(f"internal://segment_efforts/{effort_id}", data)]
return [
_user_text(md.strip()),
_resource(f"internal://segment_efforts/{effort_id}", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching segment effort: {str(e)}")]
return [
TextContent(
type="text", text=f"Error fetching segment effort: {str(e)}"
)
]
@mcp.tool()
async def list_segment_efforts(
@@ -101,9 +117,16 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
for effort in data:
pr = f"#{effort['pr_rank']}" if effort["pr_rank"] else "-"
md += f"| {format_date_human(effort['start_date'])} | {effort['elapsed_time']} | {effort['moving_time']} | {pr} |\n"
return [_user_text(md.strip()), _resource(f"internal://segments/{segment_id}/efforts", data)]
return [
_user_text(md.strip()),
_resource(f"internal://segments/{segment_id}/efforts", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching segment efforts: {str(e)}")]
return [
TextContent(
type="text", text=f"Error fetching segment efforts: {str(e)}"
)
]
@mcp.tool()
async def get_segment_effort_streams(
+39 -19
View File
@@ -7,12 +7,17 @@ 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)),
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"]))
return TextContent(
type="text", text=text, annotations=Annotations(audience=["user"])
)
def register(mcp: FastMCP, strava: StravaClient) -> None:
@@ -43,22 +48,25 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
"country": s.get("country"),
}
loc = ", ".join(filter(None, [data["city"], data["country"]])) or "N/A"
md = f"""### 📍 Segment: {data['name']}
md = f"""### 📍 Segment: {data["name"]}
| Feld | Wert |
|------|------|
| Sport | {data['activity_type']} |
| Distanz | {data['distance']} |
| Ø Steigung | {data['average_grade']} |
| Max Steigung | {data['maximum_grade']} |
| Höhe (hoch) | {data['elevation_high']} |
| Höhe (tief) | {data['elevation_low']} |
| Höhenmeter | {data['total_elevation_gain']} |
| Versuche | {data['effort_count']} |
| Athleten | {data['athlete_count']} |
| KOM/QOM | {data['kom'] or 'N/A'} |
| Sport | {data["activity_type"]} |
| Distanz | {data["distance"]} |
| Ø Steigung | {data["average_grade"]} |
| Max Steigung | {data["maximum_grade"]} |
| Höhe (hoch) | {data["elevation_high"]} |
| Höhe (tief) | {data["elevation_low"]} |
| Höhenmeter | {data["total_elevation_gain"]} |
| Versuche | {data["effort_count"]} |
| Athleten | {data["athlete_count"]} |
| KOM/QOM | {data["kom"] or "N/A"} |
| Ort | {loc} |
| Favorit | {'⭐ Ja' if data['starred'] else 'Nein'} |"""
return [_user_text(md.strip()), _resource(f"internal://segments/{segment_id}", data)]
| Favorit | {"⭐ Ja" if data["starred"] else "Nein"} |"""
return [
_user_text(md.strip()),
_resource(f"internal://segments/{segment_id}", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching segment: {str(e)}")]
@@ -90,9 +98,16 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
md += "|------|-------|---------|------------|----------|\n"
for s in data:
md += f"| {s['name']} | {s['activity_type']} | {s['distance']} | {s['average_grade']} | {s['effort_count']} |\n"
return [_user_text(md.strip()), _resource("internal://segments/starred", data)]
return [
_user_text(md.strip()),
_resource("internal://segments/starred", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching starred segments: {str(e)}")]
return [
TextContent(
type="text", text=f"Error fetching starred segments: {str(e)}"
)
]
@mcp.tool()
async def explore_segments(
@@ -127,9 +142,14 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
md += "|------|---------|------------|-----------|----------|\n"
for s in data:
md += f"| {s['name']} | {s['distance']} | {s['average_grade']} | {s['elevation_difference']} | {s['climb_category']} |\n"
return [_user_text(md.strip()), _resource("internal://segments/explore", data)]
return [
_user_text(md.strip()),
_resource("internal://segments/explore", data),
]
except Exception as e:
return [TextContent(type="text", text=f"Error exploring segments: {str(e)}")]
return [
TextContent(type="text", text=f"Error exploring segments: {str(e)}")
]
@mcp.tool()
async def get_segment_streams(