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 # --------------------------------------------------------------------------- #