feat: add direct HTTP custom route /get-map/{activity_id} using FastMCP decorator and bump version to 0.2.12
CI/CD Pipeline / Publish to PyPI (push) Successful in 13s
CI/CD Pipeline / Lint & Check (push) Successful in 10s
CI/CD Pipeline / Build & Push Docker Image (push) Successful in 1m30s

This commit is contained in:
2026-05-30 20:56:07 +02:00
parent 7d15789a0f
commit a30bef822b
3 changed files with 70 additions and 1 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""Strava MCP Server""" """Strava MCP Server"""
__version__ = "0.2.11" __version__ = "0.2.12"
+17
View File
@@ -931,3 +931,20 @@ def register(mcp: FastMCP, strava: StravaClient) -> None:
return await _render_activity_map_html(last_id) return await _render_activity_map_html(last_id)
except Exception as e: except Exception as e:
return f"<html><body><p>Error loading last activity: {str(e)}</p></body></html>" return f"<html><body><p>Error loading last activity: {str(e)}</p></body></html>"
from starlette.requests import Request
from starlette.responses import HTMLResponse
@mcp.custom_route("/get-map/{activity_id}", methods=["GET"])
async def get_web_map(request: Request) -> HTMLResponse:
"""HTTP Endpoint to directly serve the interactive Leaflet HTML map."""
activity_id_str = request.path_params.get("activity_id")
try:
activity_id = int(activity_id_str)
html_content = await _render_activity_map_html(activity_id)
return HTMLResponse(content=html_content)
except Exception as e:
return HTMLResponse(
content=f"<html><body><h3>Error loading map:</h3><p>{str(e)}</p></body></html>",
status_code=400,
)
+52
View File
@@ -25,6 +25,16 @@ class MockMCP:
return decorator return decorator
def custom_route(self, path, methods, **kwargs):
if not hasattr(self, "routes"):
self.routes = {}
def decorator(func):
self.routes[path] = 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():
@@ -109,3 +119,45 @@ async def test_get_activity_map_no_polyline():
assert len(result.content) == 1 assert len(result.content) == 1
assert result.content[0].type == "text" assert result.content[0].type == "text"
assert "⚠️ Keine GPS-Routen-Daten" in result.content[0].text assert "⚠️ Keine GPS-Routen-Daten" in result.content[0].text
@pytest.mark.asyncio
async def test_get_web_map_route_success():
mcp = MockMCP()
strava = MagicMock()
# Mock detailed activity with a valid polyline
strava.get_activity = AsyncMock(
return_value={
"name": "Evening Ride",
"map": {"polyline": "a~l~FqzbwOq}@it@..."},
"sport_type": "Ride",
}
)
strava.get_activity_streams = AsyncMock(
return_value={
"altitude": {"data": [100.0, 101.0, 102.0]},
"distance": {"data": [0.0, 100.0, 200.0]},
"latlng": {"data": [[47.1, 9.1], [47.2, 9.2], [47.3, 9.3]]},
}
)
register(mcp, strava)
assert "/get-map/{activity_id}" in mcp.routes
route_handler = mcp.routes["/get-map/{activity_id}"]
# Mock Starlette Request
request = MagicMock()
request.path_params = {"activity_id": "12345"}
response = await route_handler(request)
from starlette.responses import HTMLResponse
assert isinstance(response, HTMLResponse)
assert response.status_code == 200
html_body = response.body.decode("utf-8")
assert "<!DOCTYPE html>" in html_body
assert "Evening Ride" in html_body