fix(mcp): scope list config reads to active profile (#5619)
This commit is contained in:
1 parent
07b9708cde
commit
3c8d9f851e
3 files changed
+32
-10
No files matched your search
+3
-2
@@ -2822,6 +2822,7 @@ from api.config import (
|
|||||||
_load_yaml_config_file,
|
_load_yaml_config_file,
|
||||||
_save_yaml_config_file,
|
_save_yaml_config_file,
|
||||||
reload_config,
|
reload_config,
|
||||||
|
get_config_for_profile_home,
|
||||||
_cfg_lock,
|
_cfg_lock,
|
||||||
PENDING_BG_TASK_COMPLETIONS,
|
PENDING_BG_TASK_COMPLETIONS,
|
||||||
)
|
)
|
||||||
@@ -24181,7 +24182,7 @@ def _mcp_tools_from_registry(server_summaries):
|
|||||||
|
|
||||||
def _handle_mcp_tools_list(handler):
|
def _handle_mcp_tools_list(handler):
|
||||||
"""List known MCP tools from already-available runtime inventory only."""
|
"""List known MCP tools from already-available runtime inventory only."""
|
||||||
cfg = get_config()
|
cfg = get_config_for_profile_home(get_active_hermes_home())
|
||||||
servers = cfg.get("mcp_servers", {})
|
servers = cfg.get("mcp_servers", {})
|
||||||
if not isinstance(servers, dict):
|
if not isinstance(servers, dict):
|
||||||
servers = {}
|
servers = {}
|
||||||
@@ -24695,7 +24696,7 @@ def _handle_notes_item(handler, parsed):
|
|||||||
|
|
||||||
def _handle_mcp_servers_list(handler):
|
def _handle_mcp_servers_list(handler):
|
||||||
"""List configured MCP servers with safe, read-only runtime visibility."""
|
"""List configured MCP servers with safe, read-only runtime visibility."""
|
||||||
cfg = get_config()
|
cfg = get_config_for_profile_home(get_active_hermes_home())
|
||||||
servers = cfg.get("mcp_servers", {})
|
servers = cfg.get("mcp_servers", {})
|
||||||
if not isinstance(servers, dict):
|
if not isinstance(servers, dict):
|
||||||
servers = {}
|
servers = {}
|
||||||
|
|||||||
@@ -42,17 +42,33 @@ SAMPLE_MCP = {
|
|||||||
class TestMcpList:
|
class TestMcpList:
|
||||||
"""GET /api/mcp/servers — list with masked secrets."""
|
"""GET /api/mcp/servers — list with masked secrets."""
|
||||||
|
|
||||||
@patch('api.routes.get_config')
|
@patch('api.routes.get_config_for_profile_home')
|
||||||
def test_returns_servers_list(self, mock_cfg):
|
@patch('api.routes.get_active_hermes_home')
|
||||||
|
def test_returns_servers_list(self, mock_home, mock_cfg):
|
||||||
|
mock_home.return_value = sentinel_home = object()
|
||||||
mock_cfg.return_value = {'mcp_servers': SAMPLE_MCP}
|
mock_cfg.return_value = {'mcp_servers': SAMPLE_MCP}
|
||||||
h = _make_handler()
|
h = _make_handler()
|
||||||
_handle_mcp_servers_list(h)
|
_handle_mcp_servers_list(h)
|
||||||
assert h.send_response.called
|
assert h.send_response.called
|
||||||
status = h.send_response.call_args[0][0]
|
status = h.send_response.call_args[0][0]
|
||||||
assert status == 200
|
assert status == 200
|
||||||
|
mock_cfg.assert_called_once_with(sentinel_home)
|
||||||
|
|
||||||
@patch('api.routes.get_config')
|
@patch('api.routes.get_config_for_profile_home')
|
||||||
def test_empty_config(self, mock_cfg):
|
@patch('api.routes.get_active_hermes_home')
|
||||||
|
def test_reads_active_profile_home_for_servers(self, mock_home, mock_cfg):
|
||||||
|
mock_home.return_value = sentinel_home = object()
|
||||||
|
mock_cfg.return_value = {'mcp_servers': {'active': SAMPLE_MCP['searxng']}}
|
||||||
|
h = _make_handler()
|
||||||
|
_handle_mcp_servers_list(h)
|
||||||
|
payload = _json_payload(h)
|
||||||
|
assert [srv['name'] for srv in payload['servers']] == ['active']
|
||||||
|
mock_cfg.assert_called_once_with(sentinel_home)
|
||||||
|
|
||||||
|
@patch('api.routes.get_config_for_profile_home')
|
||||||
|
@patch('api.routes.get_active_hermes_home')
|
||||||
|
def test_empty_config(self, mock_home, mock_cfg):
|
||||||
|
mock_home.return_value = object()
|
||||||
mock_cfg.return_value = {}
|
mock_cfg.return_value = {}
|
||||||
h = _make_handler()
|
h = _make_handler()
|
||||||
_handle_mcp_servers_list(h)
|
_handle_mcp_servers_list(h)
|
||||||
@@ -65,8 +81,10 @@ class TestMcpList:
|
|||||||
assert payload['reload_required'] is True
|
assert payload['reload_required'] is True
|
||||||
|
|
||||||
@patch('api.routes._mcp_runtime_status_by_name')
|
@patch('api.routes._mcp_runtime_status_by_name')
|
||||||
@patch('api.routes.get_config')
|
@patch('api.routes.get_config_for_profile_home')
|
||||||
def test_list_payload_includes_status_tool_counts_and_safe_invalid_config(self, mock_cfg, mock_runtime):
|
@patch('api.routes.get_active_hermes_home')
|
||||||
|
def test_list_payload_includes_status_tool_counts_and_safe_invalid_config(self, mock_home, mock_cfg, mock_runtime):
|
||||||
|
mock_home.return_value = object()
|
||||||
mock_cfg.return_value = {
|
mock_cfg.return_value = {
|
||||||
'mcp_servers': {
|
'mcp_servers': {
|
||||||
'searxng': {'command': 'mcp-searxng', 'args': ['--port', '8888']},
|
'searxng': {'command': 'mcp-searxng', 'args': ['--port', '8888']},
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ def _read(relative_path: str) -> str:
|
|||||||
|
|
||||||
class TestMcpToolInventoryApi:
|
class TestMcpToolInventoryApi:
|
||||||
@patch("api.routes._mcp_runtime_status_by_name")
|
@patch("api.routes._mcp_runtime_status_by_name")
|
||||||
@patch("api.routes.get_config")
|
@patch("api.routes.get_config_for_profile_home")
|
||||||
def test_endpoint_returns_sanitized_registered_mcp_tools(self, mock_cfg, mock_runtime):
|
@patch("api.routes.get_active_hermes_home")
|
||||||
|
def test_endpoint_returns_sanitized_registered_mcp_tools(self, mock_home, mock_cfg, mock_runtime):
|
||||||
|
mock_home.return_value = sentinel_home = object()
|
||||||
mock_cfg.return_value = {
|
mock_cfg.return_value = {
|
||||||
"mcp_servers": {
|
"mcp_servers": {
|
||||||
"web-reader": {"url": "http://localhost:3001/mcp", "headers": {"Authorization": "Bearer secret-token"}},
|
"web-reader": {"url": "http://localhost:3001/mcp", "headers": {"Authorization": "Bearer secret-token"}},
|
||||||
@@ -72,6 +74,7 @@ class TestMcpToolInventoryApi:
|
|||||||
{"name": "url", "type": "string", "required": True, "description": "URL to fetch"},
|
{"name": "url", "type": "string", "required": True, "description": "URL to fetch"},
|
||||||
{"name": "limit", "type": "integer", "required": False, "description": "Maximum bytes"},
|
{"name": "limit", "type": "integer", "required": False, "description": "Maximum bytes"},
|
||||||
]
|
]
|
||||||
|
mock_cfg.assert_called_once_with(sentinel_home)
|
||||||
raw = json.dumps(payload)
|
raw = json.dumps(payload)
|
||||||
assert "secret-token" not in raw
|
assert "secret-token" not in raw
|
||||||
assert "default" not in raw
|
assert "default" not in raw
|
||||||
|
|||||||
Reference in new issue
Block a user