fix: attach MCP-created nodes/edges to a design (#225)

create_node/create_edge persisted rows with design_id=null when the
client omitted it (the MCP write tools), so they existed in the DB but
never rendered on the canvas until a container restart reconciled them.
Both routes now fall back to the first design, matching bulk-approve.

Also fix MCP resource reads (homelable://canvas, homelable://edges):
the framework passes a pydantic AnyUrl, not a str, which raised
"'AnyUrl' object has no attribute 'startswith'". Coerce to str.

EdgeResponse now exposes design_id for symmetry with NodeResponse.

ha-relevant: no
This commit is contained in:
Pouzor
2026-06-28 14:00:59 +02:00
parent 7e99d77edc
commit cbc2bc03c2
7 changed files with 121 additions and 4 deletions
+22
View File
@@ -1,4 +1,5 @@
import pytest
from pydantic import AnyUrl
from unittest.mock import AsyncMock, patch
from app.resources import read_resource
@@ -45,3 +46,24 @@ async def test_read_scan_pending(mock_backend):
async def test_read_unknown_uri(mock_backend):
with pytest.raises(ValueError, match="Unknown resource URI"):
await read_resource("homelable://unknown")
@pytest.mark.anyio
async def test_read_resource_accepts_anyurl(mock_backend):
# Regression for #225: the MCP framework calls the handler with a pydantic
# AnyUrl, not a str, which raised "'AnyUrl' object has no attribute
# 'startswith'". The handler must coerce to str and still route correctly.
await read_resource(AnyUrl("homelable://canvas"))
mock_backend.get.assert_called_once_with("/api/v1/canvas")
@pytest.mark.anyio
async def test_read_edges_anyurl(mock_backend):
await read_resource(AnyUrl("homelable://edges"))
mock_backend.get.assert_called_once_with("/api/v1/edges")
@pytest.mark.anyio
async def test_read_single_node_anyurl(mock_backend):
await read_resource(AnyUrl("homelable://nodes/abc123"))
mock_backend.get.assert_called_once_with("/api/v1/nodes/abc123")