#!/bin/sh set -e # --------------------------------------------------------------------------- # Proxy MCP installer # Usage: curl -sL useproxy.dev/install | sh # --------------------------------------------------------------------------- # ---- Dependencies --------------------------------------------------------- missing=0 if ! command -v curl >/dev/null 2>&1; then printf '\033[0;31merror: curl is required. Install it and try again.\033[0m\n' >&2 printf ' macOS: included by default\n' >&2 printf ' Linux: sudo apt install curl\n' >&2 missing=1 fi if ! command -v python3 >/dev/null 2>&1; then printf '\033[0;31merror: python3 is required. Install it and try again.\033[0m\n' >&2 printf ' macOS: xcode-select --install\n' >&2 printf ' Linux: sudo apt install python3\n' >&2 missing=1 fi if ! command -v openssl >/dev/null 2>&1; then printf '\033[0;31merror: openssl is required. Install it and try again.\033[0m\n' >&2 printf ' macOS: included by default\n' >&2 printf ' Linux: sudo apt install openssl\n' >&2 missing=1 fi if [ "$missing" -eq 1 ]; then exit 1 fi # ---- Colors --------------------------------------------------------------- if [ -t 1 ]; then BOLD='\033[1m' DIM='\033[2m' GREEN='\033[0;32m' RED='\033[0;31m' ACCENT='\033[38;2;130;170;140m' # sage — change this to swap the accent colour ITALIC='\033[3m' STRIKE='\033[9m' RESET='\033[0m' HIDE_CURSOR='\033[?25l' SHOW_CURSOR='\033[?25h' else BOLD=''; DIM=''; GREEN=''; RED=''; ACCENT=''; ITALIC=''; STRIKE=''; RESET=''; HIDE_CURSOR=''; SHOW_CURSOR='' fi # ---- Temp file cleanup on exit -------------------------------------------- _TMPFILES="" _mktmp() { _f=$(mktemp) _TMPFILES="$_TMPFILES $_f" printf "%s" "$_f" } _nc_pid="" trap 'stty sane /dev/null; printf "%b" "${SHOW_CURSOR}"; rm -f $_TMPFILES; [ -n "$_nc_pid" ] && kill "$_nc_pid" 2>/dev/null' EXIT INT TERM # ---- Step state ----------------------------------------------------------- step_labels="Authorize|Configure MCP" step_states="pending|pending" set_step_state() { _idx=$1 _new=$2 _i=1 _out="" _IFS_old=$IFS IFS='|' for s in $step_states; do if [ "$_i" -eq "$_idx" ]; then if [ -z "$_out" ]; then _out="$_new"; else _out="$_out|$_new"; fi else if [ -z "$_out" ]; then _out="$s"; else _out="$_out|$s"; fi fi _i=$((_i + 1)) done IFS=$_IFS_old step_states="$_out" } set_step_label() { _idx=$1 _new=$2 _i=1 _out="" _IFS_old=$IFS IFS='|' for l in $step_labels; do if [ "$_i" -eq "$_idx" ]; then if [ -z "$_out" ]; then _out="$_new"; else _out="$_out|$_new"; fi else if [ -z "$_out" ]; then _out="$l"; else _out="$_out|$l"; fi fi _i=$((_i + 1)) done IFS=$_IFS_old step_labels="$_out" } render_steps() { _IFS_old=$IFS IFS='|' # shellcheck disable=SC2086 set -- $step_labels _l1=$1; _l2=$2 # shellcheck disable=SC2086 set -- $step_states _s1=$1; _s2=$2 IFS=$_IFS_old for n in 1 2; do eval "lbl=\$_l$n" eval "st=\$_s$n" case "$st" in pending) printf "%d. %s\033[K\n" "$n" "$lbl" ;; active) printf "${ACCENT}%d.${RESET} %s\033[K\n" "$n" "$lbl" ;; done) printf "${GREEN}\xe2\x9c\x93${RESET} ${DIM}${STRIKE}%s${RESET}\033[K\n" "$lbl" ;; fail) printf "${RED}\xe2\x9c\x97 %s${RESET}\033[K\n" "$lbl" ;; esac done } redraw_steps() { printf "\033[2A\033[0J" render_steps } # ---- Welcome screen ------------------------------------------------------- printf "\n" printf "${ACCENT}┌──────────────────────────────────────────────────────────────────────┐${RESET}\n" printf "${ACCENT}│${RESET} ${ACCENT}✦${RESET} ${BOLD}Proxy${RESET}%-62s${ACCENT}│${RESET}\n" "" printf "${ACCENT}│${RESET} ${DIM}Meta, Google, LinkedIn & TikTok Ads inside your coding agent${RESET}%-8s${ACCENT}│${RESET}\n" "" printf "${ACCENT}└──────────────────────────────────────────────────────────────────────┘${RESET}\n" printf "\n" render_steps # ---- Yes/No prompt -------------------------------------------------------- read_key() { c1=$(dd bs=1 count=1 /dev/null) if [ "$c1" = "$(printf '\033')" ]; then c2=$(dd bs=1 count=1 /dev/null) if [ "$c2" = "[" ]; then c3=$(dd bs=1 count=1 /dev/null) case "$c3" in A) printf "UP" ;; B) printf "DOWN" ;; C) printf "RIGHT" ;; D) printf "LEFT" ;; *) printf "ESC" ;; esac else printf "ESC" fi else case "$c1" in "") printf "ENTER" ;; " ") printf "SPACE" ;; "$(printf '\n')") printf "ENTER" ;; "$(printf '\r')") printf "ENTER" ;; *) printf "%s" "$c1" ;; esac fi } render_selector2() { # $1=selected (0=first,1=second), $2=label1, $3=label2 if [ "$1" -eq 0 ]; then printf "${ACCENT}> %s${RESET}\033[K\n" "$2" printf " %s\033[K\n" "$3" else printf " %s\033[K\n" "$2" printf "${ACCENT}> %s${RESET}\033[K\n" "$3" fi } # Wrap in a loop so "Try again" restarts the prompt _ready_done=0 while [ "$_ready_done" -eq 0 ]; do printf "\n${BOLD}Ready?${RESET}\n" yesno_selected=0 printf "%b" "${HIDE_CURSOR}" render_selector2 "$yesno_selected" "Yes" "No" stty -echo -icanon min 1 time 0 /dev/null || true while :; do key=$(read_key) case "$key" in UP|DOWN|LEFT|RIGHT) yesno_selected=$((1 - yesno_selected)) ;; y|Y) yesno_selected=0 break ;; n|N) yesno_selected=1 break ;; ENTER) break ;; ESC) yesno_selected=1 break ;; esac printf "\033[2A" render_selector2 "$yesno_selected" "Yes" "No" done stty sane /dev/null || true printf "%b" "${SHOW_CURSOR}" if [ "$yesno_selected" -eq 0 ]; then _ready_done=1 else # Clear: blank + Ready? + 2 selector lines = 4 lines printf "\033[4A\033[J" printf "\nBold choice. The ads will miss you.\n" retry_selected=0 printf "%b" "${HIDE_CURSOR}" render_selector2 "$retry_selected" "Try again" "Exit" stty -echo -icanon min 1 time 0 /dev/null || true while :; do key=$(read_key) case "$key" in UP|DOWN|LEFT|RIGHT) retry_selected=$((1 - retry_selected)) ;; ENTER) break ;; ESC) retry_selected=1 break ;; esac printf "\033[2A" render_selector2 "$retry_selected" "Try again" "Exit" done stty sane /dev/null || true printf "%b" "${SHOW_CURSOR}" if [ "$retry_selected" -ne 0 ]; then printf "\033[4A\033[J" exit 0 fi # Try again: clear funny message + selector (blank + message + 2 lines = 4 lines) printf "\033[4A\033[J" fi done # Clear: blank + Ready? + 2 selector lines = 4 lines printf "\033[4A\033[J" # ---- Step 1: Authorize ---------------------------------------------------- set_step_state 1 active redraw_steps # PKCE state=$(openssl rand -hex 16) code_verifier=$(openssl rand -base64 48 | tr -d '=+/\n' | cut -c1-43) code_challenge=$(printf "%s" "$code_verifier" | openssl sha256 -binary | openssl base64 | tr -d '=' | tr '+/' '-_') # Pick a free port — bind to 0 and let the OS assign one _listen_port=$(python3 -c "import socket; s=socket.socket(); s.bind(('localhost',0)); print(s.getsockname()[1]); s.close()") if [ -z "$_listen_port" ]; then _listen_port=8123 fi # Dynamic client registration reg_response=$(curl -sf -X POST https://mcp.useproxy.dev/register \ -H "Content-Type: application/json" \ -d "{\"redirect_uris\":[\"http://localhost:${_listen_port}/callback\"],\"token_endpoint_auth_method\":\"none\"}" 2>/dev/null) || reg_response="" if [ -z "$reg_response" ]; then set_step_state 1 fail redraw_steps printf "\n${RED}Failed to register OAuth client with mcp.useproxy.dev${RESET}\n" exit 1 fi client_id=$(printf "%s" "$reg_response" | python3 -c "import sys,json; print(json.load(sys.stdin)['client_id'])" 2>/dev/null) || client_id="" if [ -z "$client_id" ]; then set_step_state 1 fail redraw_steps printf "\n${RED}Invalid response from OAuth registration endpoint${RESET}\n" exit 1 fi _redir_encoded="http%3A%2F%2Flocalhost%3A${_listen_port}%2Fcallback" auth_url="https://mcp.useproxy.dev/authorize?client_id=${client_id}&redirect_uri=${_redir_encoded}&response_type=code&scope=openid%20email%20profile&state=${state}&code_challenge=${code_challenge}&code_challenge_method=S256" # Start callback listener before opening browser to avoid race condition. _nc_out=$(_mktmp) python3 - "$_nc_out" "$_listen_port" <<'PYEOF' & import http.server, sys, threading out_file = sys.argv[1] port = int(sys.argv[2]) class _H(http.server.BaseHTTPRequestHandler): def do_GET(self): with open(out_file, 'w') as f: f.write("GET " + self.path + " HTTP/1.1\r\n") self.send_response(302) self.send_header("Location", "https://useproxy.dev/authenticated") self.send_header("Connection", "close") self.end_headers() threading.Thread(target=self.server.shutdown, daemon=True).start() def log_message(self, *a): pass srv = http.server.HTTPServer(("localhost", port), _H) srv.timeout = 120 srv.handle_request() PYEOF _nc_pid=$! set_step_label 1 "Sign up / Log in — check your browser" redraw_steps case "$(uname -s)" in Darwin) open "$auth_url" 2>/dev/null ;; Linux) xdg-open "$auth_url" 2>/dev/null ;; esac # Animated waiting message while nc listens for the OAuth callback printf "\n" _spin=0 while kill -0 "$_nc_pid" 2>/dev/null; do case $((_spin % 4)) in 0) _dot="" ;; 1) _dot="." ;; 2) _dot=".." ;; 3) _dot="..." ;; esac printf "\r ${DIM}Sign in to continue%s${RESET}\033[K" "$_dot" _spin=$((_spin + 1)) sleep 1 done # Clear spinner line + blank line, return cursor to right after step list printf "\r\033[K\033[1A\033[K" callback=$(cat "$_nc_out") if [ -z "$callback" ]; then set_step_state 1 fail redraw_steps printf "\n${RED}Sign in timed out. Run the script again to retry.${RESET}\n" exit 1 fi callback_url=$(printf "%s" "$callback" | head -1 | sed 's/GET \(.*\) HTTP.*/\1/') auth_code=$(printf "%s" "$callback_url" | sed 's/.*[?&]code=\([^&]*\).*/\1/') returned_state=$(printf "%s" "$callback_url" | sed 's/.*[?&]state=\([^&]*\).*/\1/') if [ "$returned_state" != "$state" ]; then set_step_state 1 fail redraw_steps printf "\n${RED}State mismatch — possible CSRF. Please try again.${RESET}\n" exit 1 fi if [ -z "$auth_code" ] || [ "$auth_code" = "$callback_url" ]; then set_step_state 1 fail redraw_steps printf "\n${RED}Failed to parse authorization code from callback.${RESET}\n" exit 1 fi # Token exchange token_response=$(curl -sf -X POST https://mcp.useproxy.dev/token \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=authorization_code" \ --data-urlencode "code=${auth_code}" \ --data-urlencode "redirect_uri=http://localhost:${_listen_port}/callback" \ --data-urlencode "client_id=${client_id}" \ --data-urlencode "code_verifier=${code_verifier}" 2>/dev/null) || token_response="" if [ -z "$token_response" ]; then set_step_state 1 fail redraw_steps printf "\n${RED}Token exchange failed.${RESET}\n" exit 1 fi access_token=$(printf "%s" "$token_response" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])" 2>/dev/null) || access_token="" if [ -z "$access_token" ]; then set_step_state 1 fail redraw_steps printf "\n${RED}No access_token in token response.${RESET}\n" exit 1 fi set_step_state 1 done redraw_steps # ---- Create token (silent) ------------------------------------------------ install_response=$(curl -sf -X POST https://mcp.useproxy.dev/install/token \ -H "Authorization: Bearer ${access_token}" 2>/dev/null) || install_response="" if [ -z "$install_response" ]; then printf "\n${RED}Failed to create API token.${RESET}\n" exit 1 fi proxy_token=$(printf "%s" "$install_response" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null) || proxy_token="" if [ -z "$proxy_token" ]; then printf "\n${RED}Failed to create API token.${RESET}\n" printf "%s\n" "$install_response" exit 1 fi set_step_label 2 "Select agents" set_step_state 2 active redraw_steps # ---- Step 2: Agent selector ----------------------------------------------- # Build agents list. Pipe-delimited: name|path|key|format uname_s=$(uname -s) agents="" add_agent() { if [ -z "$agents" ]; then agents="$1|$2|$3|$4" else agents="${agents} $1|$2|$3|$4" fi } HOME_DIR="$HOME" add_agent "Claude Code" "$HOME_DIR/.claude.json" "mcpServers" "http" if [ "$uname_s" = "Darwin" ]; then add_agent "Claude Desktop" "$HOME_DIR/Library/Application Support/Claude/claude_desktop_config.json" "mcpServers" "mcp_remote" fi add_agent "Claude Web (mobile, routines)" "" "" "claude_web" # Cursor: prefer ~/.cursor, fallback ~/.config/cursor if [ -d "$HOME_DIR/.cursor" ] || [ ! -d "$HOME_DIR/.config/cursor" ]; then add_agent "Cursor" "$HOME_DIR/.cursor/mcp.json" "mcpServers" "standard" else add_agent "Cursor" "$HOME_DIR/.config/cursor/mcp.json" "mcpServers" "standard" fi # VS Code if [ "$uname_s" = "Darwin" ]; then add_agent "VS Code" "$HOME_DIR/Library/Application Support/Code/User/mcp.json" "servers" "http" else add_agent "VS Code" "$HOME_DIR/.config/Code/User/mcp.json" "servers" "http" fi # Hidden by default — restore the add_agent line if a user asks for these. # Format-building code for these remains in inject_json_config. # Windsurf: prefer ~/.codeium/windsurf, fallback ~/.config/windsurf # if [ -d "$HOME_DIR/.codeium/windsurf" ] || [ ! -d "$HOME_DIR/.config/windsurf" ]; then # add_agent "Windsurf" "$HOME_DIR/.codeium/windsurf/mcp_config.json" "mcpServers" "standard" # else # add_agent "Windsurf" "$HOME_DIR/.config/windsurf/mcp_config.json" "mcpServers" "standard" # fi add_agent "Codex" "$HOME_DIR/.codex/config.toml" "mcp_servers.proxy" "codex" add_agent "Gemini CLI" "$HOME_DIR/.gemini/settings.json" "mcpServers" "gemini_cli" # add_agent "Kiro" "$HOME_DIR/.kiro/settings/mcp.json" "mcpServers" "standard" # add_agent "Zed" "$HOME_DIR/.config/zed/settings.json" "context_servers" "zed" add_agent "OpenCode" "$HOME_DIR/.config/opencode/opencode.json" "mcp" "opencode" add_agent "Copilot CLI" "$HOME_DIR/.copilot/mcp-config.json" "mcpServers" "http" add_agent "Antigravity" "$HOME_DIR/.gemini/antigravity/mcp_config.json" "mcpServers" "antigravity" add_agent "Your own agent" "" "" "copy_token" # Count agents num_agents=$(printf "%s\n" "$agents" | awk 'NF' | wc -l | tr -d ' ') # Build parallel arrays using temp files (POSIX sh has no arrays) agents_tmp=$(_mktmp) printf "%s\n" "$agents" | awk 'NF' > "$agents_tmp" # Selection state: one line per agent, "0" or "1" selected_tmp=$(_mktmp) i=0 while [ "$i" -lt "$num_agents" ]; do echo "0" >> "$selected_tmp" i=$((i + 1)) done cursor=1 # 1-indexed get_agent_field() { _line=$(sed -n "${1}p" "$agents_tmp") _field=$2 printf "%s" "$_line" | awk -F'|' -v f="$_field" '{print $f}' } get_selected() { sed -n "${1}p" "$selected_tmp" } toggle_selected() { _idx=$1 _cur=$(get_selected "$_idx") if [ "$_cur" = "1" ]; then _new="0"; else _new="1"; fi # Rewrite file _tmp=$(_mktmp) _i=1 while IFS= read -r line; do if [ "$_i" -eq "$_idx" ]; then echo "$_new" >> "$_tmp" else echo "$line" >> "$_tmp" fi _i=$((_i + 1)) done < "$selected_tmp" mv "$_tmp" "$selected_tmp" } count_selected() { _n=$(grep -c '^1$' "$selected_tmp" 2>/dev/null || true) printf '%s\n' "${_n:-0}" } render_agent_list() { _total_rows=$((num_agents + 1)) # agents + hint line _i=1 while [ "$_i" -le "$num_agents" ]; do _name=$(get_agent_field "$_i" 1) _sel=$(get_selected "$_i") if [ "$_sel" = "1" ]; then _marker="${GREEN}\xe2\x97\x8f${RESET}" else _marker="${DIM}\xe2\x97\x8b${RESET}" fi if [ "$_i" -eq "$cursor" ]; then printf " ${ACCENT}\xe2\x80\xba${RESET} %b ${BOLD}%s${RESET}\033[K\n" "$_marker" "$_name" else printf " %b %s\033[K\n" "$_marker" "$_name" fi _i=$((_i + 1)) done _count=$(count_selected) if [ "$_count" -gt 0 ]; then _hint="${DIM}${ITALIC}\xe2\x86\x91\xe2\x86\x93 move \xc2\xb7 space select \xc2\xb7 enter confirm \xc2\xb7 esc cancel${RESET}" printf " %b${DIM} \xe2\x80\x94 %s selected${RESET}\033[K\n" "$_hint" "$_count" else _hint="${DIM}${ITALIC}\xe2\x86\x91\xe2\x86\x93 move \xc2\xb7 space select \xc2\xb7 esc cancel${RESET}" printf " %b\033[K\n" "$_hint" fi } # Print blank line before list so redraw works printf "\n" printf "%b" "${HIDE_CURSOR}" render_agent_list stty -echo -icanon min 1 time 0 /dev/null || true # Drain any input that arrived during the browser auth flow (focus-in sequences, # escape codes from terminal switching). Must run after stty -icanon so the tty # is in raw mode and escape sequences aren't buffered waiting for newlines. # Reads with a 200ms window; exits as soon as no bytes arrive within that window. python3 -c " import os, select, time try: fd = open('/dev/tty', 'rb') deadline = time.time() + 0.2 while True: remaining = deadline - time.time() if remaining <= 0: break r, _, _ = select.select([fd], [], [], remaining) if r: os.read(fd.fileno(), 256) else: break fd.close() except Exception: pass " 2>/dev/null || true _total_rows=$((num_agents + 1)) while :; do key=$(read_key) case "$key" in UP) cursor=$((cursor - 1)) if [ "$cursor" -lt 1 ]; then cursor=$num_agents; fi ;; DOWN) cursor=$((cursor + 1)) if [ "$cursor" -gt "$num_agents" ]; then cursor=1; fi ;; SPACE) toggle_selected "$cursor" ;; ENTER) if [ "$(get_selected "$cursor")" = "0" ]; then toggle_selected "$cursor" fi _count=$(count_selected) if [ "$_count" -ge 1 ]; then break fi ;; ESC|q|Q) stty sane /dev/null || true printf "%b" "${SHOW_CURSOR}" printf "\nCancelled.\n" exit 0 ;; esac # Move cursor up total_rows lines and redraw printf "\033[%dA" "$_total_rows" render_agent_list done stty sane /dev/null || true printf "%b" "${SHOW_CURSOR}" selected_count=$(count_selected) # Clear the agent list area printf "\033[%dA" "$_total_rows" printf "\033[J" # ---- Config injection ----------------------------------------------------- configured_count=0 failed_agents="" PROXY_URL="https://mcp.useproxy.dev/mcp" inject_json_config() { _path="$1" _key="$2" _format="$3" _token="$4" PROXY_PATH="$_path" PROXY_KEY="$_key" PROXY_FORMAT="$_format" PROXY_TOKEN="$_token" \ python3 - <<'PYEOF' 2>/dev/null import json import os import re import shutil import sys path = os.environ['PROXY_PATH'] key = os.environ['PROXY_KEY'] fmt = os.environ['PROXY_FORMAT'] token = os.environ['PROXY_TOKEN'] URL = 'https://mcp.useproxy.dev/mcp' AUTH = f'Bearer {token}' def strip_jsonc(text): # Strip // line comments text = re.sub(r'//[^\n]*', '', text) # Strip /* */ block comments text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) # Strip trailing commas text = re.sub(r',(\s*[}\]])', r'\1', text) return text def build_value(fmt, token, auth): if fmt == 'http': return {"type": "http", "url": URL, "headers": {"Authorization": auth}} if fmt == 'standard': return {"url": URL, "headers": {"Authorization": auth}} if fmt == 'mcp_remote': return {"command": "npx", "args": ["mcp-remote", URL, "--header", f"Authorization: {auth}"]} if fmt == 'opencode': return {"type": "remote", "url": URL, "enabled": True, "oauth": False, "headers": {"Authorization": auth}} if fmt == 'antigravity': return {"serverUrl": URL, "headers": {"Authorization": auth}} if fmt == 'gemini_cli': return {"httpUrl": URL, "headers": {"Authorization": auth}, "trust": True} if fmt == 'zed': return {"enabled": True, "url": URL, "headers": {"Authorization": auth}} raise ValueError(f"unknown format: {fmt}") try: # Backup existing config if os.path.exists(path): shutil.copy2(path, path + '.proxy-backup') # Ensure parent dir exists os.makedirs(os.path.dirname(path), exist_ok=True) # Load existing if os.path.exists(path) and os.path.getsize(path) > 0: with open(path, 'r', encoding='utf-8') as f: raw = f.read() try: config = json.loads(raw) except json.JSONDecodeError: config = json.loads(strip_jsonc(raw)) else: config = {} if not isinstance(config, dict): raise ValueError("config root is not an object") # Navigate to key (top-level for all agents here) if key not in config or not isinstance(config.get(key), dict): config[key] = {} # Remove any legacy key if 'useproxy.dev' in config[key]: del config[key]['useproxy.dev'] # Inject config[key]['proxy'] = build_value(fmt, token, AUTH) # Atomic write tmp_path = path + '.proxy-tmp' with open(tmp_path, 'w', encoding='utf-8') as f: json.dump(config, f, indent=2) f.write('\n') os.replace(tmp_path, path) sys.exit(0) except Exception as e: sys.stderr.write(f"{type(e).__name__}: {e}\n") sys.exit(1) PYEOF } inject_toml_config() { _path="$1" _token="$2" PROXY_PATH="$_path" PROXY_TOKEN="$_token" \ python3 - <<'PYEOF' 2>/dev/null import os import re import shutil import sys path = os.environ['PROXY_PATH'] token = os.environ['PROXY_TOKEN'] URL = 'https://mcp.useproxy.dev/mcp' AUTH = f'Bearer {token}' try: if os.path.exists(path): shutil.copy2(path, path + '.proxy-backup') os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.exists(path): with open(path, 'r', encoding='utf-8') as f: content = f.read() else: content = '' # Remove any existing [mcp_servers."useproxy.dev"] or [mcp_servers.proxy] section # A section runs from its header to the next header or EOF def strip_section(text, header_re): pattern = re.compile( r'^' + header_re + r'\s*$[\s\S]*?(?=^\s*\[|\Z)', re.MULTILINE, ) return pattern.sub('', text) content = strip_section(content, r'\[mcp_servers\.proxy\]') content = strip_section(content, r'\[mcp_servers\."useproxy\.dev"\]') # Trim trailing whitespace content = content.rstrip() + ('\n\n' if content.strip() else '') new_section = ( '[mcp_servers.proxy]\n' f'url = "{URL}"\n' f'http_headers = {{ Authorization = "{AUTH}" }}\n' ) content += new_section tmp_path = path + '.proxy-tmp' with open(tmp_path, 'w', encoding='utf-8') as f: f.write(content) # Validate with tomllib if available try: import tomllib with open(tmp_path, 'rb') as f: tomllib.load(f) except ImportError: pass # Python < 3.11 except Exception as e: os.unlink(tmp_path) raise os.replace(tmp_path, path) sys.exit(0) except Exception as e: sys.stderr.write(f"{type(e).__name__}: {e}\n") sys.exit(1) PYEOF } # Iterate selected agents _show_token=0 _show_claude_web=0 i=1 while [ "$i" -le "$num_agents" ]; do if [ "$(get_selected "$i")" = "1" ]; then agent_name=$(get_agent_field "$i" 1) agent_path=$(get_agent_field "$i" 2) agent_key=$(get_agent_field "$i" 3) agent_format=$(get_agent_field "$i" 4) if [ "$agent_format" = "copy_token" ]; then _show_token=1 configured_count=$((configured_count + 1)) elif [ "$agent_format" = "claude_web" ]; then _show_claude_web=1 configured_count=$((configured_count + 1)) elif [ "$agent_format" = "codex" ]; then set +e inject_toml_config "$agent_path" "$proxy_token" _rc=$? set -e if [ $_rc -eq 0 ]; then configured_count=$((configured_count + 1)) else if [ -z "$failed_agents" ]; then failed_agents="$agent_name|$agent_path" else failed_agents="${failed_agents} $agent_name|$agent_path" fi fi else set +e inject_json_config "$agent_path" "$agent_key" "$agent_format" "$proxy_token" _rc=$? set -e if [ $_rc -eq 0 ]; then configured_count=$((configured_count + 1)) else if [ -z "$failed_agents" ]; then failed_agents="$agent_name|$agent_path" else failed_agents="${failed_agents} $agent_name|$agent_path" fi fi fi fi i=$((i + 1)) done # ---- Analytics: derive preferred_llms from selected agents ---------------- _llms="" i=1 while [ "$i" -le "$num_agents" ]; do if [ "$(get_selected "$i")" = "1" ]; then _name=$(get_agent_field "$i" 1) case "$_name" in "Claude Code") _llm="claude_code" ;; "Claude Desktop") _llm="claude_desktop" ;; "Claude Web (mobile, routines)") _llm="claude_web" ;; "Cursor") _llm="cursor" ;; "VS Code") _llm="vscode" ;; "Codex") _llm="codex" ;; "Gemini CLI") _llm="gemini_cli" ;; "OpenCode") _llm="opencode" ;; "Copilot CLI") _llm="copilot" ;; "Antigravity") _llm="antigravity" ;; "Your own agent") _llm="custom" ;; *) _llm="" ;; esac if [ -n "$_llm" ]; then case "|${_llms}|" in *"|${_llm}|"*) ;; # deduplicate *) _llms="${_llms:+${_llms},}\"${_llm}\"" ;; esac fi fi i=$((i + 1)) done # Fire-and-forget — never blocks or fails the install curl -sf -X POST https://mcp.useproxy.dev/install/identify \ -H "Authorization: Bearer ${proxy_token}" \ -H "Content-Type: application/json" \ -d "{\"preferred_llms\":[${_llms}]}" 2>/dev/null || true # Cleanup tmp files rm -f "$agents_tmp" "$selected_tmp" failed_count=0 if [ -n "$failed_agents" ]; then failed_count=$(printf "%s\n" "$failed_agents" | awk 'NF' | wc -l | tr -d ' ') fi # Determine outcome if [ "$selected_count" -eq 0 ]; then set_step_state 2 fail set_step_label 2 "Configure MCP" redraw_steps printf "\n${RED}No agents selected.${RESET}\n" exit 1 fi set_step_state 2 done if [ "$failed_count" -gt 0 ]; then set_step_label 2 "Configure MCP ($configured_count agents, $failed_count failed)" else set_step_label 2 "Configure MCP ($selected_count agents)" fi # Cursor is below the cleared agent list area. Move up over the blank # line + 2 step lines, clear, then render final step states fresh. printf "\033[3A\033[0J" render_steps printf "\n" if [ "$failed_count" -gt 0 ]; then _IFS_old=$IFS IFS=' ' for fa in $failed_agents; do [ -z "$fa" ] && continue fname=$(printf "%s" "$fa" | awk -F'|' '{print $1}') fpath=$(printf "%s" "$fa" | awk -F'|' '{print $2}') printf "Could not configure %s — see %s.proxy-backup\n" "$fname" "$fpath" done IFS=$_IFS_old fi # ---- Completion ----------------------------------------------------------- printf "\n" printf "Restart your coding agents to start using Proxy\n" if [ "$_show_claude_web" -eq 1 ]; then _cw_copied=0 case "$(uname -s)" in Darwin) printf "%s" "$PROXY_URL" | pbcopy 2>/dev/null && _cw_copied=1 ;; Linux) printf "%s" "$PROXY_URL" | xclip -selection clipboard 2>/dev/null && _cw_copied=1 || printf "%s" "$PROXY_URL" | xsel --clipboard --input 2>/dev/null && _cw_copied=1 ;; esac if [ "$_cw_copied" -eq 1 ]; then _cw_hint=" ${DIM}(in clipboard)${RESET}" else _cw_hint="" fi printf "\n${BOLD}Claude Web (finish in browser)${RESET}\n\n" printf " 1. Open ${ACCENT}https://claude.ai/settings/connectors?modal=add-custom-connector${RESET}\n" printf " 2. Name Proxy\n" printf " 3. Paste %s%b\n" "$PROXY_URL" "$_cw_hint" printf " 4. Click Add, then complete OAuth\n\n" printf "Works in claude.ai web, mobile, and scheduled routines.\n" fi if [ "$_show_token" -eq 1 ]; then printf "\nYour MCP token:\n" printf "%b%s%b\n" "${ACCENT}" "$proxy_token" "${RESET}" _copied=0 case "$(uname -s)" in Darwin) printf "%s" "$proxy_token" | pbcopy 2>/dev/null && _copied=1 ;; Linux) printf "%s" "$proxy_token" | xclip -selection clipboard 2>/dev/null && _copied=1 || printf "%s" "$proxy_token" | xsel --clipboard --input 2>/dev/null && _copied=1 ;; esac if [ "$_copied" -eq 1 ]; then printf "(copied to clipboard)\n" fi fi printf "\n" printf "%b" "${SHOW_CURSOR}" exit 0