deer-flow/backend/tests/test_mcp_config_error_details.py
yetuge 859d45db7e
fix(mcp): keep host paths out of the MCP config mutation 500 details (#7275)
* fix(mcp): keep host paths out of the MCP config mutation 500 details

The five MCP config mutation routes answered an unexpected failure with
`f"Failed to …: {str(e)}"`. Each one runs a read-modify-write of
`extensions_config.json` (`_apply_mcp_config_update`,
`_apply_mcp_servers_create`, `_apply_mcp_server_config_update`,
`_apply_mcp_server_delete`, `_apply_mcp_server_state_update`), so the error the
handler catches is usually an `OSError` from `atomic_write_extensions_config`
that names the absolute host path — the `DEER_FLOW_HOME` layout of the server.

The custom-agent, user-profile and skills-read routes already return fixed text
for this reason (#7255; `routers/AGENTS.md`: "agent config parse errors and
`OSError`s carry host paths and stay in the server log"). These five were the
remaining MCP mutation 500s on the same file, so they now return the fixed
detail and keep the exception in the existing log line.

Red first, on the unmodified `main` — all five put the planted host path in
`detail`:

    $ python -m pytest tests/test_mcp_config_error_details.py -q
    5 failed in 6.37s

After the change, 5 passed; the wider MCP suites (246 tests across
`blocking_io/test_mcp_router.py`, `test_mcp_config_secrets.py`,
`test_mcp_lifecycle*.py`) stay green.

* fix(mcp): normalize personal config mutation errors

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-23 07:20:21 +08:00

125 lines
4.9 KiB
Python

"""MCP config 500s keep host paths out of the response detail.
An ``OSError`` raised while the router resolves, reads or writes
``extensions_config.json`` names the absolute host path in ``str(e)``. The
custom-agent and user-profile routes already return a fixed detail for exactly
this reason (#7255, and ``routers/AGENTS.md`` states the rule); the five MCP
mutation routes still concatenated the exception text into the 500 body, so a
caller learned where ``DEER_FLOW_HOME`` lives on the server.
Each test drives one handler to the write step and asserts the response carries
a fixed detail while the exception itself never reaches the caller. The full
error still goes to the server log.
"""
from __future__ import annotations
import logging
from pathlib import Path
import pytest
from fastapi import HTTPException
from app.gateway.routers import mcp as mcp_router
from app.gateway.routers.mcp import (
McpConfigUpdateRequest,
McpServerConfigResponse,
McpServerConfigUpdateRequest,
McpServerStateUpdateRequest,
create_mcp_servers,
delete_mcp_server,
update_mcp_configuration,
update_mcp_server,
update_mcp_server_state,
)
pytestmark = pytest.mark.asyncio
HOST_PATH = "/srv/private/deer-flow/extensions_config.json"
@pytest.fixture
def written_config(tmp_path: Path, monkeypatch) -> Path:
"""A minimal extensions config the handlers can load before the write fails."""
config_path = tmp_path / "extensions_config.json"
config_path.write_text(
'{"mcpServers": {"remote": {"enabled": true, "type": "http", "url": "https://example.test/mcp"}}, "skills": {}}',
encoding="utf-8",
)
monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(config_path))
async def _noop_admin(_request, **_kwargs) -> None:
return None
monkeypatch.setattr(mcp_router, "require_admin_user", _noop_admin)
return config_path
@pytest.fixture
def failing_write(monkeypatch):
"""Fail the commit with an OSError whose message names an absolute host path."""
def _raise(path, data, **_kwargs):
raise PermissionError(13, "Permission denied", HOST_PATH)
monkeypatch.setattr(mcp_router, "atomic_write_extensions_config", _raise)
def _assert_fixed_detail(exc: HTTPException, expected: str) -> None:
assert exc.status_code == 500
assert exc.detail == expected, f"detail must be fixed text, got {exc.detail!r}"
assert HOST_PATH not in str(exc.detail)
async def test_update_mcp_configuration_hides_the_host_path(written_config, failing_write, caplog) -> None:
body = McpConfigUpdateRequest(mcp_servers={"remote": McpServerConfigResponse(type="http", url="https://example.test/mcp")})
with caplog.at_level(logging.ERROR, logger="app.gateway.routers.mcp"):
with pytest.raises(HTTPException) as excinfo:
await update_mcp_configuration(request=None, body=body)
_assert_fixed_detail(excinfo.value, "Failed to update MCP configuration")
assert HOST_PATH in caplog.text, "the full error must stay in the server log"
async def test_create_mcp_servers_hides_the_host_path(written_config, failing_write, caplog) -> None:
body = McpConfigUpdateRequest(mcp_servers={"added": McpServerConfigResponse(type="http", url="https://example.test/mcp")})
with caplog.at_level(logging.ERROR, logger="app.gateway.routers.mcp"):
with pytest.raises(HTTPException) as excinfo:
await create_mcp_servers(request=None, body=body)
_assert_fixed_detail(excinfo.value, "Failed to add MCP servers")
assert HOST_PATH in caplog.text
async def test_update_mcp_server_hides_the_host_path(written_config, failing_write, caplog) -> None:
body = McpServerConfigUpdateRequest(server_name="remote", server=McpServerConfigResponse(type="http", url="https://example.test/mcp"))
with caplog.at_level(logging.ERROR, logger="app.gateway.routers.mcp"):
with pytest.raises(HTTPException) as excinfo:
await update_mcp_server(request=None, body=body)
_assert_fixed_detail(excinfo.value, "Failed to update MCP server")
assert HOST_PATH in caplog.text
async def test_delete_mcp_server_hides_the_host_path(written_config, failing_write, caplog) -> None:
with caplog.at_level(logging.ERROR, logger="app.gateway.routers.mcp"):
with pytest.raises(HTTPException) as excinfo:
await delete_mcp_server(request=None, server_name="remote")
_assert_fixed_detail(excinfo.value, "Failed to delete MCP server")
assert HOST_PATH in caplog.text
async def test_update_mcp_server_state_hides_the_host_path(written_config, failing_write, caplog) -> None:
body = McpServerStateUpdateRequest(server_name="remote", enabled=False)
with caplog.at_level(logging.ERROR, logger="app.gateway.routers.mcp"):
with pytest.raises(HTTPException) as excinfo:
await update_mcp_server_state(request=None, body=body)
_assert_fixed_detail(excinfo.value, "Failed to update MCP server state")
assert HOST_PATH in caplog.text