Harden ARR MCP tool surface and installer
This commit is contained in:
@@ -14,12 +14,6 @@ SOURCE = Path(__file__).parents[1] / "platform/mcp/patches/mcp_radarr.py"
|
||||
|
||||
|
||||
def load_module():
|
||||
utilities = types.ModuleType("agent_utilities.mcp_utilities")
|
||||
utilities.dispatch = lambda *args, **kwargs: None
|
||||
utilities.public_actions = lambda client: client.actions
|
||||
utilities.run_blocking = lambda *args, **kwargs: None
|
||||
sys.modules["agent_utilities"] = types.ModuleType("agent_utilities")
|
||||
sys.modules["agent_utilities.mcp_utilities"] = utilities
|
||||
fastmcp = types.ModuleType("fastmcp")
|
||||
fastmcp.FastMCP = object
|
||||
sys.modules["fastmcp"] = fastmcp
|
||||
@@ -66,9 +60,24 @@ class RadarrPatchTests(unittest.TestCase):
|
||||
self.assertEqual(result["returned"], 2)
|
||||
self.assertTrue(result["hasMore"])
|
||||
|
||||
def test_internal_transport_and_power_actions_are_hidden(self):
|
||||
client = types.SimpleNamespace(actions=["get_movie", "request", "post_system_shutdown", "get_queue"])
|
||||
self.assertEqual(self.module._safe_actions(client), ["get_movie", "get_queue"])
|
||||
def test_surface_has_only_explicit_read_tools(self):
|
||||
class FakeMcp:
|
||||
def __init__(self):
|
||||
self.names = []
|
||||
|
||||
def tool(self, **_kwargs):
|
||||
def decorate(function):
|
||||
self.names.append(function.__name__)
|
||||
return function
|
||||
return decorate
|
||||
|
||||
mcp = FakeMcp()
|
||||
self.module.register_radarr_tools(mcp)
|
||||
self.assertEqual(
|
||||
mcp.names,
|
||||
["radarr_find_movie", "radarr_movie_codec_inventory", "radarr_search_releases"],
|
||||
)
|
||||
self.assertNotIn("radarr_action", mcp.names)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -8,6 +8,7 @@ import sys
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
SOURCE = Path(__file__).parents[1] / "platform/mcp/patches/mcp_sonarr.py"
|
||||
@@ -145,6 +146,50 @@ class ReleaseGrabTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(result["results"]["total"], 1)
|
||||
self.assertEqual(result["results"]["items"][0]["guid"], "exact-guid")
|
||||
|
||||
def test_read_only_surface_is_explicit_and_has_no_generic_action(self) -> None:
|
||||
class FakeMcp:
|
||||
def __init__(self):
|
||||
self.names = []
|
||||
|
||||
def tool(self, **_kwargs):
|
||||
def decorate(function):
|
||||
self.names.append(function.__name__)
|
||||
return function
|
||||
return decorate
|
||||
|
||||
with patch.dict("os.environ", {"ARR_MCP_WRITE": "0"}):
|
||||
mcp = FakeMcp()
|
||||
self.module.register_sonarr_tools(mcp)
|
||||
self.assertEqual(
|
||||
mcp.names,
|
||||
[
|
||||
"sonarr_find_series",
|
||||
"sonarr_get_season_summary",
|
||||
"sonarr_search_releases",
|
||||
"sonarr_system_status",
|
||||
],
|
||||
)
|
||||
self.assertNotIn("sonarr_action", mcp.names)
|
||||
|
||||
def test_write_tools_are_registered_only_when_enabled(self) -> None:
|
||||
class FakeMcp:
|
||||
def __init__(self):
|
||||
self.names = []
|
||||
|
||||
def tool(self, **_kwargs):
|
||||
def decorate(function):
|
||||
self.names.append(function.__name__)
|
||||
return function
|
||||
return decorate
|
||||
|
||||
with patch.dict("os.environ", {"ARR_MCP_WRITE": "1"}):
|
||||
mcp = FakeMcp()
|
||||
self.module.register_sonarr_tools(mcp)
|
||||
self.assertIn("sonarr_preview_release_grab", mcp.names)
|
||||
self.assertIn("sonarr_grab_release", mcp.names)
|
||||
self.assertIn("sonarr_preview_episode_search", mcp.names)
|
||||
self.assertIn("sonarr_start_episode_search", mcp.names)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user