refactor: enhance tool outputs by returning formatted markdown and JSON resources for structured display
This commit is contained in:
@@ -4,6 +4,22 @@ from mcp.types import ContentBlock, TextContent, Annotations, EmbeddedResource,
|
||||
from strava_mcp_server.strava_client import StravaClient
|
||||
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."""
|
||||
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:
|
||||
"""Helper: return a user-facing TextContent."""
|
||||
return TextContent(type="text", text=text, annotations=Annotations(audience=["user"]))
|
||||
|
||||
def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
@mcp.tool()
|
||||
async def list_activities(
|
||||
@@ -29,7 +45,7 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
before=parse_iso_to_unix(before),
|
||||
after=parse_iso_to_unix(after),
|
||||
)
|
||||
|
||||
|
||||
essential_data = []
|
||||
for a in activities:
|
||||
start_date_raw = a.get("start_date")
|
||||
@@ -57,20 +73,8 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
markdown_summary += f"| {a['start_date_local']} | {a['sport_type']} | {a['name']} | {a['distance']} | {a['moving_time']} | {a['total_elevation_gain']} | {hr} |\n"
|
||||
|
||||
return [
|
||||
TextContent(
|
||||
type="text",
|
||||
text=markdown_summary.strip(),
|
||||
annotations=Annotations(audience=["user"])
|
||||
),
|
||||
EmbeddedResource(
|
||||
type="resource",
|
||||
resource=TextResourceContents(
|
||||
uri="internal://activities/list",
|
||||
mimeType="application/json",
|
||||
text=json.dumps(essential_data, indent=2)
|
||||
),
|
||||
annotations=Annotations(audience=["assistant"])
|
||||
)
|
||||
_user_text(markdown_summary.strip()),
|
||||
_resource("internal://activities/list", essential_data),
|
||||
]
|
||||
except Exception as e:
|
||||
error_msg = f"Error listing activities: {str(e)}"
|
||||
@@ -95,9 +99,41 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
segment["id"] = str(segment["id"])
|
||||
if "id" in activity:
|
||||
activity["id"] = str(activity["id"])
|
||||
return activity
|
||||
|
||||
name = activity.get("name", "N/A")
|
||||
sport = activity.get("sport_type") or activity.get("type", "N/A")
|
||||
date = format_date_human(activity.get("start_date"))
|
||||
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"
|
||||
gear = activity.get("gear_id") or "N/A"
|
||||
n_efforts = len(activity.get("segment_efforts", []))
|
||||
|
||||
markdown_summary = f"""### 🏃 Aktivität: {name}
|
||||
| Feld | Wert |
|
||||
|------|------|
|
||||
| Sport | {sport} |
|
||||
| Datum | {date} |
|
||||
| Distanz | {dist} |
|
||||
| Zeit | {time} |
|
||||
| Höhenmeter | {elev} |
|
||||
| Ø Herzfrequenz | {avg_hr} |
|
||||
| Max Herzfrequenz | {max_hr} |
|
||||
| Ø Geschwindigkeit | {avg_spd} |
|
||||
| Ø Leistung | {avg_w} |
|
||||
| Ausrüstung | {gear} |
|
||||
| Segment-Efforts | {n_efforts} |"""
|
||||
|
||||
return [
|
||||
_user_text(markdown_summary.strip()),
|
||||
_resource(f"internal://activities/{activity_id}", activity),
|
||||
]
|
||||
except Exception as e:
|
||||
return 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):
|
||||
@@ -108,7 +144,7 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
"""
|
||||
try:
|
||||
comments = await strava.get_activity_comments(activity_id, per_page=limit)
|
||||
return [
|
||||
data = [
|
||||
{
|
||||
"id": c.get("id"),
|
||||
"text": c.get("text"),
|
||||
@@ -117,8 +153,15 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
}
|
||||
for c in comments
|
||||
]
|
||||
if not data:
|
||||
md = "### 💬 Keine Kommentare vorhanden."
|
||||
else:
|
||||
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)]
|
||||
except Exception as e:
|
||||
return f"Error fetching comments: {str(e)}"
|
||||
return [TextContent(type="text", text=f"Error fetching comments: {str(e)}")]
|
||||
|
||||
@mcp.tool()
|
||||
async def get_activity_kudoers(activity_id: int, limit: int = 30):
|
||||
@@ -129,7 +172,7 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
"""
|
||||
try:
|
||||
kudoers = await strava.get_activity_kudoers(activity_id, per_page=limit)
|
||||
return [
|
||||
data = [
|
||||
{
|
||||
"id": k.get("id"),
|
||||
"name": f"{k.get('firstname')} {k.get('lastname')}",
|
||||
@@ -138,8 +181,17 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
}
|
||||
for k in kudoers
|
||||
]
|
||||
if not data:
|
||||
md = "### 👍 Noch keine Kudos."
|
||||
else:
|
||||
md = f"### 👍 Kudos ({len(data)})\n"
|
||||
md += "| Name | Ort |\n|------|-----|\n"
|
||||
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)]
|
||||
except Exception as e:
|
||||
return f"Error fetching kudoers: {str(e)}"
|
||||
return [TextContent(type="text", text=f"Error fetching kudoers: {str(e)}")]
|
||||
|
||||
@mcp.tool()
|
||||
async def get_laps_by_activity_id(activity_id: int):
|
||||
@@ -150,7 +202,7 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
"""
|
||||
try:
|
||||
laps = await strava.get_activity_laps(activity_id)
|
||||
return [
|
||||
data = [
|
||||
{
|
||||
"lap_index": lap.get("lap_index"),
|
||||
"name": lap.get("name"),
|
||||
@@ -165,8 +217,18 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
}
|
||||
for lap in laps
|
||||
]
|
||||
if not data:
|
||||
md = "### 🔄 Keine Runden gefunden."
|
||||
else:
|
||||
md = f"### 🔄 Runden ({len(data)})\n"
|
||||
md += "| # | Distanz | Zeit | Ø Speed | Ø HR |\n"
|
||||
md += "|---|---------|------|---------|------|\n"
|
||||
for lap in data:
|
||||
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)]
|
||||
except Exception as e:
|
||||
return f"Error fetching laps: {str(e)}"
|
||||
return [TextContent(type="text", text=f"Error fetching laps: {str(e)}")]
|
||||
|
||||
@mcp.tool()
|
||||
async def get_zones_by_activity_id(activity_id: int):
|
||||
@@ -177,7 +239,8 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
"""
|
||||
try:
|
||||
zones = await strava.get_activity_zones(activity_id)
|
||||
result = []
|
||||
data = []
|
||||
md = "### 💓 Zonen-Verteilung\n\n"
|
||||
for zone in zones:
|
||||
buckets = [
|
||||
{
|
||||
@@ -188,17 +251,24 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
||||
}
|
||||
for i, b in enumerate(zone.get("distribution_buckets", []))
|
||||
]
|
||||
result.append({
|
||||
zone_data = {
|
||||
"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
|
||||
}
|
||||
data.append(zone_data)
|
||||
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)]
|
||||
except Exception as e:
|
||||
return 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(
|
||||
|
||||
Reference in New Issue
Block a user