From 300567c88da261d81206a89a25c9e6a443520ec0 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Fri, 13 Mar 2026 21:29:27 +0100 Subject: [PATCH] feat(mcp): expose parent_id in update_node tool - Add parent_id to NodeUpdate schema in backend so PATCH /nodes/{id} accepts it (was silently ignored before) - Expose parent_id in update_node MCP tool schema so the MCP SDK forwards it to the backend instead of stripping it - Add regression tests in both backend and MCP layers --- backend/app/schemas/nodes.py | 1 + backend/tests/test_nodes.py | 10 ++++++++++ mcp/app/tools.py | 11 ++++++----- mcp/tests/test_tools.py | 6 ++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/backend/app/schemas/nodes.py b/backend/app/schemas/nodes.py index bd3cad3..9f71db3 100644 --- a/backend/app/schemas/nodes.py +++ b/backend/app/schemas/nodes.py @@ -42,6 +42,7 @@ class NodeUpdate(BaseModel): notes: str | None = None pos_x: float | None = None pos_y: float | None = None + parent_id: str | None = None container_mode: bool | None = None custom_colors: dict[str, Any] | None = None custom_icon: str | None = None diff --git a/backend/tests/test_nodes.py b/backend/tests/test_nodes.py index eafff2c..db54305 100644 --- a/backend/tests/test_nodes.py +++ b/backend/tests/test_nodes.py @@ -102,6 +102,16 @@ async def test_update_node_container_mode(client: AsyncClient, headers: dict): assert res.json()["container_mode"] is True +async def test_update_node_parent_id(client: AsyncClient, headers: dict): + parent = await client.post("/api/v1/nodes", json={"type": "proxmox", "label": "PVE", "status": "unknown"}, headers=headers) + parent_id = parent.json()["id"] + child = await client.post("/api/v1/nodes", json={"type": "lxc", "label": "Child", "status": "unknown"}, headers=headers) + child_id = child.json()["id"] + res = await client.patch(f"/api/v1/nodes/{child_id}", json={"parent_id": parent_id}, headers=headers) + assert res.status_code == 200 + assert res.json()["parent_id"] == parent_id + + async def test_create_node_requires_auth(client: AsyncClient): res = await client.post("/api/v1/nodes", json={"type": "server", "label": "N", "status": "unknown"}) assert res.status_code == 401 diff --git a/mcp/app/tools.py b/mcp/app/tools.py index 56fb933..b9b934c 100644 --- a/mcp/app/tools.py +++ b/mcp/app/tools.py @@ -24,11 +24,12 @@ def register_tools(server: Server): "type": "object", "required": ["id"], "properties": { - "id": {"type": "string"}, - "label": {"type": "string"}, - "ip": {"type": "string"}, - "hostname": {"type": "string"}, - "status": {"type": "string"}, + "id": {"type": "string"}, + "label": {"type": "string"}, + "ip": {"type": "string"}, + "hostname": {"type": "string"}, + "status": {"type": "string"}, + "parent_id": {"type": "string", "description": "ID of the parent node (e.g. Proxmox host for a VM/LXC). Pass null to detach."}, }, }), Tool(name="delete_node", description="Delete a node from the canvas", inputSchema={ diff --git a/mcp/tests/test_tools.py b/mcp/tests/test_tools.py index 80a6cee..c0b4bcf 100644 --- a/mcp/tests/test_tools.py +++ b/mcp/tests/test_tools.py @@ -26,6 +26,12 @@ async def test_update_node(mock_backend): mock_backend.patch.assert_called_once_with("/api/v1/nodes/42", {"label": "New name"}) +@pytest.mark.anyio +async def test_update_node_parent_id(mock_backend): + await _dispatch("update_node", {"id": "42", "parent_id": "proxmox-1"}) + mock_backend.patch.assert_called_once_with("/api/v1/nodes/42", {"parent_id": "proxmox-1"}) + + @pytest.mark.anyio async def test_delete_node(mock_backend): await _dispatch("delete_node", {"id": "42"})