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/bin/codex CHANGED
@@ -73,11 +73,112 @@ REAL="$(find_real)" || {
73
73
  exit 127
74
74
  }
75
75
 
76
+ if [ "$(uname -s)" = "Darwin" ]; then
77
+ file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
78
+ file_sig() { stat -L -f '%m:%z' "$1" 2>/dev/null || echo '?'; }
79
+ else
80
+ file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
81
+ file_sig() { stat -L -c '%Y:%s' "$1" 2>/dev/null || echo '?'; }
82
+ fi
83
+
84
+ # ---- MCP servers for every account (lib/mcp_registry.py) -------------------------
85
+ # Codex keeps MCP servers in CODEX_HOME (`<dir>/config.toml`), so under the pool there
86
+ # is one MCP configuration per account — and a stock `codex mcp add` lands in ONE random
87
+ # one. The pool's registry (mcp-servers.json, synced) plus a machine-local overlay
88
+ # (mcp-servers.local.json) are reconciled into the picked account right before exec, so
89
+ # a session gets the same servers whichever account it drew. Bash only on the hot path:
90
+ # python runs when the registry, the overlay or the account's own config changed since
91
+ # the last verified reconcile — the stamp in <dir>/.mcp-applied — and never otherwise.
92
+ # Fail-open throughout: a missing python, lib or a broken registry costs nothing but
93
+ # the servers themselves. Same contract as bin/claude's mcp_ensure.
94
+ MCP_PY="$SELF_DIR/../lib/mcp_registry.py"
95
+ MCP_PYBIN="${CLAUDE_MULTIACC_PYTHON:-python3}"
96
+ mcp_sig() { # $1 acct dir -> "m:z,m:z,m:z" (registry, overlay, account config; - = absent)
97
+ local f out="" s
98
+ for f in "$ACC_ROOT/mcp-servers.json" "$ACC_ROOT/mcp-servers.local.json" "$1/config.toml"; do
99
+ if [ -f "$f" ]; then s="$(file_sig "$f")"; else s="-"; fi
100
+ out="$out${out:+,}$s"
101
+ done
102
+ printf '%s\n' "$out"
103
+ }
104
+ mcp_ensure() { # $1 acct dir — silent, never changes the exit status
105
+ [ "${CODEX_MULTIACC_MCP:-1}" = "0" ] && return 0
106
+ [ -f "$ACC_ROOT/mcp-servers.json" ] || [ -f "$ACC_ROOT/mcp-servers.local.json" ] || return 0
107
+ [ -f "$MCP_PY" ] || return 0
108
+ local stamp=""
109
+ [ -f "$1/.mcp-applied" ] && IFS= read -r stamp < "$1/.mcp-applied" 2>/dev/null
110
+ [ "${stamp:-}" = "$(mcp_sig "$1")" ] && return 0
111
+ "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex apply --account-dir "$1" \
112
+ --fail-open --quiet >/dev/null 2>&1 </dev/null || true
113
+ return 0
114
+ }
115
+
116
+ # `codex mcp add|remove` writes ONE account's config.toml — the one it ran under — and
117
+ # nobody else's. So the shim runs it as a child instead of exec-ing it, then mirrors what
118
+ # it wrote into the registry and every other account (`learn` diffs the account against a
119
+ # snapshot taken just before). The real CLI's own parser and writer do the work; the shim
120
+ # only copies the outcome. Exits with the real binary's status; a mirror failure is one
121
+ # hint line. Returns 1 when this run is not such a command.
122
+ mcp_mirror_run() { # $1 acct dir, then the user's argv — exits when it handled the run
123
+ local d="$1" snap="" rc
124
+ shift
125
+ [ "${1:-}" = "mcp" ] || return 1
126
+ case "${2:-}" in add|remove) ;; *) return 1 ;; esac
127
+ [ "${CODEX_MULTIACC_MCP:-1}" != "0" ] && [ -f "$MCP_PY" ] || return 1
128
+ mcp_ensure "$d"
129
+ snap="$(mktemp "${TMPDIR:-/tmp}/codex-mcp-snap.XXXXXX" 2>/dev/null)" || snap=""
130
+ if [ -n "$snap" ] && ! "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex snapshot \
131
+ --account-dir "$d" > "$snap" 2>/dev/null </dev/null; then
132
+ rm -f "$snap" 2>/dev/null; snap=""
133
+ fi
134
+ "$REAL" "$@"
135
+ rc=$?
136
+ if [ "$rc" -eq 0 ] && [ -n "$snap" ]; then
137
+ "$MCP_PYBIN" "$MCP_PY" --root "$ACC_ROOT" --provider codex learn --account-dir "$d" \
138
+ --before "$snap" </dev/null \
139
+ || printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
140
+ elif [ "$rc" -eq 0 ]; then
141
+ printf 'codex-multiacc: could not mirror the MCP change to the other accounts — run: codex-accounts mcp apply\n' >&2
142
+ fi
143
+ [ -n "$snap" ] && rm -f "$snap" 2>/dev/null
144
+ exit "$rc"
145
+ }
146
+
147
+ # A pooled SESSION exports CODEX_HOME=<acct> and CODEX_SHIM_ACTIVE=1, and every child
148
+ # process inherits both — so a `codex mcp add` run from INSIDE a session (an agent's
149
+ # Bash tool, `npx appinspire-mcp install` run by an agent) arrives at the fast passthrough
150
+ # below with the account already chosen, not at the selection path where the mirror
151
+ # lives. That is the very write the 2026-09-22 outage was made of, in the place MCP
152
+ # servers are most often installed. The account is known, so the same mirror applies:
153
+ # exits inside when it handled the run; anything else stays byte-identical passthrough.
154
+ mcp_nested_mirror() {
155
+ [ -f "$MANIFEST" ] || return 1
156
+ [ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] && return 1
157
+ [ "${CODEX_MULTIACC_MCP:-1}" = "0" ] && return 1
158
+ [ -n "${CODEX_HOME:-}" ] || return 1
159
+ local verb="${1:-}" cfg="${CODEX_HOME%/}" d=""
160
+ [ "$verb" = "mcp" ] || return 1
161
+ # A DIRECT child of the pool root, by its given spelling or its physical one (an
162
+ # adopted account is a symlink whose target is the operator's ~/.claude).
163
+ case "$cfg" in
164
+ "$ACC_ROOT"/acct-*) case "${cfg#"$ACC_ROOT"/}" in */*) ;; *) d="$cfg" ;; esac ;;
165
+ esac
166
+ if [ -z "$d" ]; then
167
+ local root_c; root_c="$(canon_path "$ACC_ROOT")"
168
+ case "$(canon_path "$cfg")" in
169
+ "$root_c"/acct-*) case "${cfg##*/}" in acct-*) d="$cfg" ;; esac ;;
170
+ esac
171
+ fi
172
+ [ -n "$d" ] && [ -d "$d" ] || return 1
173
+ mcp_mirror_run "$d" "$@"
174
+ }
175
+
76
176
  # Fast passthrough: caller pinned a config dir, addon disabled, recursion guard, or
77
177
  # no account data yet. Byte-identical behavior to stock codex.
78
178
  if [ -n "${CODEX_HOME:-}" ] \
79
179
  || [ "${CODEX_MULTIACC_DISABLE:-0}" = "1" ] || [ -n "${CODEX_SHIM_ACTIVE:-}" ] \
80
180
  || [ ! -f "$MANIFEST" ]; then
181
+ mcp_nested_mirror "$@" || true
81
182
  exec "$REAL" "$@"
82
183
  fi
83
184
 
@@ -94,13 +195,6 @@ fi
94
195
  # complaining to stderr.
95
196
  case "$CODEX_MULTIACC_THRESHOLD" in ''|*[!0-9]*|??????*) CODEX_MULTIACC_THRESHOLD=90 ;; esac
96
197
 
97
- if [ "$(uname -s)" = "Darwin" ]; then
98
- file_mtime() { stat -f %m "$1" 2>/dev/null || echo 0; }
99
- else
100
- file_mtime() { stat -c %Y "$1" 2>/dev/null || echo 0; }
101
- fi
102
-
103
-
104
198
  # A number this shim will do ARITHMETIC on: digits only, and short enough that bash
105
199
  # cannot go out of range. An over-range value makes `[ x -lt y ]` print
106
200
  # "integer expression expected" on stderr — which a service-spawned run must never see —
@@ -645,6 +739,10 @@ if [ -n "${CODEX_ACCOUNT:-}" ]; then
645
739
  share_state_index "$d"
646
740
  export CODEX_HOME="$d"
647
741
  export CODEX_SHIM_ACTIVE=1
742
+ # A pinned account carries every registered MCP server too, and a pinned
743
+ # `codex mcp add|remove` is mirrored like any other (exits inside).
744
+ mcp_ensure "$d"
745
+ mcp_mirror_run "$d" "$@" || true
648
746
  exec "$REAL" "$@"
649
747
  fi
650
748
  sel_log "pin-invalid account=$CODEX_ACCOUNT (no such dir; random fallback)"
@@ -874,6 +972,8 @@ pick="$PICK_DIR"
874
972
  # CODEX_ACCOUNT pin deliberately does not: a pin is a caller overriding selection,
875
973
  # not a turn in the rotation.
876
974
  remember_pick "$pick"
975
+ # The picked account carries every registered MCP server before the client starts.
976
+ mcp_ensure "$pick"
877
977
 
878
978
  # Opportunistic limits refresh: non-blocking, throttled, backgrounded. The windows are
879
979
  # deliberately wide (10m, matching the 5m scheduled pass): the usage endpoint rate-limits
@@ -902,6 +1002,14 @@ sel_log "$acct weekly=$(fresh_field "$pick" weekly_percent || echo '?')%" \
902
1002
 
903
1003
  export CODEX_SHIM_ACTIVE=1
904
1004
 
1005
+ # A `codex mcp add|remove` is run as a child under the picked account and mirrored to
1006
+ # every other one (exits inside); anything else falls through to exec.
1007
+ if [ "${1:-}" = "mcp" ]; then
1008
+ share_state_index "$pick"
1009
+ export CODEX_HOME="$pick"
1010
+ mcp_mirror_run "$pick" "$@" || true
1011
+ fi
1012
+
905
1013
  # Auto-retry applies only to `codex exec` runs with an alternative account available,
906
1014
  # and only when stdin is finite (tty, regular file, or char device like /dev/null).
907
1015
  # A service-spawned pipe that never EOFs must take the plain exec path, or the
@@ -1033,6 +1141,7 @@ while :; do
1033
1141
  # The account that actually serves the work is the one the next run should rotate
1034
1142
  # away from — not the one that bounced.
1035
1143
  remember_pick "$cur"
1144
+ mcp_ensure "$cur"
1036
1145
  attempt=2
1037
1146
  continue
1038
1147
  fi
@@ -89,8 +89,10 @@ USAGE
89
89
  `codex exec` per account
90
90
  codex-accounts limits [--quiet] [--force] [--json]
91
91
  refresh usage windows from the ChatGPT usage endpoint, apply >=90% markers.
92
- When a fresh bucket is at least 95% used, automatically redeems an earned
93
- usage-limit reset if the account has one, using an idempotent request.
92
+ When the pool has parked the account (a window at/over the threshold, a
93
+ finished limit, or a client-reported limit that lifts more than an hour from
94
+ now), automatically redeems an earned usage-limit reset if the account has
95
+ one, using an idempotent request.
94
96
  Auto-refreshes long-expired access tokens via the OAuth refresh-token grant
95
97
  (rotated credential is persisted), so idle accounts keep fresh telemetry and
96
98
  stay selectable. Skips accounts fetched in the last 45s and honors
@@ -98,6 +100,32 @@ USAGE
98
100
  codex-accounts health limits + full verify; logs to health.log
99
101
  codex-accounts self-update update this npm/git install; logs to update.log
100
102
  codex-accounts post-sync (server side) seed dirs, fix perms, quick verify
103
+ codex-accounts mcp add <name> [--provider claude|codex|both] [--scope user]
104
+ [-e KEY=VAL]... [-H 'Header: v']... [--transport stdio|http]
105
+ -- <command> [args...] (or a URL)
106
+ register an MCP server for EVERY account: writes the pool's mcp-servers.json
107
+ registry ([mcp_servers.<name>] in every account's config.toml), syncs the
108
+ registry to the server and peers (their post-sync applies it). Default
109
+ --provider both: the same server is registered in the claude pool too.
110
+ A stock `codex mcp add|remove` run under the shim is mirrored into the
111
+ registry the same way — it no longer lands in one random account.
112
+ Project scope is claude-only: Codex reads no per-project MCP config.
113
+ -e takes ONE variable per flag (KEY=VAL). Exit 3 = registry saved, but an account
114
+ could not be reconciled (named on stderr): the change still syncs; fix the account
115
+ and run `mcp apply`. On a replica pool the change lands in the machine-local
116
+ overlay instead of the synced registry — make registry changes on the source.
117
+ The codex registry syncs only to codex sync targets; a runner Mac's local-only
118
+ codex pool is fed by app-robot's overlay (`mcp import-local --owner app-robot`).
119
+ codex-accounts mcp add-json <name> '<json>' [--provider ...]
120
+ codex-accounts mcp remove <name> [--provider ...] [--user-only]
121
+ unregister AND retire EVERYWHERE: every account (fleet-wide after sync) drops the
122
+ server, even one that was hand-added to a single account (--user-only spares a
123
+ claude project-local copy in the claude pool).
124
+ codex-accounts mcp list [--json] effective registry (+ machine-local overlay)
125
+ codex-accounts mcp apply [--account-dir DIR]... [--all]
126
+ re-apply the registry now (repair drift)
127
+ codex-accounts mcp import-local --owner NAME (stdin JSON) machine-local overlay for
128
+ a runner daemon; applied here, never synced
101
129
 
102
130
  ENV
103
131
  CODEX_ACCOUNTS_ROOT pool root, overriding ~/.codex-accounts — one isolated pool
@@ -110,6 +138,7 @@ ENV
110
138
  CODEX_SHIM_RETRY=0 disable the `codex exec` auto-retry
111
139
  CODEX_MULTIACC_DISABLE=1 bypass the shim entirely
112
140
  CODEX_MULTIACC_AUTO_RESET=0 disable automatic usage-reset redemption
141
+ CODEX_MULTIACC_MCP=0 disable the MCP registry (no reconcile at seed/launch, no mirror)
113
142
  EOF
114
143
  }
115
144
 
@@ -1620,7 +1649,8 @@ for acct in manifest.get('accounts', []):
1620
1649
  if sessionp is None and live:
1621
1650
  sessionp = 0
1622
1651
  try:
1623
- reset_result, reset_view = refresh_reset_credits(d, aid, maxp, data, url, headers, say, int(now))
1652
+ reset_result, reset_view = refresh_reset_credits(d, aid, maxp, data, url, headers, say,
1653
+ int(now), threshold)
1624
1654
  except Exception as e:
1625
1655
  say(f'{aid}: usage reset automation failed unexpectedly ({type(e).__name__}); failing open')
1626
1656
  reset_result = {'status': 'error'}
@@ -1998,6 +2028,14 @@ sync_push_target() {
1998
2028
  || fail "cannot reach $server"
1999
2029
  rsync -az "$MANIFEST" "$server:$sroot/accounts.json" >>"$ACC_ROOT/sync.log" 2>&1 \
2000
2030
  || fail "manifest push to $server failed"
2031
+ # The pool's MCP registry rides with the manifest; the target's post-sync reconciles it
2032
+ # into every account dir there. A missing local registry leaves the target's alone,
2033
+ # and the machine-local overlay (mcp-servers.local.json, a runner daemon's own view
2034
+ # of THIS Mac) never travels.
2035
+ if [ -f "$ACC_ROOT/mcp-servers.json" ]; then
2036
+ rsync -az "$ACC_ROOT/mcp-servers.json" "$server:$sroot/mcp-servers.json" >>"$ACC_ROOT/sync.log" 2>&1 \
2037
+ || fail "MCP registry push to $server failed"
2038
+ fi
2001
2039
 
2002
2040
  local id d
2003
2041
  for id in $(account_ids); do
@@ -2185,6 +2223,97 @@ EOF
2185
2223
  fi
2186
2224
  }
2187
2225
 
2226
+ # MCP servers for EVERY account (lib/mcp_registry.py) — the codex twin of
2227
+ # claude-accounts' cmd_mcp. `add`/`add-json`/`remove` edit this pool's registry under
2228
+ # the mutate lock, reconcile every account's config.toml, auto-sync, and by default
2229
+ # (--provider both) make the same change in the claude pool. `list`/`apply`/
2230
+ # `import-local` act on this pool alone; the overlay `import-local` writes is
2231
+ # machine-local by definition, so it is applied here and never synced.
2232
+ mcp_sibling_root() { # the other provider's pool root, by the same env precedence
2233
+ if [ "$MULTIACC_PROVIDER" = "codex" ]; then
2234
+ printf '%s\n' "${CLAUDE_ACCOUNTS_ROOT:-${CLAUDE_ACCOUNTS_DIR:-$HOME/.claude-accounts}}"
2235
+ else
2236
+ printf '%s\n' "${CODEX_ACCOUNTS_ROOT:-${CODEX_ACCOUNTS_DIR:-$HOME/.codex-accounts}}"
2237
+ fi
2238
+ }
2239
+ cmd_mcp() {
2240
+ require_manifest
2241
+ local sub="${1:-}" provider=both before_dd=1 a rc=0 sibling sibling_root
2242
+ local args=()
2243
+ case "$sub" in
2244
+ add|add-json|remove|list|apply|import-local) shift ;;
2245
+ "") usage; return 1 ;;
2246
+ *) die "unknown mcp subcommand: $sub (add, add-json, remove, list, apply, import-local)" ;;
2247
+ esac
2248
+ [ -f "$LIB_DIR/mcp_registry.py" ] || die "lib/mcp_registry.py is missing from this install"
2249
+ case "$sub" in
2250
+ list|apply|import-local)
2251
+ "$PYBIN" "$LIB_DIR/mcp_registry.py" --root "$ACC_ROOT" --provider "$MULTIACC_PROVIDER" "$sub" "$@"
2252
+ return $? ;;
2253
+ esac
2254
+ # --provider is OUR option: strip it, but only before a literal `--` — after it every
2255
+ # word belongs to the MCP server's own command line and is passed through untouched.
2256
+ while [ $# -gt 0 ]; do
2257
+ a="$1"
2258
+ if [ "$before_dd" = 1 ]; then
2259
+ case "$a" in
2260
+ --) before_dd=0 ;;
2261
+ --provider)
2262
+ [ $# -ge 2 ] || die "--provider needs a value: claude, codex or both"
2263
+ provider="$2"; shift 2; continue ;;
2264
+ --provider=*) provider="${a#--provider=}"; shift; continue ;;
2265
+ esac
2266
+ fi
2267
+ args+=("$a"); shift
2268
+ done
2269
+ case "$provider" in
2270
+ claude|codex|both) ;;
2271
+ *) die "--provider must be claude, codex or both (got '$provider')" ;;
2272
+ esac
2273
+ if [ "$provider" = "$MULTIACC_PROVIDER" ] || [ "$provider" = both ]; then
2274
+ mutate_lock || die "could not acquire the account lock — try again"
2275
+ trap mutate_unlock EXIT
2276
+ "$PYBIN" "$LIB_DIR/mcp_registry.py" --root "$ACC_ROOT" --provider "$MULTIACC_PROVIDER" "$sub" \
2277
+ ${args[@]+"${args[@]}"}
2278
+ rc=$?
2279
+ mutate_unlock
2280
+ trap - EXIT
2281
+ # 3 = the registry WAS saved and some account could not be reconciled (a corrupt
2282
+ # config, a layout the TOML editor refuses): the change still has to reach the
2283
+ # server, the peers and the sibling pool, and `mcp apply` repairs the account once
2284
+ # it is fixed. 1/2 = nothing was saved: stop here.
2285
+ case "$rc" in
2286
+ 0) ;;
2287
+ 3) printf '%s: registry saved; some accounts were not reconciled — run: %s mcp apply\n' \
2288
+ "$PROVIDER_CLI" "$PROVIDER_CLI" >&2 ;;
2289
+ *) return "$rc" ;;
2290
+ esac
2291
+ auto_sync
2292
+ fi
2293
+ if [ "$provider" != "$MULTIACC_PROVIDER" ]; then
2294
+ if [ "$MULTIACC_PROVIDER" = "codex" ]; then sibling=claude; else sibling=codex; fi
2295
+ sibling_root="$(mcp_sibling_root)"
2296
+ # With the DEFAULT (both), a machine without the sibling pool or CLI has nothing to
2297
+ # mirror, and silently so — a claude-only Mac must not warn on every codex
2298
+ # change. An EXPLICIT --provider claude asked for that pool by name: refuse loudly.
2299
+ if [ ! -f "$sibling_root/accounts.json" ]; then
2300
+ [ "$provider" = both ] && return "$rc"
2301
+ die "no $sibling pool on this machine ($sibling_root/accounts.json is missing)"
2302
+ fi
2303
+ if [ ! -x "$BIN_DIR/$sibling-accounts" ]; then
2304
+ [ "$provider" = both ] && return "$rc"
2305
+ die "$sibling-accounts is not installed beside $PROVIDER_CLI"
2306
+ fi
2307
+ # The caller's no-sync request travels with the hop, in the sibling's spelling.
2308
+ if [ "${CODEX_MULTIACC_NO_SYNC:-0}" = "1" ]; then export CLAUDE_MULTIACC_NO_SYNC=1; fi
2309
+ local sib_rc=0
2310
+ MULTIACC_PROVIDER="$sibling" "$BIN_DIR/$sibling-accounts" mcp "$sub" --provider "$sibling" \
2311
+ ${args[@]+"${args[@]}"} || sib_rc=$?
2312
+ [ "$rc" -eq 0 ] && rc="$sib_rc"
2313
+ fi
2314
+ return "$rc"
2315
+ }
2316
+
2188
2317
  cmd_post_sync() {
2189
2318
  require_manifest
2190
2319
  local_pool_fixup
@@ -2303,11 +2432,13 @@ PYEOF
2303
2432
  # distribution for as long as the arg loops rejected the flag. An UNKNOWN verb must still
2304
2433
  # FAIL, or the probe stops meaning what it says — so this list must hold exactly the verbs
2305
2434
  # the dispatcher below implements, and a test pins that both ways.
2306
- _KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool"
2435
+ _KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool mcp"
2307
2436
  case " $_KNOWN_VERBS " in
2308
2437
  *" ${1:-help} "*)
2438
+ # ...but never past a literal `--`: from there on the words belong to an MCP
2439
+ # server's own command line (`mcp add … -- node s.js -h 127.0.0.1`).
2309
2440
  for _arg in "$@"; do
2310
- case "$_arg" in --help|-h) usage; exit 0 ;; esac
2441
+ case "$_arg" in --) break ;; --help|-h) usage; exit 0 ;; esac
2311
2442
  done ;;
2312
2443
  esac
2313
2444
 
@@ -2332,6 +2463,7 @@ case "${1:-help}" in
2332
2463
  health) shift; cmd_health "$@" ;;
2333
2464
  self-update) shift; cmd_self_update "$@" ;;
2334
2465
  init-pool) shift; cmd_init_pool "$@" ;;
2466
+ mcp) shift; cmd_mcp "$@" ;;
2335
2467
  help|--help|-h) usage ;;
2336
2468
  *) usage; exit 1 ;;
2337
2469
  esac
@@ -165,6 +165,40 @@ set, which is what prevents two machines from overwriting each other's manifests
165
165
  Make account changes on the source; sign-ins (`login`/`relogin`) still happen on
166
166
  whichever machine needs the credential.
167
167
 
168
+ ### MCP servers for every account
169
+
170
+ MCP servers live in the config dir, i.e. per account. Never register one with a stock
171
+ `claude mcp add` on a pool Mac and expect it everywhere — under the shim that command is
172
+ mirrored into the registry (see below), but the registry is the source of truth, and it is
173
+ edited on the **source** machine (a replica's own changes land in its machine-local overlay
174
+ and are not pushed anywhere):
175
+
176
+ ```bash
177
+ claude-accounts mcp add appinspire-mcp -- npx -y appinspire-mcp@latest serve
178
+ # writes ~/.claude-accounts/mcp-servers.json AND ~/.codex-accounts/mcp-servers.json,
179
+ # reconciles every account of both pools, auto-syncs the registry to server + peers
180
+ claude-accounts mcp add --provider claude --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
181
+ # a checkout, registered for ONE project (claude only: codex reads no project config);
182
+ # one -e per variable, and $HOME rather than ~ inside a value
183
+ claude-accounts mcp remove adspower-local-api # retire EVERYWHERE: user scope + every project entry, hand-added copies too
184
+ claude-accounts mcp remove foo --user-only # user scope only
185
+ claude-accounts mcp list # effective registry (+ machine-local overlay)
186
+ claude-accounts mcp apply # re-apply to every account now (exit 3 from add/remove = do this after fixing the account it named)
187
+ codex-accounts mcp list # the codex pool's view
188
+ ```
189
+
190
+ How a fleet Mac receives it: `sync` (and every claude limits pass) pushes the claude
191
+ `mcp-servers.json` next to the manifest to the server and every peer; the target's
192
+ `post-sync` re-seeds its account dirs, which applies the registry; and the shim there
193
+ re-applies it to the picked account right before exec whenever the file changed. The codex
194
+ registry travels only to codex sync targets (the server, gas-mini) — runner Macs' codex pools
195
+ are local-only and are fed by app-robot's runner overlay (`codex-accounts mcp import-local
196
+ --owner app-robot`) or a direct `codex-accounts mcp add --provider codex …` on that Mac. A
197
+ runner daemon's overlay (`<provider>-accounts mcp import-local --owner NAME`, JSON on stdin)
198
+ is applied on the Mac and never synced. `--help` on `mcp` lists every option;
199
+ `CLAUDE_MULTIACC_MCP=0` / `CODEX_MULTIACC_MCP=0` stop the reconcile at seed and launch and the
200
+ shim's mirror (the CLI, sync and fan-out keep working).
201
+
168
202
  ### Pointing sync somewhere else (or nowhere)
169
203
 
170
204
  The target is no longer baked in. In precedence order:
package/docs/CODEX.md CHANGED
@@ -44,8 +44,9 @@ Codex-specific notes:
44
44
  own public client id) and the rotated credential is persisted 0600. Overrides for
45
45
  tests: `CODEX_MULTIACC_TOKEN_URL`, `CODEX_MULTIACC_CLIENT_ID`,
46
46
  `CODEX_MULTIACC_USAGE_URL`.
47
- - **Earned usage resets are redeemed automatically at 95% used** (5% remaining) or
48
- when the backend reports the limit finished. The limits refresher checks the reset
47
+ - **Earned usage resets are redeemed automatically for a parked account:** a bucket
48
+ at/over the pool's exclusion threshold (90%), a limit the backend reports finished, or
49
+ an active usage park (a client-reported 429) that lifts more than an hour from now. The limits refresher checks the reset
49
50
  credits belonging to that same account, uses the soonest-expiring available credit,
50
51
  and writes a per-account/window idempotency key before redemption so a lost response
51
52
  or simultaneous fleet poll cannot spend a second reset. Set
@@ -82,6 +83,32 @@ codex-accounts --help # all account commands and options
82
83
  claude-multiacc codex status # same status command via the npm wrapper
83
84
  ```
84
85
 
86
+ ## MCP servers for every account
87
+
88
+ `codex-accounts mcp add|add-json|remove|list|apply|import-local` manages the pool's MCP
89
+ registry (`~/.codex-accounts/mcp-servers.json`); `add`/`remove` default to `--provider both`,
90
+ so the claude pool beside it gets the same change, and `mcp remove` retires the server in
91
+ every account (`--user-only` keeps a claude project-local copy). A stock `codex mcp add|remove`
92
+ run under the shim — or from inside a pooled codex session — is mirrored into the registry and
93
+ every account; on a replica pool the mirror lands in the machine-local overlay instead, so
94
+ make registry changes on the source. The registry holds Claude-style blocks; for codex each
95
+ becomes a `[mcp_servers.<name>]` table in every account's `config.toml`, and unrelated
96
+ tables, their comments and blank lines are left alone: `command`/`args` as is, literal `env`
97
+ values as an inline `env` table, a `KEY=${KEY}` reference as `env_vars = ["KEY"]` (codex
98
+ forwards a variable under its own name only and expands nothing, so `KEY=${OTHER}` and
99
+ `${VAR:-default}` cannot be expressed and are left out with a warning), `headers` on an HTTP
100
+ server as `http_headers` (literal) / `env_http_headers` (references), `Authorization: Bearer
101
+ ${VAR}` as `bearer_token_env_var`, and, for a stdio server, `startup_timeout_sec = 60` unless
102
+ the block sets one (a cold `npx` download must not trip codex's 10 s default). Codex-only keys
103
+ codex itself wrote (`oauth`, `enabled_tools`, …) ride along in the registry block and never
104
+ reach `.claude.json`. SSE transports cannot be expressed and are skipped with a warning;
105
+ project scope is claude-only because Codex 0.156 reads no per-project MCP config. Python
106
+ 3.11+ is found the same way as for `configure`. `CODEX_MULTIACC_MCP=0` stops the reconcile at
107
+ seed and launch and the mirror; the CLI keeps working. The codex registry syncs only to codex
108
+ sync targets — a runner Mac's local-only codex pool gets its servers from app-robot's runner
109
+ overlay (`mcp import-local --owner app-robot`) or a direct `codex-accounts mcp add
110
+ --provider codex …` there.
111
+
85
112
  ## Opt-in subagent concurrency policy
86
113
 
87
114
  The addon does not choose delegation policy by default. To manage the concurrent