feat(resources): add unraid://live/{action} MCP resources for 9 snapshot subscriptions

Registers cpu, memory, cpu_telemetry, array_state, parity_progress,
ups_status, notifications_overview, owner, and server_status as MCP
resources under unraid://live/{action}. Each opens a transient WebSocket
via subscribe_once() and returns JSON; exceptions degrade gracefully to
an error JSON dict rather than raising. Skips log_tail and
notification_feed (require params, not suitable as resources).
This commit is contained in:
Jacob Magar
2026-03-15 21:51:20 -04:00
parent 7c99fe1527
commit f5978d67ec
2 changed files with 117 additions and 0 deletions

97
tests/test_resources.py Normal file
View File

@@ -0,0 +1,97 @@
"""Tests for MCP subscription resources."""
import json
from unittest.mock import AsyncMock, patch
import pytest
from fastmcp import FastMCP
from unraid_mcp.subscriptions.resources import register_subscription_resources
def _make_resources():
"""Register resources on a test FastMCP instance and return it."""
test_mcp = FastMCP("test")
register_subscription_resources(test_mcp)
return test_mcp
@pytest.fixture
def _mock_subscribe_once():
with patch(
"unraid_mcp.subscriptions.resources.subscribe_once",
new_callable=AsyncMock,
) as mock:
yield mock
@pytest.fixture
def _mock_ensure_started():
with patch(
"unraid_mcp.subscriptions.resources.ensure_subscriptions_started",
new_callable=AsyncMock,
) as mock:
yield mock
class TestLiveResources:
@pytest.mark.parametrize(
"action",
[
"cpu",
"memory",
"cpu_telemetry",
"array_state",
"parity_progress",
"ups_status",
"notifications_overview",
"owner",
"server_status",
],
)
async def test_resource_returns_json(
self,
action: str,
_mock_subscribe_once: AsyncMock,
_mock_ensure_started: AsyncMock,
) -> None:
_mock_subscribe_once.return_value = {"data": "ok"}
mcp = _make_resources()
local_provider = mcp.providers[0]
resource_key = f"resource:unraid://live/{action}@"
resource = local_provider._components[resource_key]
result = await resource.fn()
parsed = json.loads(result)
assert parsed == {"data": "ok"}
async def test_resource_returns_error_dict_on_failure(
self,
_mock_subscribe_once: AsyncMock,
_mock_ensure_started: AsyncMock,
) -> None:
from fastmcp.exceptions import ToolError
_mock_subscribe_once.side_effect = ToolError("WebSocket timeout")
mcp = _make_resources()
local_provider = mcp.providers[0]
resource = local_provider._components["resource:unraid://live/cpu@"]
result = await resource.fn()
parsed = json.loads(result)
assert "error" in parsed
assert "WebSocket timeout" in parsed["error"]
class TestLogsStreamResource:
async def test_logs_stream_no_data(self, _mock_ensure_started: AsyncMock) -> None:
with patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr:
mock_mgr.get_resource_data = AsyncMock(return_value=None)
mcp = _make_resources()
local_provider = mcp.providers[0]
resource = local_provider._components["resource:unraid://logs/stream@"]
result = await resource.fn()
parsed = json.loads(result)
assert "status" in parsed