Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9b5a04a
scripts(feat[mcp_swap]): add --scope {user,project} for Claude two-la…
tony May 4, 2026
1d89169
tests(mcp_swap) cover --scope behaviour, v1→v2 state migration, backu…
tony May 4, 2026
a7bccf8
docs(scripts/README,CHANGES) document --scope flag and v2 state migra…
tony May 4, 2026
72bd516
docs(CHANGES) move mcp_swap entry under ### Development
tony May 4, 2026
ecb7f82
scripts(fix[mcp_swap]): revert in LIFO order so multi-scope unwind is…
tony May 4, 2026
4a659f5
scripts(fix[mcp_swap]): guard user-scope Claude mcpServers shape on w…
tony May 4, 2026
e2e295f
docs(scripts/mcp_swap) document --scope in the module Scope section
tony May 4, 2026
a128c86
docs(scripts/mcp_swap) clarify cmd_revert --cli fallback comment
tony May 4, 2026
3ecde4d
scripts(refactor[mcp_swap]): extract _claude_user_servers helper for …
tony May 4, 2026
cf2d14d
scripts(refactor[mcp_swap]): add swapped_at to SwapEntry for explicit…
tony May 4, 2026
a8b02e2
scripts(refactor[mcp_swap]): drop migration code — no-compat dev tooling
tony May 4, 2026
72c56b3
scripts(refactor[mcp_swap]): explicit seq_no for LIFO — match Lib/sch…
tony May 4, 2026
d75ed5b
docs(scripts/mcp_swap,CHANGES) describe shipped schema — drop stale v…
tony May 4, 2026
5f1aaac
docs(scripts/mcp_swap) drop "hand-edited state" from LIFO independenc…
tony May 4, 2026
2a3316d
docs(scripts/mcp_swap) drop stale "v1 backups" terminology
tony May 4, 2026
7d98e58
docs(scripts/README) clarify install-path wording — drop "post-migrat…
tony May 4, 2026
4f16ef1
fixup! docs(scripts/mcp_swap,CHANGES) describe shipped schema — drop …
tony May 7, 2026
818e0e7
scripts(fix[mcp_swap]): print clean error on malformed Claude config …
tony May 7, 2026
d37a149
scripts(fix[mcp_swap]): drop entries with corrupt seq_no on state-fil…
tony May 7, 2026
ee9ca9b
scripts(fix[mcp_swap]): unlink backup file after successful revert
tony May 7, 2026
7768b72
scripts(feat[mcp_swap]): add --scope filter to status command
tony May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@
_Notes on upcoming releases will be added here_
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### Development

- `scripts/mcp_swap.py` — `use-local` and `revert` accept
`--scope {user,project}` so a Claude install can target either the
top-level `mcpServers` fallback (every project without an override
picks it up) or the per-project `projects.<abs>.mcpServers` entry.
Default `project` preserves the previous behaviour. Codex / Cursor /
Gemini have no per-project layer; the flag is silently coerced to
`user` for them. A single install can hold both scopes
simultaneously with independent backups, and `revert --scope user`
restores just one layer. Full `revert` unwinds in LIFO order via an
explicit `seq_no` counter on each state entry — no reliance on dict
iteration through the JSON round-trip. Shape validation is
centralised in `_claude_user_servers` / `_claude_project_node`
helpers so read / write / delete fail symmetrically with
`RuntimeError` on a malformed Claude config. The state-file schema
is internal — no compatibility contract; running an older `mcp_swap`
against a newer `state.json` is undefined behaviour and not
supported. (#35)

### Documentation

- Bump gp-sphinx docs stack to v0.0.1a16 — docs site now renders
Expand Down
45 changes: 39 additions & 6 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,47 @@ This matches Claude's conventional dev form and takes advantage of `uv
run`'s automatic editable install — source edits flow through on the next
invocation with no reinstall step.

### `--scope {user,project}` (Claude only)

Claude's `~/.claude.json` has two layers: a top-level `mcpServers`
fallback that any project without an override picks up, and per-project
`projects.<abs>.mcpServers` overrides. The flag selects which layer the
swap writes:

```console
$ uv run scripts/mcp_swap.py use-local --cli claude --scope project # default
$ uv run scripts/mcp_swap.py use-local --cli claude --scope user # global fallback
```

`--scope project` (the default) preserves pre-flag behaviour: writes
under `projects[<repo-abs-path>].mcpServers` and only that one project
sees the change. `--scope user` flips the top-level fallback so every
unrelated project directory picks up the local checkout too — useful
when you're branch-testing across many repos at once.

Codex, Cursor, and Gemini have no per-project layer in their config
files. The flag is silently coerced to `user` for them, so passing
`--scope` with a non-Claude `--cli` is harmless.

A single Claude install can hold both scopes simultaneously — separate
state entries, separate backups. `revert --scope user` restores only
the user-level fallback; the project entry stays. `revert` without
`--scope` rolls back every recorded scope for the targeted CLIs.

### Safety

- Every rewrite writes a timestamped backup (`<config>.bak.mcp-swap-<ts>`)
before touching the file.
before touching the file. Claude backups also embed the scope
(`<config>.bak.mcp-swap-<ts>-{user,project}`) so two scope swaps in the
same second don't collide.
- State is tracked in `~/.local/state/libtmux-mcp-dev/swap/state.json`
(honours `XDG_STATE_HOME`) so `revert` knows which backup to restore
per CLI, including the "added" case where Codex had no libtmux block
before.
per `(cli, scope)` pair, including the "added" case where Codex had
no libtmux block before. Each entry records `swapped_at` (wall-clock
timestamp, human-readable for debug) and `seq_no` (monotonic counter,
the primary LIFO sort key). Schema is internal — no compatibility
contract; running an older `mcp_swap` against a newer `state.json` is
undefined.
- Writes are atomic (tempfile + `os.replace`) and re-validated by
re-parsing; a bad write is rolled back immediately.
- `--dry-run` prints a unified diff and writes nothing.
Expand Down Expand Up @@ -82,9 +115,9 @@ entries untouched.
script writes.
- **Custom binary install locations.** Detection is `shutil.which` plus
the file existing at the configured global path. Homebrew, npm
prefixes (`~/.npm-global/bin`), and post-migration paths
(`~/.claude/local/claude`, `~/.gemini/local/gemini`) are picked up
only when the binary is already on `PATH`.
prefixes (`~/.npm-global/bin`), and the canonical local-install
layouts (`~/.claude/local/claude`, `~/.gemini/local/gemini`) are
picked up only when the binary is already on `PATH`.

### Extending to a new CLI

Expand Down
Loading