feat: add direct HTTP custom route /get-map/{activity_id} using FastMCP decorator and bump version to 0.2.12
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
"""Strava MCP Server"""
|
"""Strava MCP Server"""
|
||||||
|
|
||||||
__version__ = "0.2.11"
|
__version__ = "0.2.12"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user