fix(mcp): fix SSE streaming crash and reduce get_canvas token usage

- Replace BaseHTTPMiddleware with pure ASGI middleware in auth.py to fix
  the "Unexpected message: http.response.start" crash on SSE streams
- Migrate from SseServerTransport to StreamableHTTPSessionManager in main.py
- Add _slim_canvas() in tools.py to strip React Flow layout fields from
  get_canvas responses (60-80% payload reduction)
- Update tests: assert canvas slimming, mock session_manager.handle_request
  in auth tests to avoid uninitialized task group errors
This commit is contained in:
Pouzor
2026-03-13 17:28:38 +01:00
parent e1d16b86e3
commit e41dbe579c
5 changed files with 137 additions and 27 deletions
+43
View File
@@ -71,6 +71,18 @@ def register_tools(server: Server):
"required": ["id"],
"properties": {"id": {"type": "string"}},
}),
Tool(name="get_canvas", description="Get the full canvas: all nodes and edges in the homelab topology", inputSchema={
"type": "object",
"properties": {},
}),
Tool(name="list_nodes", description="List all nodes (devices) in the homelab", inputSchema={
"type": "object",
"properties": {},
}),
Tool(name="list_pending_devices", description="List devices discovered by scan but not yet approved or hidden", inputSchema={
"type": "object",
"properties": {},
}),
]
@server.call_tool()
@@ -79,6 +91,27 @@ def register_tools(server: Server):
return [TextContent(type="text", text=json.dumps(result, indent=2))]
def _slim_canvas(raw: dict) -> dict:
"""Strip React Flow layout/style fields — keep only semantic data for AI use."""
NODE_KEEP = {"id", "type", "label", "ip", "hostname", "status", "services", "description", "parentId"}
EDGE_KEEP = {"id", "source", "target", "type", "label"}
def slim_node(n: dict) -> dict:
data = n.get("data", {})
out = {k: v for k, v in data.items() if k in NODE_KEEP and v not in (None, "", [])}
out["id"] = n.get("id")
out["node_type"] = n.get("type")
return out
def slim_edge(e: dict) -> dict:
return {k: v for k, v in e.items() if k in EDGE_KEEP and v not in (None, "")}
return {
"nodes": [slim_node(n) for n in raw.get("nodes", [])],
"edges": [slim_edge(e) for e in raw.get("edges", [])],
}
async def _dispatch(name: str, args: dict) -> dict:
if name == "create_node":
return await backend.post("/api/v1/nodes", args)
@@ -107,4 +140,14 @@ async def _dispatch(name: str, args: dict) -> dict:
if name == "hide_device":
return await backend.post(f"/api/v1/scan/pending/{args['id']}/hide", {})
if name == "get_canvas":
raw = await backend.get("/api/v1/canvas")
return _slim_canvas(raw)
if name == "list_nodes":
return await backend.get("/api/v1/nodes")
if name == "list_pending_devices":
return await backend.get("/api/v1/scan/pending")
raise ValueError(f"Unknown tool: {name}")