claude-multiacc 2.0.37 → 2.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +84 -6
  2. package/bin/claude +122 -6
  3. package/bin/claude-accounts +146 -9
  4. package/bin/codex +116 -7
  5. package/bin/codex-accounts +137 -5
  6. package/docs/ACCOUNT_OPERATIONS.md +34 -0
  7. package/docs/CODEX.md +29 -2
  8. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  9. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  10. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  11. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  12. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  16. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  17. package/lib/claude_reset.py +70 -35
  18. package/lib/codex_reset.py +31 -4
  19. package/lib/codex_reset_telemetry.py +4 -2
  20. package/lib/common.sh +19 -0
  21. package/lib/mcp_registry.py +1372 -0
  22. package/package.json +2 -2
  23. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  24. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  25. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  26. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  27. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  28. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  29. package/tests/run-tests.sh +588 -0
  30. package/tests/test_account_seeding.py +40 -0
  31. package/tests/test_claude_reset.py +61 -11
  32. package/tests/test_codex_reset.py +43 -2
  33. package/tests/test_mcp_registry.py +597 -0
package/README.md CHANGED
@@ -177,17 +177,17 @@ buckets keep being tracked and the ≥90% rule keeps working. Unparseable entrie
177
177
  skipped, an all-legacy payload falls back to `five_hour`/`seven_day`, and a payload the
178
178
  code cannot read at all degrades that one account (fail open), never the run.
179
179
 
180
- **Limit resets are redeemed automatically, but only when one earns its keep.** Claude
180
+ **A parked account gets its limit reset automatically.** Claude
181
181
  Code 2.1.280 added `/limit-reset`: an account can hold a *grant* (the Opus 5.5 launch gave
182
182
  every Pro/Max account one reset, usable until 2026-10-22) that refills its five-hour and
183
183
  weekly limits at once while the weekly reset day stays put. The same usage call carries the
184
184
  grant's status (`?cedar_ember=1&skip_spend=1`, Claude Code's own read, so it costs nothing
185
185
  against the endpoint's hourly budget), and the limits pass claims it with
186
- `POST /api/organizations/<org>/reset_rate_limits` when a **weekly** limit the grant refills
187
- is ≥ 95% used or reported exhausted — the codex rule, narrowed because a Claude reset is one
188
- irreversible shot. A full five-hour window alone never spends it (it refills in hours and the
189
- pool picks another account), and it is held while the account's own week refills in under
190
- 24 h (`CLAUDE_MULTIACC_RESET_MIN_HORIZON`, seconds). The claim carries a request id derived
186
+ `POST /api/organizations/<org>/reset_rate_limits` whenever the pool has **parked** the account on a
187
+ window the grant refills — a bucket at/over the pool's own threshold (90%), a limit the server
188
+ reports exhausted, or an active park (a client 429, or the app-robot panel's own park) — and holds
189
+ it only when every such park lifts on its own within the hour (`CLAUDE_MULTIACC_RESET_MIN_HORIZON`,
190
+ seconds). The claim carries a request id derived
191
191
  from the account, grant, remaining count and week, persisted before the POST, so a lost
192
192
  response is retried with the same id and fleet peers converge on one claim; a claim whose
193
193
  answer never came back is settled by the grant's own remaining count dropping, and a claim
@@ -395,6 +395,84 @@ an account, and if one slips through it returns to the pool by itself. Output is
395
395
  never double-emits. Only engages when stdin is finite (tty / regular file / `/dev/null`)
396
396
  and ≥2 accounts are eligible; service-spawned pipes take the plain exec path untouched.
397
397
 
398
+ ## MCP servers for every account
399
+
400
+ Both clients keep MCP servers in the **config dir** — `<dir>/.claude.json` for Claude Code,
401
+ `<CODEX_HOME>/config.toml` for Codex — so under the pool there is one MCP configuration per
402
+ account, and a stock `claude mcp add` lands in **one random account**. That is how
403
+ `appinspire-mcp` was connected in some sessions and missing in others (2026-09-22). The pool
404
+ now owns a **registry**, `<pool>/mcp-servers.json`, that is reconciled into every account:
405
+
406
+ ```bash
407
+ claude-accounts mcp add appinspire-mcp -- npx -y appinspire-mcp@latest serve # both pools
408
+ claude-accounts mcp add --provider claude local-dev -e KEY=v -- node /path/server.mjs serve
409
+ claude-accounts mcp add --scope project --project "$HOME/design-lab/run1" appinspire -e APPINSPIRE_LIBRARY_DIR="$HOME/.appinspire-mcp/library" -- node "$HOME/appinspire-mcp/bin/appinspire-mcp.mjs" serve
410
+ claude-accounts mcp remove adspower-local-api # retire EVERYWHERE: every account, every project entry
411
+ claude-accounts mcp remove foo --user-only # user scope only; project-local copies stay
412
+ claude-accounts mcp list [--json]
413
+ claude-accounts mcp apply # re-apply now (repair drift)
414
+ ```
415
+
416
+ - **Default `--provider both`**: an MCP server is not provider-specific, so `claude-accounts
417
+ mcp add` also writes the codex pool's registry (and `codex-accounts mcp add` the claude
418
+ one); `--provider claude|codex` narrows it, and an explicit provider whose pool is missing
419
+ on this machine is an error (the default is silent about it). `-e KEY=VAL` takes one
420
+ variable per flag; write `$HOME/...`, never `~/...`, inside a value (zsh does not expand
421
+ `~` after `=` in a word).
422
+ - **Tombstones**: an explicit `mcp remove NAME` retires the server **everywhere** — user
423
+ scope and every project entry, in every account, on every Mac after sync — even a copy
424
+ that was only ever hand-added to a single account (registry key `retired`). `--user-only`
425
+ records a user-scope-only tombstone (`retiredUser`). A stock removal mirrored from the shim
426
+ is scope-limited: `claude mcp remove -s user X` retires X in user scope only, a `-s local`
427
+ removal retires it in that one project only. Re-adding a name lifts its tombstone.
428
+ - **Exit status 3** from `mcp add|add-json|remove` means the registry was saved but some
429
+ account could not be reconciled (a corrupt `.claude.json`, a `config.toml` layout the
430
+ editor refuses): the change still syncs and still reaches the sibling pool; fix the account
431
+ and run `mcp apply`. Exit 1 means nothing was saved.
432
+ - **Where it is applied**: at seed time (`add`/`import`/`sync`/`post-sync`, so a new account or
433
+ a fleet Mac gets it), and by the **shim right before exec** — the picked account is
434
+ reconciled when the registry, the machine-local overlay or the account's own config changed
435
+ since the last verified reconcile (stamp in `<acct>/.mcp-applied`); Claude Code rewrites
436
+ `.claude.json` during most sessions, so on the claude side that check usually costs one
437
+ short python start-up per launch. Fail-open: a missing python or lib never breaks a launch.
438
+ - **Stock commands are mirrored**: `claude mcp add|add-json|remove` and `codex mcp add|remove`
439
+ run under the shim execute against the picked account as usual, and the shim then copies the
440
+ outcome into the registry and every other account (`learn`). This includes a command run
441
+ from **inside** a pooled session (an agent's Bash tool, `npx appinspire-mcp install` run by
442
+ an agent): the session's config dir is a pool account, so the same mirror applies. A
443
+ config dir outside the pool is plain passthrough.
444
+ - **Replicas**: on a pool whose `sync-role` file says `replica`, `add`/`remove` and the
445
+ shim's mirror land in the machine-local overlay (owner `local`) instead of the synced
446
+ registry, because the source's next push would overwrite them. Make registry changes on
447
+ the source machine.
448
+ - **Project scope is claude-only** (`--scope project --project PATH`, or a stock `-s local`
449
+ add): Codex 0.156 reads no per-project MCP config, so a project entry is applied to every
450
+ claude account's `projects[PATH]` and skipped for codex.
451
+ - **Fleet**: the **claude** registry rides with the manifest — `sync` pushes it to the server
452
+ and every manifest peer (all runner Macs), and the claude limits pass fans it out with the
453
+ telemetry, so a mirrored add reaches every Mac within one cadence; each target's
454
+ `post-sync` re-seeds and applies it. The **codex** registry reaches only codex sync
455
+ targets (the server, gas-mini): runner Macs' codex pools are local-only and get their
456
+ servers from app-robot's runner overlay (`codex-accounts mcp import-local --owner
457
+ app-robot`, app-robot PR #1870) or from a direct `codex-accounts mcp add --provider codex …`
458
+ on that Mac. The **machine-local overlay** `mcp-servers.local.json` is applied on its Mac
459
+ and **never synced**; the synced registry wins on a name both hold.
460
+ - **Codex translation**: a Claude-style block becomes `[mcp_servers.<name>]`; unrelated
461
+ tables, their comments and blank lines are left alone. `KEY=${KEY}` env references become
462
+ `env_vars` (codex forwards a variable under its own name only); `KEY=${OTHER}` and
463
+ `${VAR:-default}` cannot be expressed, so that variable is left out with a warning. A stdio
464
+ server gets `startup_timeout_sec = 60` unless the block sets one (a cold `npx` download must
465
+ not trip Codex's 10 s default); SSE transports are skipped; codex-only keys (`oauth`,
466
+ `enabled_tools`, …) ride along in the registry block and never reach `.claude.json`.
467
+ - **Kill switches**: `CLAUDE_MULTIACC_MCP=0` / `CODEX_MULTIACC_MCP=0` stop the reconcile at
468
+ seed and launch and the shim's mirror; `claude-accounts mcp …`, sync and the fan-out still
469
+ work.
470
+
471
+ app-robot tasks are different: the panel launches Claude with `--strict-mcp-config` and a
472
+ merged MCP document (`agent_mcp.servers_for`) and Codex with the same document as `-c`
473
+ overrides, so a server every task must see is added there — never with `claude mcp add` on a
474
+ Mac.
475
+
398
476
  ## Codex support (OpenAI Codex CLI)
399
477
 
400
478
  Both pools use the same selection policy. See [Codex account commands and behavior](docs/CODEX.md).
package/bin/claude CHANGED
@@ -71,11 +71,117 @@ REAL="$(find_real)" || {
71
71
  exit 127
72
72
  }
73
73
 
74
+ if [ "$(uname -s)" = "Darwin" ]; then
75
+ file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
76
+ file_sig() { stat -L -f '%m:%z' "$1" 2>/dev/null || echo '?'; }
77
+ else
78
+ file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
79
+ file_sig() { stat -L -c '%Y:%s' "$1" 2>/dev/null || echo '?'; }
80
+ fi
81
+
82
+ # ---- MCP servers for every account (lib/mcp_registry.py) -------------------------
83
+ # Claude Code keeps MCP servers in the CONFIG DIR (`<dir>/.claude.json`), so under the
84
+ # pool there is one MCP configuration per account — and a stock `claude mcp add` lands
85
+ # in ONE random one (2026-09-22: appinspire was connected in some sessions and not in
86
+ # others, depending on the account drawn). The pool's registry (mcp-servers.json, synced
87
+ # fleet-wide) plus a machine-local overlay (mcp-servers.local.json) are reconciled into
88
+ # the picked account right before exec, so a session gets the same servers whichever
89
+ # account it drew. Bash only on the hot path: python runs when the registry, the overlay
90
+ # or the account's own config changed since the last verified reconcile — the stamp in
91
+ # <dir>/.mcp-applied — and never otherwise. Fail-open throughout: a missing python, a
92
+ # missing lib or a broken registry costs nothing but the servers themselves.
93
+ MCP_PY="$SELF_DIR/../lib/mcp_registry.py"
94
+ MCP_PYBIN="${CLAUDE_MULTIACC_PYTHON:-python3}"
95
+ mcp_sig() { # $1 acct dir -> "m:z,m:z,m:z" (registry, overlay, account config; - = absent)
96
+ local f out="" s
97
+ for f in "$ACC_ROOT/mcp-servers.json" "$ACC_ROOT/mcp-servers.local.json" "$1/.claude.json"; do
98
+ if [ -f "$f" ]; then s="$(file_sig "$f")"; else s="-"; fi
99
+ out="$out${out:+,}$s"
100
+ done
101
+ printf '%s\n' "$out"
102
+ }
103
+ mcp_ensure() { # $1 acct dir — silent, never changes the exit status
104
+ [ "${CLAUDE_MULTIACC_MCP:-1}" = "0" ] && return 0
105
+ [ -f "$ACC_ROOT/mcp-servers.json" ] || [ -f "$ACC_ROOT/mcp-servers.local.json" ] || return 0
106
+ [ -f "$MCP_PY" ] || return 0
107
+ local stamp=""
108
+ [ -f "$1/.mcp-applied" ] && IFS= read -r stamp < "$1/.mcp-applied" 2>/dev/null
109
+ [ "${stamp:-}" = "$(mcp_sig "$1")" ] && return 0
110
+ "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider claude apply --account-dir "$1" \
111
+ --fail-open --quiet >/dev/null 2>&1 </dev/null || true
112
+ return 0
113
+ }
114
+
115
+ # `claude mcp add|add-json|remove` writes ONE account's .claude.json — the one it ran
116
+ # under — and nobody else's. So the shim runs it as a child instead of exec-ing it, then
117
+ # mirrors what it wrote into the registry and every other account (`learn` diffs the
118
+ # account against a snapshot taken just before). The real CLI's own parser and writer do
119
+ # the work; the shim only copies the outcome. Exits with the real binary's status; a
120
+ # mirror failure is one hint line. Returns 1 when this run is not such a command.
121
+ mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
122
+ local d="$1" snap="" rc verb="${2:-}" sub="${3:-}"
123
+ shift
124
+ # The scoped-limit fallback may have prepended `--model X` (a global option the real
125
+ # CLI accepts in front of any subcommand): look past it.
126
+ if [ "$verb" = "--model" ]; then verb="${3:-}"; sub="${4:-}"; fi
127
+ [ "$verb" = "mcp" ] || return 1
128
+ case "$sub" in add|add-json|remove) ;; *) return 1 ;; esac
129
+ [ "${CLAUDE_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
130
+ mcp_ensure "$d"
131
+ snap="$(mktemp "${TMPDIR:-/tmp}/claude-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
132
+ if [ -n "$snap" ] && ! "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider claude snapshot \
133
+ --account-dir "$d" > "$snap" 2>/dev/null </dev/null; then
134
+ rm -f "$snap" 2>/dev/null; snap=""
135
+ fi
136
+ "$REAL" "$@"
137
+ rc=$?
138
+ if [ "$rc" -eq 0 ] && [ -n "$snap" ]; then
139
+ "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider claude learn --account-dir "$d" \
140
+ --before "$snap" --project "$(pwd -P 2>/dev/null || pwd)" </dev/null \
141
+ || printf 'claude-multiacc: could not mirror the MCP change to the other accounts — run: claude-accounts mcp apply\n' >&2
142
+ elif [ "$rc" -eq 0 ]; then
143
+ printf 'claude-multiacc: could not mirror the MCP change to the other accounts — run: claude-accounts mcp apply\n' >&2
144
+ fi
145
+ [ -n "$snap" ] && rm -f "$snap" 2>/dev/null
146
+ exit "$rc"
147
+ }
148
+
149
+ # A pooled SESSION exports CLAUDE_CONFIG_DIR=<acct> and CLAUDE_SHIM_ACTIVE=1, and every child
150
+ # process inherits both — so a `claude mcp add` run from INSIDE a session (an agent's
151
+ # Bash tool, `npx appinspire-mcp install` run by an agent) arrives at the fast passthrough
152
+ # below with the account already chosen, not at the selection path where the mirror
153
+ # lives. That is the very write the 2026-09-22 outage was made of, in the place MCP
154
+ # servers are most often installed. The account is known, so the same mirror applies:
155
+ # exits inside when it handled the run; anything else stays byte-identical passthrough.
156
+ mcp_nested_mirror() {
157
+ [ -f "$MANIFEST" ] || return 1
158
+ [ "${CLAUDE_MULTIACC_DISABLE:-0}" = "1" ] && return 1
159
+ [ "${CLAUDE_MULTIACC_MCP:-1}" = "0" ] && return 1
160
+ [ -n "${CLAUDE_CONFIG_DIR:-}" ] || return 1
161
+ local verb="${1:-}" cfg="${CLAUDE_CONFIG_DIR%/}" d=""
162
+ if [ "$verb" = "--model" ]; then verb="${3:-}"; fi
163
+ [ "$verb" = "mcp" ] || return 1
164
+ # A DIRECT child of the pool root, by its given spelling or its physical one (an
165
+ # adopted account is a symlink whose target is the operator's ~/.claude).
166
+ case "$cfg" in
167
+ "$ACC_ROOT"/acct-*) case "${cfg#"$ACC_ROOT"/}" in */*) ;; *) d="$cfg" ;; esac ;;
168
+ esac
169
+ if [ -z "$d" ]; then
170
+ local root_c; root_c="$(canon_path "$ACC_ROOT")"
171
+ case "$(canon_path "$cfg")" in
172
+ "$root_c"/acct-*) case "${cfg##*/}" in acct-*) d="$cfg" ;; esac ;;
173
+ esac
174
+ fi
175
+ [ -n "$d" ] && [ -d "$d" ] || return 1
176
+ mcp_mirror_run "$d" "$@"
177
+ }
178
+
74
179
  # Fast passthrough: caller pinned a config dir or token, addon disabled, recursion
75
180
  # guard, or no account data yet. Byte-identical behavior to stock claude.
76
181
  if [ -n "${CLAUDE_CONFIG_DIR:-}" ] || [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ] \
77
182
  || [ "${CLAUDE_MULTIACC_DISABLE:-0}" = "1" ] || [ -n "${CLAUDE_SHIM_ACTIVE:-}" ] \
78
183
  || [ ! -f "$MANIFEST" ]; then
184
+ mcp_nested_mirror "$@" || true
79
185
  exec "$REAL" "$@"
80
186
  fi
81
187
 
@@ -92,12 +198,6 @@ fi
92
198
  # complaining to stderr.
93
199
  case "$CLAUDE_MULTIACC_THRESHOLD" in ''|*[!0-9]*|??????*) CLAUDE_MULTIACC_THRESHOLD=90 ;; esac
94
200
 
95
- if [ "$(uname -s)" = "Darwin" ]; then
96
- file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
97
- else
98
- file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
99
- fi
100
-
101
201
  # ---- macOS Keychain-held logins ----------------------------------------------------
102
202
  # Claude Code on macOS moves a config dir's OAuth login into the login Keychain
103
203
  # (service "Claude Code-credentials-<sha256(dir)[:8]>") the first time a GUI-session
@@ -1232,6 +1332,10 @@ if [ -n "${CLAUDE_ACCOUNT:-}" ]; then
1232
1332
  fi
1233
1333
  export CLAUDE_CODE_OAUTH_TOKEN="$tok"
1234
1334
  fi
1335
+ # A pinned account carries every registered MCP server too, and a pinned
1336
+ # `claude mcp add|remove` is mirrored like any other (exits inside).
1337
+ mcp_ensure "$d"
1338
+ mcp_mirror_run "$d" "$@" || true
1235
1339
  sel_capture_session "$d"
1236
1340
  exec "$REAL" "$@"
1237
1341
  fi
@@ -1572,6 +1676,8 @@ fi
1572
1676
  # CLAUDE_ACCOUNT pin deliberately does not: a pin is a caller overriding selection,
1573
1677
  # not a turn in the rotation.
1574
1678
  remember_pick "$pick"
1679
+ # The picked account carries every registered MCP server before the client starts.
1680
+ mcp_ensure "$pick"
1575
1681
 
1576
1682
  # Opportunistic limits refresh: non-blocking, throttled, backgrounded. The windows are
1577
1683
  # deliberately wide (10m, matching the 5m scheduled pass): the usage endpoint rate-limits
@@ -1648,6 +1754,15 @@ fi
1648
1754
 
1649
1755
  export CLAUDE_SHIM_ACTIVE=1
1650
1756
 
1757
+ # A `claude mcp add|add-json|remove` is run as a child under the picked account and
1758
+ # mirrored to every other one (exits inside); anything else falls through to exec.
1759
+ if [ "${1:-}" = "mcp" ] || { [ "${1:-}" = "--model" ] && [ "${3:-}" = "mcp" ]; }; then
1760
+ export CLAUDE_CONFIG_DIR="$pick"
1761
+ tok="$(acct_token "$pick")"
1762
+ [ -n "$tok" ] && export CLAUDE_CODE_OAUTH_TOKEN="$tok"
1763
+ mcp_mirror_run "$pick" "$@" || true
1764
+ fi
1765
+
1651
1766
  # Auto-retry applies only to -p/--print runs with an alternative account available,
1652
1767
  # and only when stdin is finite (tty, regular file, or char device like /dev/null).
1653
1768
  # A service-spawned pipe that never EOFs must take the plain exec path, or the
@@ -1878,6 +1993,7 @@ while :; do
1878
1993
  # The account that actually serves the work is the one the next run should rotate
1879
1994
  # away from — not the one that bounced.
1880
1995
  remember_pick "$cur"
1996
+ mcp_ensure "$cur"
1881
1997
  rotated=1
1882
1998
  attempt=2
1883
1999
  continue
@@ -14,6 +14,12 @@ while [ -L "$_self" ]; do
14
14
  done
15
15
  BIN_DIR="$(cd "$(dirname "$_self")" && pwd -P)"
16
16
  REPO_DIR="$(dirname "$BIN_DIR")"
17
+ # Pin the provider here, the way codex-accounts pins codex: lib/common.sh defaults to
18
+ # claude only when the variable is UNSET, and codex-accounts exports it — so a
19
+ # claude-accounts started by codex-accounts (the `mcp --provider both` hop) inherited
20
+ # `codex`, became a second codex CLI, hopped to "its sibling" claude-accounts again, and
21
+ # forked itself to death ("fork: Resource temporarily unavailable").
22
+ export MULTIACC_PROVIDER=claude
17
23
  # shellcheck source=lib/common.sh
18
24
  . "$REPO_DIR/lib/common.sh"
19
25
 
@@ -81,10 +87,10 @@ USAGE
81
87
  or runner daemon distributes.
82
88
  claude-accounts verify [--quick] auth matrix; full mode runs `-p "reply OK"` per account
83
89
  claude-accounts limits [--quiet] [--force] [--json]
84
- refresh usage buckets, apply >=90% markers. When a WEEKLY limit is at least
85
- 95% used or exhausted, automatically redeems the account's Claude Code limit
86
- reset (/limit-reset) with an idempotent request — never for a five-hour
87
- window alone, and not while the week refills on its own within 24h.
90
+ refresh usage buckets, apply >=90% markers. When the pool has parked an
91
+ account on a window its Claude Code limit reset (/limit-reset) refills,
92
+ redeems it with an idempotent request — unless every park lifts on its own
93
+ within the hour.
88
94
  Auto-refreshes long-expired
89
95
  OAuth access tokens via the refresh-token grant (rotated credential is
90
96
  persisted), so idle accounts keep fresh telemetry and stay selectable.
@@ -93,6 +99,30 @@ USAGE
93
99
  claude-accounts health limits + full verify + shim-on-PATH probe; logs to health.log
94
100
  claude-accounts self-update update this npm/git install; logs to update.log
95
101
  claude-accounts post-sync (server side) seed dirs, fix perms, quick verify
102
+ claude-accounts mcp add <name> [--provider claude|codex|both] [--scope user|project]
103
+ [--project PATH] [-e KEY=VAL]... [-H 'Header: v']...
104
+ [--transport stdio|http] -- <command> [args...] (or a URL)
105
+ register an MCP server for EVERY account: writes the pool's mcp-servers.json
106
+ registry, reconciles every account dir, syncs the registry to the server and
107
+ peers (their post-sync applies it). Default --provider both: the same server is
108
+ registered in the codex pool too (MCP servers are not provider-specific).
109
+ A stock `claude mcp add|add-json|remove` run under the shim is mirrored into
110
+ the registry the same way — it no longer lands in one random account.
111
+ -e takes ONE variable per flag (KEY=VAL; write $HOME/..., not ~/..., in a value).
112
+ Exit 3 = registry saved, but an account could not be reconciled (it is named on
113
+ stderr): the change still syncs; fix the account and run `mcp apply`.
114
+ On a replica pool the change lands in the machine-local overlay instead of the
115
+ synced registry — make registry changes on the source machine.
116
+ claude-accounts mcp add-json <name> '<json>' [--provider ...] [--scope ...]
117
+ claude-accounts mcp remove <name> [--provider ...] [--user-only] [--scope project --project PATH]
118
+ unregister AND retire EVERYWHERE: every account (fleet-wide after sync) drops the
119
+ server from user scope and from every project entry, even one that was hand-added
120
+ to a single account. --user-only spares project-local copies.
121
+ claude-accounts mcp list [--json] effective registry (+ machine-local overlay)
122
+ claude-accounts mcp apply [--account-dir DIR]... [--all]
123
+ re-apply the registry now (repair drift)
124
+ claude-accounts mcp import-local --owner NAME (stdin JSON) machine-local overlay for
125
+ a runner daemon; applied here, never synced
96
126
 
97
127
  ENV
98
128
  CLAUDE_ACCOUNTS_ROOT pool root, overriding ~/.claude-accounts — one isolated pool
@@ -105,8 +135,9 @@ ENV
105
135
  CLAUDE_SHIM_RETRY=0 disable -p auto-retry
106
136
  CLAUDE_MULTIACC_DISABLE=1 bypass the shim entirely
107
137
  CLAUDE_MULTIACC_AUTO_RESET=0 disable automatic limit-reset redemption
108
- CLAUDE_MULTIACC_RESET_MIN_HORIZON seconds before the natural weekly reset
109
- inside which a reset is held (default 86400)
138
+ CLAUDE_MULTIACC_MCP=0 disable the MCP registry (no reconcile at seed/launch, no mirror)
139
+ CLAUDE_MULTIACC_RESET_MIN_HORIZON a reset is held while every park lifts on
140
+ its own within this many seconds (default 3600)
110
141
  EOF
111
142
  }
112
143
 
@@ -1541,6 +1572,10 @@ limits_distribute_now() {
1541
1572
  # never grants any. Erring toward exclusion is the safe direction here.
1542
1573
  [ -f "$d/.limited" ] && printf '%s/.limited\n' "$id" >> "$list"
1543
1574
  done
1575
+ # The MCP registry rides along: a server the shim mirrored from a stock `claude mcp
1576
+ # add` reaches every peer within one limits cadence, with no explicit sync. The
1577
+ # machine-local overlay stays home (see sync_push_target).
1578
+ [ -f "$ACC_ROOT/mcp-servers.json" ] && printf 'mcp-servers.json\n' >> "$list"
1544
1579
  if [ ! -s "$list" ]; then rm -f "$list"; return 0; fi
1545
1580
 
1546
1581
  local pushed=0 failed=0
@@ -2408,7 +2443,7 @@ for acct in manifest.get('accounts', []):
2408
2443
  if source == 'oauth':
2409
2444
  try:
2410
2445
  reset_result, reset_view = claude_reset.refresh_reset_credits(
2411
- d, aid, data, url, headers, say, int(now))
2446
+ d, aid, data, url, headers, say, int(now), threshold)
2412
2447
  except Exception as e:
2413
2448
  say(f'{aid}: limit reset automation failed unexpectedly ({type(e).__name__}); '
2414
2449
  f'failing open')
@@ -2893,6 +2928,14 @@ sync_push_target() {
2893
2928
  || fail "cannot reach $server"
2894
2929
  rsync -az "$MANIFEST" "$server:$sroot/accounts.json" >>"$ACC_ROOT/sync.log" 2>&1 \
2895
2930
  || fail "manifest push to $server failed"
2931
+ # The pool's MCP registry rides with the manifest; the target's post-sync reconciles it
2932
+ # into every account dir there. A missing local registry leaves the target's alone,
2933
+ # and the machine-local overlay (mcp-servers.local.json, a runner daemon's own view
2934
+ # of THIS Mac) never travels.
2935
+ if [ -f "$ACC_ROOT/mcp-servers.json" ]; then
2936
+ rsync -az "$ACC_ROOT/mcp-servers.json" "$server:$sroot/mcp-servers.json" >>"$ACC_ROOT/sync.log" 2>&1 \
2937
+ || fail "MCP registry push to $server failed"
2938
+ fi
2896
2939
 
2897
2940
  local id d
2898
2941
  for id in $(account_ids); do
@@ -3079,6 +3122,97 @@ EOF
3079
3122
  fi
3080
3123
  }
3081
3124
 
3125
+ # MCP servers for EVERY account (lib/mcp_registry.py). `add`/`add-json`/`remove` edit
3126
+ # this pool's registry under the mutate lock, reconcile every account, auto-sync, and
3127
+ # by default (--provider both) make the same change in the sibling provider's pool —
3128
+ # an MCP server is not provider-specific, and the operator wants it in both CLIs.
3129
+ # `list`/`apply`/`import-local` act on this pool alone; the overlay `import-local`
3130
+ # writes is machine-local by definition, so it is applied here and never synced.
3131
+ mcp_sibling_root() { # the other provider's pool root, by the same env precedence
3132
+ if [ "$MULTIACC_PROVIDER" = "codex" ]; then
3133
+ printf '%s\n' "${CLAUDE_ACCOUNTS_ROOT:-${CLAUDE_ACCOUNTS_DIR:-$HOME/.claude-accounts}}"
3134
+ else
3135
+ printf '%s\n' "${CODEX_ACCOUNTS_ROOT:-${CODEX_ACCOUNTS_DIR:-$HOME/.codex-accounts}}"
3136
+ fi
3137
+ }
3138
+ cmd_mcp() {
3139
+ require_manifest
3140
+ local sub="${1:-}" provider=both before_dd=1 a rc=0 sibling sibling_root
3141
+ local args=()
3142
+ case "$sub" in
3143
+ add|add-json|remove|list|apply|import-local) shift ;;
3144
+ "") usage; return 1 ;;
3145
+ *) die "unknown mcp subcommand: $sub (add, add-json, remove, list, apply, import-local)" ;;
3146
+ esac
3147
+ [ -f "$LIB_DIR/mcp_registry.py" ] || die "lib/mcp_registry.py is missing from this install"
3148
+ case "$sub" in
3149
+ list|apply|import-local)
3150
+ "$PYBIN" "$LIB_DIR/mcp_registry.py" --root "$ACC_ROOT" --provider "$MULTIACC_PROVIDER" "$sub" "$@"
3151
+ return $? ;;
3152
+ esac
3153
+ # --provider is OUR option: strip it, but only before a literal `--` — after it every
3154
+ # word belongs to the MCP server's own command line and is passed through untouched.
3155
+ while [ $# -gt 0 ]; do
3156
+ a="$1"
3157
+ if [ "$before_dd" = 1 ]; then
3158
+ case "$a" in
3159
+ --) before_dd=0 ;;
3160
+ --provider)
3161
+ [ $# -ge 2 ] || die "--provider needs a value: claude, codex or both"
3162
+ provider="$2"; shift 2; continue ;;
3163
+ --provider=*) provider="${a#--provider=}"; shift; continue ;;
3164
+ esac
3165
+ fi
3166
+ args+=("$a"); shift
3167
+ done
3168
+ case "$provider" in
3169
+ claude|codex|both) ;;
3170
+ *) die "--provider must be claude, codex or both (got '$provider')" ;;
3171
+ esac
3172
+ if [ "$provider" = "$MULTIACC_PROVIDER" ] || [ "$provider" = both ]; then
3173
+ mutate_lock || die "could not acquire the account lock — try again"
3174
+ trap mutate_unlock EXIT
3175
+ "$PYBIN" "$LIB_DIR/mcp_registry.py" --root "$ACC_ROOT" --provider "$MULTIACC_PROVIDER" "$sub" \
3176
+ ${args[@]+"${args[@]}"}
3177
+ rc=$?
3178
+ mutate_unlock
3179
+ trap - EXIT
3180
+ # 3 = the registry WAS saved and some account could not be reconciled (a corrupt
3181
+ # config, a layout the TOML editor refuses): the change still has to reach the
3182
+ # server, the peers and the sibling pool, and `mcp apply` repairs the account once
3183
+ # it is fixed. 1/2 = nothing was saved: stop here.
3184
+ case "$rc" in
3185
+ 0) ;;
3186
+ 3) printf '%s: registry saved; some accounts were not reconciled — run: %s mcp apply\n' \
3187
+ "$PROVIDER_CLI" "$PROVIDER_CLI" >&2 ;;
3188
+ *) return "$rc" ;;
3189
+ esac
3190
+ auto_sync
3191
+ fi
3192
+ if [ "$provider" != "$MULTIACC_PROVIDER" ]; then
3193
+ if [ "$MULTIACC_PROVIDER" = "codex" ]; then sibling=claude; else sibling=codex; fi
3194
+ sibling_root="$(mcp_sibling_root)"
3195
+ # With the DEFAULT (both), a machine without the sibling pool or CLI has nothing to
3196
+ # mirror, and silently so — a codex-only Mac must not warn on every claude
3197
+ # change. An EXPLICIT --provider codex asked for that pool by name: refuse loudly.
3198
+ if [ ! -f "$sibling_root/accounts.json" ]; then
3199
+ [ "$provider" = both ] && return "$rc"
3200
+ die "no $sibling pool on this machine ($sibling_root/accounts.json is missing)"
3201
+ fi
3202
+ if [ ! -x "$BIN_DIR/$sibling-accounts" ]; then
3203
+ [ "$provider" = both ] && return "$rc"
3204
+ die "$sibling-accounts is not installed beside $PROVIDER_CLI"
3205
+ fi
3206
+ # The caller's no-sync request travels with the hop, in the sibling's spelling.
3207
+ if [ "${CLAUDE_MULTIACC_NO_SYNC:-0}" = "1" ]; then export CODEX_MULTIACC_NO_SYNC=1; fi
3208
+ local sib_rc=0
3209
+ MULTIACC_PROVIDER="$sibling" "$BIN_DIR/$sibling-accounts" mcp "$sub" --provider "$sibling" \
3210
+ ${args[@]+"${args[@]}"} || sib_rc=$?
3211
+ [ "$rc" -eq 0 ] && rc="$sib_rc"
3212
+ fi
3213
+ return "$rc"
3214
+ }
3215
+
3082
3216
  cmd_post_sync() {
3083
3217
  require_manifest
3084
3218
  local_pool_fixup
@@ -3194,11 +3328,13 @@ $shim_out"
3194
3328
  # distribution for as long as the arg loops rejected the flag. An UNKNOWN verb must still
3195
3329
  # FAIL, or the probe stops meaning what it says — so this list must hold exactly the verbs
3196
3330
  # the dispatcher below implements, and a test pins that both ways.
3197
- _KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove mint login expired relogin re-login sync verify limits post-sync health self-update"
3331
+ _KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove mint login expired relogin re-login sync verify limits post-sync health self-update mcp"
3198
3332
  case " $_KNOWN_VERBS " in
3199
3333
  *" ${1:-help} "*)
3334
+ # ...but never past a literal `--`: from there on the words belong to an MCP
3335
+ # server's own command line (`mcp add … -- node s.js -h 127.0.0.1`).
3200
3336
  for _arg in "$@"; do
3201
- case "$_arg" in --help|-h) usage; exit 0 ;; esac
3337
+ case "$_arg" in --) break ;; --help|-h) usage; exit 0 ;; esac
3202
3338
  done ;;
3203
3339
  esac
3204
3340
 
@@ -3224,6 +3360,7 @@ case "${1:-help}" in
3224
3360
  post-sync) shift; cmd_post_sync "$@" ;;
3225
3361
  health) shift; cmd_health "$@" ;;
3226
3362
  self-update) shift; cmd_self_update "$@" ;;
3363
+ mcp) shift; cmd_mcp "$@" ;;
3227
3364
  help|--help|-h) usage ;;
3228
3365
  *) usage; exit 1 ;;
3229
3366
  esac