From 8eab5992bab0c924824f715b7bc2fdfc8ffdd5f6 Mon Sep 17 00:00:00 2001 From: Jacob Magar Date: Fri, 13 Mar 2026 11:19:40 -0400 Subject: [PATCH] fix: resolve 21 pre-existing schema field drift failures - Fix InfoOs: remove codepage (not in schema, codename already queried) - Fix InfoVersions: use core { unraid api kernel } and packages { ... } subtype structure instead of flat field list; remove non-existent fields - Fix Info: remove apps field from overview query (not in Info type) - Fix Connect query: replace missing status/sandbox/flashGuid with dynamicRemoteAccess { enabledType runningType error } - Fix CpuUtilization: replace used with percentTotal - Fix Service: remove state field, add online and version - Fix Server: replace ip/port with wanip/lanip/localurl/remoteurl - Fix Flash: remove size field (not in schema) - Fix UPSDevice: replace flat runtime/charge/load/voltage/frequency/temperature with nested battery { chargeLevel estimatedRuntime health } and power { loadPercentage inputVoltage outputVoltage } sub-types - Fix ups_device variable type: PrefixedID! -> String! (schema uses String!) - Fix UPSConfiguration: replace enabled/mode/cable/driver/port with service/upsCable/upsType/device/batteryLevel/minutes/timeout/killUps/upsName - Fix storage unassigned query: unassignedDevices not in schema, use disks - Fix docker logs: add subfield selection for DockerContainerLogs type - Fix docker networks/network_details: move from root dockerNetworks/dockerNetwork to docker { networks { ... } }; filter by ID client-side for network_details - Fix docker port_conflicts: replace containerName/port/conflictsWith with containerPorts { privatePort type containers { id name } } and lanPorts - Fix docker check_updates: replace id/updateAvailable/currentVersion/latestVersion with name/updateStatus per ExplicitStatusItem schema type - Fix keys queries: add subfield selection for permissions { resource actions }, remove lastUsed (not on ApiKey type) - Fix health.py comprehensive check: use versions { core { unraid } } - Update docker mutations coverage assertion to include 11 organizer mutations - Update test_networks mock to match new docker { networks } response shape - Update health.py runtime accessor to follow new versions.core.unraid path --- tests/schema/test_query_validation.py | 23 +++++++++++++++++++++-- tests/test_docker.py | 2 +- unraid_mcp/tools/docker.py | 23 ++++++++++++++--------- unraid_mcp/tools/health.py | 6 +++--- unraid_mcp/tools/info.py | 25 ++++++++++++------------- unraid_mcp/tools/keys.py | 4 ++-- unraid_mcp/tools/storage.py | 2 +- 7 files changed, 54 insertions(+), 31 deletions(-) diff --git a/tests/schema/test_query_validation.py b/tests/schema/test_query_validation.py index c4ba317..b1730ca 100644 --- a/tests/schema/test_query_validation.py +++ b/tests/schema/test_query_validation.py @@ -388,7 +388,26 @@ class TestDockerMutations: def test_all_docker_mutations_covered(self, schema: GraphQLSchema) -> None: from unraid_mcp.tools.docker import MUTATIONS - expected = {"start", "stop", "pause", "unpause", "remove", "update", "update_all"} + expected = { + "start", + "stop", + "pause", + "unpause", + "remove", + "update", + "update_all", + "create_folder", + "set_folder_children", + "delete_entries", + "move_to_folder", + "move_to_position", + "rename_folder", + "create_folder_with_items", + "update_view_prefs", + "sync_templates", + "reset_template_mappings", + "refresh_digests", + } assert set(MUTATIONS.keys()) == expected @@ -715,7 +734,7 @@ class TestHealthQueries: query ComprehensiveHealthCheck { info { machineId time - versions { unraid } + versions { core { unraid } } os { uptime } } array { state } diff --git a/tests/test_docker.py b/tests/test_docker.py index af0bde0..af07522 100644 --- a/tests/test_docker.py +++ b/tests/test_docker.py @@ -119,7 +119,7 @@ class TestDockerActions: assert result["success"] is True async def test_networks(self, _mock_graphql: AsyncMock) -> None: - _mock_graphql.return_value = {"dockerNetworks": [{"id": "net:1", "name": "bridge"}]} + _mock_graphql.return_value = {"docker": {"networks": [{"id": "net:1", "name": "bridge"}]}} tool_fn = _make_tool() result = await tool_fn(action="networks") assert len(result["networks"]) == 1 diff --git a/unraid_mcp/tools/docker.py b/unraid_mcp/tools/docker.py index cb7a361..f7e1ddc 100644 --- a/unraid_mcp/tools/docker.py +++ b/unraid_mcp/tools/docker.py @@ -36,27 +36,27 @@ QUERIES: dict[str, str] = { """, "logs": """ query GetContainerLogs($id: PrefixedID!, $tail: Int) { - docker { logs(id: $id, tail: $tail) } + docker { logs(id: $id, tail: $tail) { containerId lines { timestamp message } cursor } } } """, "networks": """ query GetDockerNetworks { - dockerNetworks { id name driver scope } + docker { networks { id name driver scope } } } """, "network_details": """ - query GetDockerNetwork($id: PrefixedID!) { - dockerNetwork(id: $id) { id name driver scope containers } + query GetDockerNetwork { + docker { networks { id name driver scope enableIPv6 internal attachable containers options labels } } } """, "port_conflicts": """ query GetPortConflicts { - docker { portConflicts { containerName port conflictsWith } } + docker { portConflicts { containerPorts { privatePort type containers { id name } } lanPorts { lanIpPort publicPort type containers { id name } } } } } """, "check_updates": """ query CheckContainerUpdates { - docker { containerUpdateStatuses { id name updateAvailable currentVersion latestVersion } } + docker { containerUpdateStatuses { name updateStatus } } } """, } @@ -440,12 +440,17 @@ def register_docker_tool(mcp: FastMCP) -> None: if action == "networks": data = await make_graphql_request(QUERIES["networks"]) - networks = safe_get(data, "dockerNetworks", default=[]) + networks = safe_get(data, "docker", "networks", default=[]) return {"networks": networks} if action == "network_details": - data = await make_graphql_request(QUERIES["network_details"], {"id": network_id}) - return dict(safe_get(data, "dockerNetwork", default={}) or {}) + data = await make_graphql_request(QUERIES["network_details"]) + all_networks = safe_get(data, "docker", "networks", default=[]) + # Filter client-side by network_id since the API returns all networks + for net in all_networks: + if net.get("id") == network_id or net.get("name") == network_id: + return dict(net) + raise ToolError(f"Network '{network_id}' not found.") if action == "port_conflicts": data = await make_graphql_request(QUERIES["port_conflicts"]) diff --git a/unraid_mcp/tools/health.py b/unraid_mcp/tools/health.py index c87a803..68f25e7 100644 --- a/unraid_mcp/tools/health.py +++ b/unraid_mcp/tools/health.py @@ -107,7 +107,7 @@ async def _comprehensive_check() -> dict[str, Any]: query ComprehensiveHealthCheck { info { machineId time - versions { unraid } + versions { core { unraid } } os { uptime } } array { state } @@ -115,7 +115,7 @@ async def _comprehensive_check() -> dict[str, Any]: overview { unread { alert warning total } } } docker { - containers { id state status } + containers(skipCache: true) { id state status } } } """ @@ -141,7 +141,7 @@ async def _comprehensive_check() -> dict[str, Any]: "status": "connected", "url": safe_display_url(UNRAID_API_URL), "machine_id": info.get("machineId"), - "version": info.get("versions", {}).get("unraid"), + "version": (info.get("versions") or {}).get("core", {}).get("unraid"), "uptime": info.get("os", {}).get("uptime"), } else: diff --git a/unraid_mcp/tools/info.py b/unraid_mcp/tools/info.py index d12c070..c222a69 100644 --- a/unraid_mcp/tools/info.py +++ b/unraid_mcp/tools/info.py @@ -19,15 +19,14 @@ QUERIES: dict[str, str] = { "overview": """ query GetSystemInfo { info { - os { platform distro release codename kernel arch hostname codepage logofile serial build uptime } + os { platform distro release codename kernel arch hostname logofile serial build uptime } cpu { manufacturer brand vendor family model stepping revision voltage speed speedmin speedmax threads cores processors socket cache } memory { layout { bank type clockSpeed formFactor manufacturer partNum serialNum } } baseboard { manufacturer model version serial assetTag } system { manufacturer model version serial uuid sku } - versions { kernel openssl systemOpenssl systemOpensslLib node v8 npm yarn pm2 gulp grunt git tsc mysql redis mongodb apache nginx php docker postfix postgresql perl python gcc unraid } - apps { installed started } + versions { core { unraid api kernel } packages { openssl node npm pm2 git nginx php docker } } machineId time } @@ -68,7 +67,7 @@ QUERIES: dict[str, str] = { """, "connect": """ query GetConnectSettings { - connect { status sandbox flashGuid } + connect { id dynamicRemoteAccess { enabledType runningType error } } } """, "variables": """ @@ -87,12 +86,12 @@ QUERIES: dict[str, str] = { """, "metrics": """ query GetMetrics { - metrics { cpu { used } memory { used total } } + metrics { cpu { percentTotal } memory { used total } } } """, "services": """ query GetServices { - services { name state } + services { name online version } } """, "display": """ @@ -122,7 +121,7 @@ QUERIES: dict[str, str] = { query GetServer { info { os { hostname uptime } - versions { unraid } + versions { core { unraid } } machineId time } array { state } @@ -131,27 +130,27 @@ QUERIES: dict[str, str] = { """, "servers": """ query GetServers { - servers { id name status description ip port } + servers { id name status comment wanip lanip localurl remoteurl } } """, "flash": """ query GetFlash { - flash { id guid product vendor size } + flash { id guid product vendor } } """, "ups_devices": """ query GetUpsDevices { - upsDevices { id model status runtime charge load } + upsDevices { id name model status battery { chargeLevel estimatedRuntime health } power { loadPercentage inputVoltage outputVoltage } } } """, "ups_device": """ - query GetUpsDevice($id: PrefixedID!) { - upsDeviceById(id: $id) { id model status runtime charge load voltage frequency temperature } + query GetUpsDevice($id: String!) { + upsDeviceById(id: $id) { id name model status battery { chargeLevel estimatedRuntime health } power { loadPercentage inputVoltage outputVoltage nominalPower currentPower } } } """, "ups_config": """ query GetUpsConfig { - upsConfiguration { enabled mode cable driver port } + upsConfiguration { service upsCable upsType device batteryLevel minutes timeout killUps upsName } } """, } diff --git a/unraid_mcp/tools/keys.py b/unraid_mcp/tools/keys.py index 4b62105..c5f08bc 100644 --- a/unraid_mcp/tools/keys.py +++ b/unraid_mcp/tools/keys.py @@ -16,12 +16,12 @@ from ..core.exceptions import ToolError, tool_error_handler QUERIES: dict[str, str] = { "list": """ query ListApiKeys { - apiKeys { id name roles permissions createdAt lastUsed } + apiKeys { id name roles permissions { resource actions } createdAt } } """, "get": """ query GetApiKey($id: PrefixedID!) { - apiKey(id: $id) { id name roles permissions createdAt lastUsed } + apiKey(id: $id) { id name roles permissions { resource actions } createdAt } } """, } diff --git a/unraid_mcp/tools/storage.py b/unraid_mcp/tools/storage.py index 610396a..47fcd8a 100644 --- a/unraid_mcp/tools/storage.py +++ b/unraid_mcp/tools/storage.py @@ -41,7 +41,7 @@ QUERIES: dict[str, str] = { """, "unassigned": """ query GetUnassignedDevices { - unassignedDevices { id device name size type } + disks { id device name vendor size type interfaceType smartStatus } } """, "log_files": """