feat: expose activity map as a native MCP App resource with _meta ui resourceUri
This commit is contained in:
@@ -113,7 +113,8 @@ async def generate():
|
|||||||
result = await get_activity_map(ctx, 99999)
|
result = await get_activity_map(ctx, 99999)
|
||||||
|
|
||||||
html_text = None
|
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 isinstance(item, TextContent):
|
||||||
if "<!DOCTYPE html>" in item.text:
|
if "<!DOCTYPE html>" in item.text:
|
||||||
html_text = item.text
|
html_text = item.text
|
||||||
@@ -191,7 +192,8 @@ async def generate():
|
|||||||
|
|
||||||
# Find the TextContent that holds the HTML
|
# Find the TextContent that holds the HTML
|
||||||
html_text = None
|
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 isinstance(item, TextContent):
|
||||||
if "<!DOCTYPE html>" in item.text:
|
if "<!DOCTYPE html>" in item.text:
|
||||||
html_text = item.text
|
html_text = item.text
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Strava MCP Server"""
|
"""Strava MCP Server"""
|
||||||
|
|
||||||
__version__ = "0.2.4"
|
__version__ = "0.2.5"
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
from mcp.server.fastmcp import FastMCP, Context
|
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.strava_client import StravaClient
|
||||||
from strava_mcp_server.utils import (
|
from strava_mcp_server.utils import (
|
||||||
parse_iso_to_unix,
|
parse_iso_to_unix,
|
||||||
@@ -396,7 +402,10 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
|
|||||||
if not polyline:
|
if not polyline:
|
||||||
warning_msg = f"⚠️ Keine GPS-Routen-Daten für diese Aktivität verfügbar (ID: {activity_id})."
|
warning_msg = f"⚠️ Keine GPS-Routen-Daten für diese Aktivität verfügbar (ID: {activity_id})."
|
||||||
await ctx.warning(warning_msg)
|
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")
|
name = activity.get("name", "Aktivität")
|
||||||
sport_type = activity.get("sport_type") or activity.get("type", "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:
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>"""
|
||||||
return [
|
resource_uri = f"strava://activity/{activity_id}/map"
|
||||||
TextContent(type="text", text=html_content.strip()),
|
return CallToolResult(
|
||||||
TextContent(
|
content=[
|
||||||
type="text",
|
TextContent(type="text", text=html_content.strip()),
|
||||||
text="[System: Render the above content directly as an HTML block so the UI can iframe it.]",
|
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:
|
except Exception as e:
|
||||||
error_msg = f"Error rendering activity map: {str(e)}"
|
error_msg = f"Error rendering activity map: {str(e)}"
|
||||||
await ctx.error(error_msg)
|
await ctx.error(error_msg)
|
||||||
return [TextContent(type="text", text=error_msg)]
|
return CallToolResult(
|
||||||
|
content=[TextContent(type="text", text=error_msg)], isError=True
|
||||||
|
)
|
||||||
|
|||||||
+19
-8
@@ -15,6 +15,12 @@ class MockMCP:
|
|||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
def resource(self, path):
|
||||||
|
def decorator(func):
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_activity_map_success():
|
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"]
|
get_activity_map = mcp.tools["get_activity_map"]
|
||||||
result = await get_activity_map(ctx, 12345)
|
result = await get_activity_map(ctx, 12345)
|
||||||
|
|
||||||
assert len(result) == 2
|
assert result.isError is not True
|
||||||
assert result[0].type == "text"
|
assert result.meta == {"ui": {"resourceUri": "strava://activity/12345/map"}}
|
||||||
html_text = result[0].text
|
|
||||||
|
|
||||||
assert result[1].type == "text"
|
content = result.content
|
||||||
assert "Render the above content" in result[1].text
|
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 "<!DOCTYPE html>" in html_text
|
assert "<!DOCTYPE html>" in html_text
|
||||||
assert "unpkg.com/leaflet" 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"]
|
get_activity_map = mcp.tools["get_activity_map"]
|
||||||
result = await get_activity_map(ctx, 67890)
|
result = await get_activity_map(ctx, 67890)
|
||||||
|
|
||||||
assert len(result) == 1
|
assert result.isError is True
|
||||||
assert result[0].type == "text"
|
assert len(result.content) == 1
|
||||||
assert "⚠️ Keine GPS-Routen-Daten" in result[0].text
|
assert result.content[0].type == "text"
|
||||||
|
assert "⚠️ Keine GPS-Routen-Daten" in result.content[0].text
|
||||||
|
|||||||
Reference in New Issue
Block a user