diff --git a/src/builtin_mcp.py b/src/builtin_mcp.py index 2a4b748ee..b777e99a4 100644 --- a/src/builtin_mcp.py +++ b/src/builtin_mcp.py @@ -104,6 +104,21 @@ def _spawn_bg(coro) -> asyncio.Task: return task +def builtin_python_env(base_dir: str) -> dict[str, str]: + """Environment for built-in Python MCP subprocesses. + + The app root must be importable so mcp_servers can import local modules, but + replacing PYTHONPATH entirely hides site-packages in container/dev launches + that rely on PYTHONPATH for their active environment. + """ + existing = os.environ.get("PYTHONPATH", "") + parts = [base_dir] + for item in existing.split(os.pathsep): + if item and item not in parts: + parts.append(item) + return {"PYTHONPATH": os.pathsep.join(parts)} + + async def register_builtin_servers(mcp_manager): """Connect all built-in MCP servers to the manager.""" if MCP_DISABLED: @@ -121,7 +136,7 @@ async def register_builtin_servers(mcp_manager): transport="stdio", command=python, args=[script_path], - env={"PYTHONPATH": base_dir}, + env=builtin_python_env(base_dir), ) if ok: logger.info(f"Built-in MCP server registered: {name}") diff --git a/src/mcp_manager.py b/src/mcp_manager.py index 8f4322375..90430c78f 100644 --- a/src/mcp_manager.py +++ b/src/mcp_manager.py @@ -504,7 +504,7 @@ class McpManager: async def _reconnect_builtin(self, server_id: str) -> bool: """Tear down and reconnect a crashed builtin MCP server.""" import sys - from src.builtin_mcp import _BUILTIN_SERVERS + from src.builtin_mcp import _BUILTIN_SERVERS, builtin_python_env if server_id not in _BUILTIN_SERVERS: return False @@ -523,7 +523,7 @@ class McpManager: transport="stdio", command=sys.executable, args=[script_path], - env={"PYTHONPATH": base_dir}, + env=builtin_python_env(base_dir), ) if ok: logger.info(f"Reconnected builtin MCP server: {name}") diff --git a/tests/test_builtin_mcp_pythonpath.py b/tests/test_builtin_mcp_pythonpath.py new file mode 100644 index 000000000..9400fd35c --- /dev/null +++ b/tests/test_builtin_mcp_pythonpath.py @@ -0,0 +1,22 @@ +import os + +from src.builtin_mcp import builtin_python_env + + +def test_builtin_python_env_preserves_existing_pythonpath(monkeypatch): + monkeypatch.setenv( + "PYTHONPATH", + os.pathsep.join(["/app/venv/lib/python3.13/site-packages", "/app", "/extra"]), + ) + + env = builtin_python_env("/app") + + assert env == { + "PYTHONPATH": os.pathsep.join(["/app", "/app/venv/lib/python3.13/site-packages", "/extra"]) + } + + +def test_builtin_python_env_uses_app_root_without_existing_pythonpath(monkeypatch): + monkeypatch.delenv("PYTHONPATH", raising=False) + + assert builtin_python_env("/srv/odysseus") == {"PYTHONPATH": "/srv/odysseus"}