From 3164e91be9b4cab22543b0757a0885663de42585 Mon Sep 17 00:00:00 2001 From: Matthias Hinrichs Date: Sat, 30 May 2026 18:48:33 +0200 Subject: [PATCH] feat: expose activity map as a native MCP App resource with _meta ui resourceUri --- scripts/generate_test_map.py | 6 ++-- src/strava_mcp_server/__init__.py | 2 +- src/strava_mcp_server/tools/activities.py | 35 ++++++++++++++++------- tests/unit/test_map.py | 27 +++++++++++------ 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/scripts/generate_test_map.py b/scripts/generate_test_map.py index 8198daf..e4d88de 100644 --- a/scripts/generate_test_map.py +++ b/scripts/generate_test_map.py @@ -113,7 +113,8 @@ async def generate(): result = await get_activity_map(ctx, 99999) html_text = None - for item in result: + content_list = result.content if hasattr(result, "content") else result + for item in content_list: if isinstance(item, TextContent): if "" in item.text: html_text = item.text @@ -191,7 +192,8 @@ async def generate(): # Find the TextContent that holds the HTML html_text = None - for item in result: + content_list = result.content if hasattr(result, "content") else result + for item in content_list: if isinstance(item, TextContent): if "" in item.text: html_text = item.text diff --git a/src/strava_mcp_server/__init__.py b/src/strava_mcp_server/__init__.py index 0cefac2..3506b38 100644 --- a/src/strava_mcp_server/__init__.py +++ b/src/strava_mcp_server/__init__.py @@ -1,3 +1,3 @@ """Strava MCP Server""" -__version__ = "0.2.4" +__version__ = "0.2.5" diff --git a/src/strava_mcp_server/tools/activities.py b/src/strava_mcp_server/tools/activities.py index 3f9f874..4c27325 100644 --- a/src/strava_mcp_server/tools/activities.py +++ b/src/strava_mcp_server/tools/activities.py @@ -1,6 +1,12 @@ import json from mcp.server.fastmcp import FastMCP, Context -from mcp.types import TextContent, Annotations, EmbeddedResource, TextResourceContents +from mcp.types import ( + TextContent, + Annotations, + EmbeddedResource, + TextResourceContents, + CallToolResult, +) from strava_mcp_server.strava_client import StravaClient from strava_mcp_server.utils import ( parse_iso_to_unix, @@ -396,7 +402,10 @@ def register(mcp: FastMCP, strava: StravaClient) -> None: if not polyline: warning_msg = f"⚠️ Keine GPS-Routen-Daten für diese Aktivität verfügbar (ID: {activity_id})." await ctx.warning(warning_msg) - return [TextContent(type="text", text=warning_msg)] + return CallToolResult( + content=[TextContent(type="text", text=warning_msg)], + isError=True, + ) name = activity.get("name", "Aktivität") sport_type = activity.get("sport_type") or activity.get("type", "Aktivität") @@ -844,14 +853,20 @@ def register(mcp: FastMCP, strava: StravaClient) -> None: """ - return [ - TextContent(type="text", text=html_content.strip()), - TextContent( - type="text", - text="[System: Render the above content directly as an HTML block so the UI can iframe it.]", - ), - ] + resource_uri = f"strava://activity/{activity_id}/map" + return CallToolResult( + content=[ + TextContent(type="text", text=html_content.strip()), + TextContent( + type="text", + text="[System: Render the above content directly as an HTML block so the UI can iframe it.]", + ), + ], + _meta={"ui": {"resourceUri": resource_uri}}, + ) except Exception as e: error_msg = f"Error rendering activity map: {str(e)}" await ctx.error(error_msg) - return [TextContent(type="text", text=error_msg)] + return CallToolResult( + content=[TextContent(type="text", text=error_msg)], isError=True + ) diff --git a/tests/unit/test_map.py b/tests/unit/test_map.py index f8f7ba2..04eb9a1 100644 --- a/tests/unit/test_map.py +++ b/tests/unit/test_map.py @@ -15,6 +15,12 @@ class MockMCP: return decorator + def resource(self, path): + def decorator(func): + return func + + return decorator + @pytest.mark.asyncio async def test_get_activity_map_success(): @@ -46,12 +52,16 @@ async def test_get_activity_map_success(): get_activity_map = mcp.tools["get_activity_map"] result = await get_activity_map(ctx, 12345) - assert len(result) == 2 - assert result[0].type == "text" - html_text = result[0].text + assert result.isError is not True + assert result.meta == {"ui": {"resourceUri": "strava://activity/12345/map"}} - assert result[1].type == "text" - assert "Render the above content" in result[1].text + content = result.content + assert len(content) == 2 + assert content[0].type == "text" + html_text = content[0].text + + assert content[1].type == "text" + assert "Render the above content" in content[1].text assert "" in html_text assert "unpkg.com/leaflet" in html_text @@ -86,6 +96,7 @@ async def test_get_activity_map_no_polyline(): get_activity_map = mcp.tools["get_activity_map"] result = await get_activity_map(ctx, 67890) - assert len(result) == 1 - assert result[0].type == "text" - assert "⚠️ Keine GPS-Routen-Daten" in result[0].text + assert result.isError is True + assert len(result.content) == 1 + assert result.content[0].type == "text" + assert "⚠️ Keine GPS-Routen-Daten" in result.content[0].text