From b16c0fd5269de17ee3d8ea9c7df3fce4a28686a9 Mon Sep 17 00:00:00 2001 From: AJ Date: Thu, 2 Jul 2026 00:27:01 -0400 Subject: [PATCH] fix: point Claude Code profile at ~/.claude.json, not settings.json Claude Code stores user-scope MCP servers in ~/.claude.json (what 'claude mcp add' writes); ~/.claude/settings.json is for permissions and hooks and rejects an mcpServers key with a schema error, so BCC was reading (and writing) servers where Claude Code never looks. If servers are found parked in settings.json, that file is still listed as 'Claude Code (legacy settings.json)' so they can be copied into the real config via Copy to. Docs updated; verified against docs.claude.com/en/docs/claude-code/settings. --- CLAUDE.md | 2 +- README.md | 9 ++++++--- bcc_core.py | 24 ++++++++++++++++++++---- tests/test_core.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7f20867..78731fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ The codebase is split into two layers: **`bcc_core.py`** — All logic with no GUI imports. Contains: - `Profile` / `ServerEntry` dataclasses (the data model) -- `discover_profiles()` — scans the platform's app-support directory for `Claude*` folders (Claude Desktop) **and** always adds `~/.claude/settings.json` (Claude Code, cross-platform) +- `discover_profiles()` — scans the platform's app-support directory for `Claude*` folders (Claude Desktop) **and** always adds `~/.claude.json` (Claude Code user scope — what `claude mcp add` writes). `~/.claude/settings.json` is NOT a server config (it rejects `mcpServers` with a schema error) and is only surfaced, labelled legacy, if servers are found parked in it. Project-scope `.mcp.json` files can be opened via Add config… - `load_config` / `extract_servers` / `apply_servers` / `write_config` — the read/write pipeline; writes are atomic with rotating timestamped backups in `.bcc_backups/` - `parse_pasted_json()` / `parse_pasted_json_verbose()` — accepts three JSON shapes (full config, inner map, or bare server object). Input does not have to be valid JSON: `repair_json_text()` auto-fixes markdown fences, surrounding prose, `//` `/* */` `#` comments, trailing/missing commas, smart quotes, single quotes, unquoted keys, Python/JS literals, and unclosed braces. The verbose variant also returns human-readable notes describing every repair applied (shown live in the paste dialog) - `check_dependency()` / `diagnostics_text()` / `pin_command_path()` — PATH resolution logic; distinguishes "found on normal PATH" (ok) vs "found only on augmented PATH" (warn) vs "not found" (missing). Uses an `lru_cache`-memoized `augmented_path()` that extends the inherited PATH with common runtime locations (nvm, homebrew, cargo, volta, etc.) diff --git a/README.md b/README.md index a9b4d08..fbba496 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,9 @@ python bcc.py - **Auto-discovers installs** — scans the platform's app-support folder for any `Claude*` directory (so `Claude` and `Claude-Work` both show up) **and** finds - Claude Code at `~/.claude/settings.json`. Use **Add config…** to point at any - other file manually. + Claude Code's user-scope config at `~/.claude.json` (the file `claude mcp add` + writes). Use **Add config…** to point at any other file manually — e.g. a + project's `.mcp.json`. - **Form-based editing** — name, command, args (one per line), env vars, or for remote servers: URL, transport, and headers. No raw JSON. - **Paste JSON — even broken JSON** — drop in any snippet from an MCP doc (full @@ -80,7 +81,9 @@ After saving, **restart that Claude install** for changes to take effect. | Windows | `%APPDATA%` | | Linux | `~/.config` | -**Claude Code** (all platforms): `~/.claude/settings.json` +**Claude Code** (all platforms): `~/.claude.json` (user scope). If servers are +found parked in `~/.claude/settings.json` — where Claude Code ignores them — that +file is also listed, marked *legacy*, so you can copy them over. ## Files diff --git a/bcc_core.py b/bcc_core.py index 4a3ff37..f198103 100644 --- a/bcc_core.py +++ b/bcc_core.py @@ -87,7 +87,10 @@ def discover_profiles() -> list[Profile]: Claude Desktop: scans the platform app-support folder for any `Claude*` directory (catches `Claude`, `Claude-Work`, etc.). - Claude Code: always at ~/.claude/settings.json on every platform. + Claude Code: user-scope MCP servers live in ~/.claude.json (that's what + `claude mcp add` writes; project scope is a per-repo .mcp.json, which can + be opened via 'Add config…'). NOT ~/.claude/settings.json — that file is + for permissions/hooks and rejects an mcpServers key with a schema error. """ base = app_support_base() out: list[Profile] = [] @@ -99,11 +102,24 @@ def discover_profiles() -> list[Profile]: cfg = d / CONFIG_FILENAME out.append(Profile(label=d.name, path=cfg, config_exists=cfg.is_file())) - # Claude Code global settings (~/.claude/settings.json) — same mcpServers format, - # cross-platform (the ~/.claude/ directory is always in the same place). - cc_cfg = Path.home() / ".claude" / "settings.json" + home = Path.home() + cc_cfg = home / ".claude.json" out.append(Profile(label="Claude Code", path=cc_cfg, config_exists=cc_cfg.is_file())) + # Legacy: earlier BCC versions (and hand-edits) may have parked servers in + # ~/.claude/settings.json, where Claude Code ignores them. Surface that + # file only when it actually contains an mcpServers block, so the user can + # Copy to ▸ the entries into the real config. + legacy = home / ".claude" / "settings.json" + if legacy.is_file(): + with contextlib.suppress(Exception): + if "mcpServers" in load_config(legacy): + out.append( + Profile( + label="Claude Code (legacy settings.json)", path=legacy, config_exists=True + ) + ) + return out diff --git a/tests/test_core.py b/tests/test_core.py index 1a4b5a8..4825bee 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -33,6 +33,34 @@ def test_flags_missing_config_file(fake_base): assert any(p.label == "Claude-Work" and not p.config_exists for p in profs) +@pytest.fixture +def fake_home(tmp_path, monkeypatch, fake_base): + home = tmp_path / "home" + (home / ".claude").mkdir(parents=True) + monkeypatch.setattr(c.Path, "home", lambda: home) + return home + + +def test_claude_code_uses_dot_claude_json(fake_home): + (fake_home / ".claude.json").write_text('{"mcpServers": {"x": {"command": "npx"}}}') + profs = c.discover_profiles() + cc = [p for p in profs if p.label == "Claude Code"] + assert len(cc) == 1 + assert cc[0].path == fake_home / ".claude.json" + assert cc[0].config_exists + + +def test_legacy_settings_json_surfaced_only_with_servers(fake_home): + # settings.json without mcpServers -> not listed + (fake_home / ".claude" / "settings.json").write_text('{"permissions": {}}') + assert not any("legacy" in p.label for p in c.discover_profiles()) + # settings.json WITH parked mcpServers -> listed so the user can migrate + (fake_home / ".claude" / "settings.json").write_text( + '{"mcpServers": {"old": {"command": "npx"}}}' + ) + assert any("legacy" in p.label for p in c.discover_profiles()) + + # --------------------------------------------------------------------------- # # 2. Write pipeline: preserves other keys + order, only touches mcpServers # --------------------------------------------------------------------------- #