diff --git a/.gitignore b/.gitignore index d06e7e2..76aff29 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,10 @@ /config/dot/espanso/match/identity.yml # Ignore backups of old config files /config/old +# MCP bearer tokens. The server list in user/agents/mcp/servers is tracked +# and names which variable carries each token; the tokens themselves are +# machine-local, because this repo is public. +/user/agents/mcp/env # Ignore Wireguard config of course! /config/wg/** # Ignore Neovim lazy-lock file diff --git a/install b/install index 9093a44..7e05903 100755 --- a/install +++ b/install @@ -343,7 +343,7 @@ fi if [[ "$PANAMA_ROLE" == server ]]; then STAGES=(install-packages link-dotfiles link-user setup-server link-server setup-identity) else - STAGES=(install-packages link-dotfiles link-skills link-user change-settings link-vicinae-scripts setup-identity install-hardware) + STAGES=(install-packages link-dotfiles link-skills link-user link-mcp change-settings link-vicinae-scripts setup-identity install-hardware) fi # The two an upgrade drops. Both exist only to act on interview answers, and diff --git a/setup/scripts/link-mcp b/setup/scripts/link-mcp new file mode 100755 index 0000000..dfc1da6 --- /dev/null +++ b/setup/scripts/link-mcp @@ -0,0 +1,99 @@ +#!/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." diff --git a/user/agents/mcp/servers b/user/agents/mcp/servers new file mode 100644 index 0000000..65f624d --- /dev/null +++ b/user/agents/mcp/servers @@ -0,0 +1,21 @@ +# MCP servers registered with the local agent runtimes. +# +# +# +# The token variable names an entry in user/agents/mcp/env, which is ignored by +# git. This file carries no secret, so it is tracked like the rest of user/ and +# a new machine gets the same servers by dropping its own env file beside it. +# +# A row with no token variable registers without an Authorization header. +# +# Lines beginning with # are comments and blank lines are ignored. + +# The NanoKVM boards. These give an agent full console control of each machine, +# including the parts SSH cannot reach: firmware, a locked screen, and the GUI +# prompts Xcode and the keychain raise during a signed iOS build. +nanokvm-fedora http https://nano.gbrown.org/api/mcp NANOKVM_FEDORA_TOKEN +nanokvm-mac http https://macnano.gbrown.org/api/mcp NANOKVM_MAC_TOKEN + +# Bill Tracker's hosted MCP server. No token: it is an OAuth server of its own +# and Claude Code signs in through Gib's Auth on first use. +bill-tracker http https://mcp.billtracker.gbrown.org/mcp