diff --git a/headroom/mcp_registry/claude.py b/headroom/mcp_registry/claude.py index c537e679b..8dc82c846 100644 --- a/headroom/mcp_registry/claude.py +++ b/headroom/mcp_registry/claude.py @@ -47,6 +47,7 @@ class ClaudeRegistrar(MCPRegistrar): """ home = home_dir if home_dir is not None else Path.home() self._claude_dir = _resolve_claude_config_dir(home, config_dir, honor_env=home_dir is None) + self._isolated_cli_env = home_dir is not None or config_dir is not None self._modern_config = self._claude_dir / ".claude.json" self._legacy_config = self._claude_dir / "mcp.json" if claude_cli is ...: @@ -96,6 +97,7 @@ class ClaudeRegistrar(MCPRegistrar): [str(self._claude_cli), "mcp", "remove", server_name, "-s", "user"], capture_output=True, text=True, + env=self._claude_cli_env(), ) if result.returncode == 0: return True @@ -121,6 +123,7 @@ class ClaudeRegistrar(MCPRegistrar): cmd, capture_output=True, text=True, + env=self._claude_cli_env(), ) if result.returncode == 0: return RegisterResult(RegisterStatus.REGISTERED, "via `claude mcp add` (scope: user)") @@ -187,10 +190,16 @@ class ClaudeRegistrar(MCPRegistrar): return None return _entry_to_spec(server_name, entry) + # ---------------------------------------------------------------------- + # Helpers + # ---------------------------------------------------------------------- -# ---------------------------------------------------------------------- -# Helpers -# ---------------------------------------------------------------------- + def _claude_cli_env(self) -> dict[str, str] | None: + if not self._isolated_cli_env: + return None + env = os.environ.copy() + env["CLAUDE_CONFIG_DIR"] = str(self._claude_dir) + return env def _resolve_claude_config_dir( diff --git a/tests/test_mcp_registry/test_claude_registrar.py b/tests/test_mcp_registry/test_claude_registrar.py index 12a1e2f68..985aeab47 100644 --- a/tests/test_mcp_registry/test_claude_registrar.py +++ b/tests/test_mcp_registry/test_claude_registrar.py @@ -158,8 +158,9 @@ def test_register_via_cli_calls_claude_mcp_add( with patch("subprocess.run", return_value=fake_result) as run_mock: result = reg.register_server(_install_spec(monkeypatch)) assert result.status == RegisterStatus.REGISTERED - cmds = [call.args[0] for call in run_mock.call_args_list] - add_cmd = next(c for c in cmds if "add" in c) + add_call = run_mock.call_args + assert add_call is not None + add_cmd = add_call.args[0] assert add_cmd[:6] == [ "/usr/local/bin/claude", "mcp", @@ -173,6 +174,7 @@ def test_register_via_cli_calls_claude_mcp_add( _RESOLVED_COMMAND[0], *_RESOLVED_ARGS, ] + assert add_call.kwargs["env"]["CLAUDE_CONFIG_DIR"] == str(tmp_path / ".claude") def test_register_via_cli_includes_env(tmp_path: Path) -> None: @@ -186,10 +188,38 @@ def test_register_via_cli_includes_env(tmp_path: Path) -> None: fake_result = subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="") with patch("subprocess.run", return_value=fake_result) as run_mock: reg.register_server(spec) - add_cmd = next(c for c in [call.args[0] for call in run_mock.call_args_list] if "add" in c) + add_call = run_mock.call_args + assert add_call is not None + add_cmd = add_call.args[0] assert "-e" in add_cmd e_idx = add_cmd.index("-e") assert add_cmd[e_idx + 1] == "HEADROOM_PROXY_URL=http://127.0.0.1:9000" + assert add_call.kwargs["env"]["CLAUDE_CONFIG_DIR"] == str(tmp_path / ".claude") + + +def test_register_via_cli_without_overrides_keeps_ambient_env( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("CLAUDE_CONFIG_DIR", "ambient") + reg = ClaudeRegistrar(claude_cli="/usr/local/bin/claude") + fake_result = subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="") + with patch("subprocess.run", return_value=fake_result) as run_mock: + reg.register_server(_spec()) + assert run_mock.call_args is not None + assert run_mock.call_args.kwargs["env"] is None + + +def test_register_via_cli_prefers_explicit_config_dir_over_ambient_env( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + config_dir = tmp_path / "explicit-config" + monkeypatch.setenv("CLAUDE_CONFIG_DIR", "ambient") + reg = ClaudeRegistrar(claude_cli="/usr/local/bin/claude", config_dir=config_dir) + fake_result = subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="") + with patch("subprocess.run", return_value=fake_result) as run_mock: + reg.register_server(_spec()) + assert run_mock.call_args is not None + assert run_mock.call_args.kwargs["env"]["CLAUDE_CONFIG_DIR"] == str(config_dir) def test_register_writes_file_when_no_cli(tmp_path: Path) -> None: @@ -333,9 +363,11 @@ def test_unregister_via_cli(tmp_path: Path) -> None: ok = subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="") with patch("subprocess.run", return_value=ok) as run_mock: assert reg.unregister_server("headroom") is True - cmd = run_mock.call_args_list[0].args[0] + assert run_mock.call_args is not None + cmd = run_mock.call_args.args[0] assert cmd[:5] == ["/usr/local/bin/claude", "mcp", "remove", "headroom", "-s"] assert cmd[5] == "user" + assert run_mock.call_args.kwargs["env"]["CLAUDE_CONFIG_DIR"] == str(tmp_path / ".claude") def test_unregister_via_file_when_no_cli(tmp_path: Path) -> None: