#!/usr/bin/env bash # MCP servers, registered with the agent runtimes on this machine. # # An MCP server is a URL plus a bearer token, and the token is the whole reason # this is a stage rather than a manifest line. Panama is a public repository, so # the tokens cannot live in it, and neither runtime keeps its server list in a # file that could be symlinked anyway: Codex writes them into config.toml beside # dozens of unrelated settings, and Claude Code into ~/.claude.json. There is no # file to point at, so this registers them through the runtime's own CLI. # # What is tracked is user/agents/mcp/servers, which names each server and which # variable carries its token. What is not tracked is user/agents/mcp/env, which # holds the tokens. A new machine gets the servers by dropping its own env file # beside the tracked one and re-running ./install. # # This is personal content, so it obeys the same interview decision link-user # does. A machine that never said yes registers nothing. # # Only Claude Code is handled. Codex stores its servers inside config.toml, and # rewriting a section of somebody's live TOML is a worse failure mode than # leaving two lines for them to paste once. set -euo pipefail log() { echo -e "\033[1;34m[INFO]\033[0m $*"; } warn() { echo -e "\033[1;33m[WARN]\033[0m $*" >&2; } PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}" MCP_DIR="$PANAMA_PATH/user/agents/mcp" SERVERS="$MCP_DIR/servers" ENV_FILE="$MCP_DIR/env" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama" DECISION="$STATE_DIR/user-content" [[ -r "$SERVERS" ]] || { log "No MCP server list; nothing to register."; exit 0; } # The same gate link-user uses, read the same way, so one answer governs all # personal content rather than two stages disagreeing about it. decision="$([[ -r "$DECISION" ]] && cat "$DECISION" || printf 'no')" if [[ "$decision" != "yes" ]]; then log "Personal content is not enabled on this machine; no MCP servers registered." exit 0 fi if ! command -v claude >/dev/null 2>&1; then log "Claude Code is not installed; nothing to register." exit 0 fi # Tokens are optional. A machine without the env file still registers any server # that needs no header, and says which ones it skipped rather than failing. if [[ -r "$ENV_FILE" ]]; then set -a # shellcheck source=/dev/null . "$ENV_FILE" set +a else warn "No $ENV_FILE; servers needing a token will be skipped." fi registered=0 skipped=0 while read -r name transport url token_var _rest; do case "${name:-}" in ''|'#'*) continue ;; esac if [[ -z "${transport:-}" || -z "${url:-}" ]]; then warn "Ignoring malformed row for '$name'." continue fi header=() if [[ -n "${token_var:-}" ]]; then token="${!token_var:-}" if [[ -z "$token" ]]; then warn "Skipping $name: $token_var is not set in $ENV_FILE." skipped=$((skipped + 1)) continue fi header=(-H "Authorization: $token") fi # Re-registering is how this stays idempotent across upgrades, and how a # rotated token reaches the runtime. Removing first avoids the CLI refusing # a name it already knows. Neither call may print the token, so both are # quiet unless they fail. claude mcp remove "$name" -s user >/dev/null 2>&1 || true if claude mcp add --transport "$transport" "$name" "$url" "${header[@]}" \ -s user >/dev/null 2>&1; then log "Registered $name." registered=$((registered + 1)) else warn "Could not register $name." skipped=$((skipped + 1)) fi done <"$SERVERS" log "MCP servers: $registered registered, $skipped skipped." log "Claude Code loads them at start, so restart a running session to pick them up."