From 9a03097f8096e05b9430ee98e1c2d1661f4ae230 Mon Sep 17 00:00:00 2001 From: Mike Sviblov Date: Sat, 11 Jul 2026 15:17:45 +0300 Subject: [PATCH] fix: list_pending_devices MCP tool leaked approved/hidden inventory rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend GET /scan/pending endpoint intentionally returns the whole inventory — approved devices stay listed so the frontend can show the canvas-presence badge. The MCP tool proxied that response verbatim while its description promises devices "not yet approved or hidden", so MCP clients saw every approved device as if it were still awaiting triage (59 "pending" entries on a fully-triaged canvas, 58 of them approved). Filter the tool response to status == "pending", keeping legacy rows that predate the status field. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Udrvtwi3cqcxUNHZtBHkBg --- mcp/app/tools.py | 7 ++++++- mcp/tests/test_tools.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/mcp/app/tools.py b/mcp/app/tools.py index 85ef890..30dac22 100644 --- a/mcp/app/tools.py +++ b/mcp/app/tools.py @@ -228,7 +228,12 @@ async def _dispatch(name: str, args: dict) -> dict: return await backend.get("/api/v1/nodes") if name == "list_pending_devices": - return await backend.get("/api/v1/scan/pending") + # Backend /scan/pending returns the whole inventory: approved rows stay + # listed so the frontend can show a canvas-presence badge. This tool + # promises only devices "not yet approved or hidden", so filter to + # actual pending rows (legacy rows without a status count as pending). + devices = await backend.get("/api/v1/scan/pending") + return [d for d in devices if d.get("status", "pending") == "pending"] if name == "list_designs": return await backend.get("/api/v1/designs") diff --git a/mcp/tests/test_tools.py b/mcp/tests/test_tools.py index db5ad1f..a8dc37f 100644 --- a/mcp/tests/test_tools.py +++ b/mcp/tests/test_tools.py @@ -266,3 +266,19 @@ def test_create_design_schema_requires_name(): async def test_unknown_tool(): with pytest.raises(ValueError, match="Unknown tool"): await _dispatch("nonexistent", {}) + + +@pytest.mark.anyio +async def test_list_pending_devices_filters_non_pending(mock_backend): + """The tool promises devices *not yet approved or hidden*; the backend + endpoint returns the whole inventory including approved rows (they carry + the canvas-presence badge). The tool must filter to status == "pending" + and keep legacy rows that lack the field.""" + mock_backend.get = AsyncMock(return_value=[ + {"id": "p1", "ip": "192.168.1.50", "status": "pending"}, + {"id": "a1", "ip": "192.168.1.60", "status": "approved"}, + {"id": "h1", "ip": "192.168.1.70", "status": "hidden"}, + {"id": "legacy", "ip": "192.168.1.80"}, + ]) + result = await _dispatch("list_pending_devices", {}) + assert [d["id"] for d in result] == ["p1", "legacy"]