Files
chaim a519798e69 fix(security): require Gitea tokens via env vars
Removed BUILTIN_TOKEN (dev) and the hardcoded prod/nautilus tokens
from BUILTIN_SERVERS. install.sh now refuses to run if
GITEA_DEFAULT_TOKEN, GITEA_PROD_TOKEN, or GITEA_NAUTILUS_TOKEN env
vars are missing.

Why: discovered during the LegalCrm 2026-04-25 security audit. Per
the Infisical-only secrets policy, tokens MUST come from Infisical
(/gitea per-environment → API_TOKEN), never baked into source. The
script was previously a public credential dump for all three
production Gitea instances.

Run with the env vars set before re-installing:
  GITEA_DEFAULT_TOKEN=<dev> GITEA_PROD_TOKEN=<prod> GITEA_NAUTILUS_TOKEN=<nautilus> ./install.sh

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:49:23 +00:00

488 lines
17 KiB
Bash
Executable File

#!/bin/bash
# install.sh - Install/upgrade gitea-mcp and configure global MCP server
#
# Usage:
# curl -fsSL https://gitea.dev.marcus-law.co.il/mcp-servers/gitea-mcp/raw/branch/main/install.sh | bash
# # or locally:
# bash install.sh
#
# Options (via env vars):
# GITEA_MCP_DIR=~/custom-dir # Custom install directory (default: ~/.gitea-mcp)
# GITEA_MCP_BRANCH=dev # Custom branch (default: main)
# GITEA_DEFAULT_HOST=https://... # Default Gitea host
# GITEA_DEFAULT_TOKEN=xxx # Default Gitea token
# AUTO_YES=1 # Skip all prompts
set -euo pipefail
REPO_URL="https://gitea.dev.marcus-law.co.il/mcp-servers/gitea-mcp.git"
INSTALL_DIR="${GITEA_MCP_DIR:-$HOME/.gitea-mcp}"
BRANCH="${GITEA_MCP_BRANCH:-main}"
AUTO_YES="${AUTO_YES:-1}"
MCP_NAME="gitea-dev"
BINARY_NAME="gitea-mcp"
GOBIN="${GOPATH:-$HOME/go}/bin"
BINARY_PATH="$GOBIN/$BINARY_NAME"
CLAUDE_JSON="$HOME/.claude.json"
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
# ── Helpers ─────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${CYAN}>>>${NC} $*"; }
ok() { echo -e "${GREEN}${NC} $*"; }
warn() { echo -e "${YELLOW} !${NC} $*"; }
error() { echo -e "${RED}${NC} $*" >&2; }
ask_yes_no() {
local prompt="$1"
if [ "$AUTO_YES" = "1" ]; then return 0; fi
if [ -e /dev/tty ]; then
printf "%s [Y/n] " "$prompt"
local answer=""
read -r answer < /dev/tty 2>/dev/null || answer="y"
case "$answer" in [nN]*) return 1 ;; *) return 0 ;; esac
else
return 0
fi
}
ask_input() {
local prompt="$1" var_name="$2" default="${3:-}"
if [ -e /dev/tty ]; then
if [ -n "$default" ]; then
printf "%s [%s]: " "$prompt" "$default"
else
printf "%s: " "$prompt"
fi
local answer=""
read -r answer < /dev/tty 2>/dev/null || answer=""
if [ -z "$answer" ]; then answer="$default"; fi
eval "$var_name=\$answer"
else
eval "$var_name=\$default"
fi
}
# ── Auto-install helpers ───────────────────────────────────────────────────
detect_pkg_manager() {
if command -v apt-get &>/dev/null; then echo "apt"
elif command -v dnf &>/dev/null; then echo "dnf"
elif command -v yum &>/dev/null; then echo "yum"
elif command -v pacman &>/dev/null; then echo "pacman"
elif command -v apk &>/dev/null; then echo "apk"
elif command -v brew &>/dev/null; then echo "brew"
else echo ""; fi
}
install_pkg() {
local pkg="$1"
local pm
pm=$(detect_pkg_manager)
case "$pm" in
apt) sudo apt-get update -qq && sudo apt-get install -y -qq "$pkg" ;;
dnf) sudo dnf install -y -q "$pkg" ;;
yum) sudo yum install -y -q "$pkg" ;;
pacman) sudo pacman -S --noconfirm "$pkg" ;;
apk) sudo apk add --quiet "$pkg" ;;
brew) brew install "$pkg" ;;
*) return 1 ;;
esac
}
install_go() {
local GO_VERSION_TARGET="1.24.2"
local ARCH
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
*) error "Unsupported architecture: $ARCH"; return 1 ;;
esac
local OS
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
local TARBALL="go${GO_VERSION_TARGET}.${OS}-${ARCH}.tar.gz"
local URL="https://go.dev/dl/$TARBALL"
info "Downloading Go $GO_VERSION_TARGET for $OS/$ARCH ..."
local tmpfile
tmpfile=$(mktemp)
if command -v curl &>/dev/null; then
curl -fsSL "$URL" -o "$tmpfile"
elif command -v wget &>/dev/null; then
wget -q "$URL" -O "$tmpfile"
else
error "Neither curl nor wget found — cannot download Go"
return 1
fi
info "Installing Go to /usr/local/go ..."
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "$tmpfile"
rm -f "$tmpfile"
# Add to PATH for this session
export PATH="/usr/local/go/bin:$PATH"
# Persist in shell profile if not already there
local profile="$HOME/.profile"
[ -f "$HOME/.bashrc" ] && profile="$HOME/.bashrc"
[ -f "$HOME/.zshrc" ] && profile="$HOME/.zshrc"
if ! grep -q '/usr/local/go/bin' "$profile" 2>/dev/null; then
echo 'export PATH="/usr/local/go/bin:$PATH"' >> "$profile"
info "Added Go to PATH in $profile"
fi
ok "Go $GO_VERSION_TARGET installed"
}
# ── Step 0: Prerequisites ──────────────────────────────────────────────────
echo ""
echo -e "${CYAN}=== Gitea MCP Server Installer ===${NC}"
echo ""
info "Checking prerequisites..."
# --- git ---
if ! command -v git &>/dev/null; then
warn "git not found, installing..."
if ! install_pkg git; then
error "Failed to install git. Install manually and re-run."
exit 1
fi
fi
ok "git ($(git --version | cut -d' ' -f3))"
# --- jq ---
if ! command -v jq &>/dev/null; then
warn "jq not found, installing..."
if ! install_pkg jq; then
error "Failed to install jq. Install manually and re-run."
exit 1
fi
fi
ok "jq ($(jq --version 2>/dev/null))"
# --- Go ---
if ! command -v go &>/dev/null; then
warn "Go not found, installing..."
if ! install_go; then
error "Failed to install Go. Install manually from https://go.dev/dl/ and re-run."
exit 1
fi
fi
GO_VERSION=$(go version | grep -oP 'go\d+\.\d+')
GO_MAJOR=$(echo "$GO_VERSION" | grep -oP '\d+' | head -1)
GO_MINOR=$(echo "$GO_VERSION" | grep -oP '\d+' | tail -1)
if [ "$GO_MAJOR" -lt 1 ] || { [ "$GO_MAJOR" -eq 1 ] && [ "$GO_MINOR" -lt 24 ]; }; then
warn "Go $GO_VERSION is too old (need 1.24+), upgrading..."
if ! install_go; then
error "Failed to upgrade Go. Install manually from https://go.dev/dl/ and re-run."
exit 1
fi
GO_VERSION=$(go version | grep -oP 'go\d+\.\d+')
fi
ok "go ($GO_VERSION)"
# ── Step 1: Clone or update ────────────────────────────────────────────────
echo ""
if [ -d "$INSTALL_DIR/.git" ]; then
info "Updating existing installation in $INSTALL_DIR ..."
cd "$INSTALL_DIR"
git fetch origin 2>&1 | tail -2
git reset --hard "origin/$BRANCH" 2>&1 | tail -1
ok "Updated to latest $BRANCH"
elif [ -d "$INSTALL_DIR" ]; then
# Directory exists but is not a git repo — remove and re-clone
warn "$INSTALL_DIR exists but is not a git repo, re-cloning..."
rm -rf "$INSTALL_DIR"
git clone --branch "$BRANCH" --single-branch "$REPO_URL" "$INSTALL_DIR" 2>&1 | tail -2
cd "$INSTALL_DIR"
ok "Cloned"
else
info "Cloning to $INSTALL_DIR ..."
git clone --branch "$BRANCH" --single-branch "$REPO_URL" "$INSTALL_DIR" 2>&1 | tail -2
cd "$INSTALL_DIR"
ok "Cloned"
fi
# ── Step 2: Build ──────────────────────────────────────────────────────────
echo ""
info "Building $BINARY_NAME ..."
mkdir -p "$GOBIN"
go build -o "$BINARY_PATH" . 2>&1
VERSION=$("$BINARY_PATH" --version 2>/dev/null || echo "unknown")
ok "Built $BINARY_PATH ($VERSION)"
# ── Step 3: Find existing MCP config ───────────────────────────────────────
echo ""
info "Checking existing MCP configuration..."
# Determine where the MCP entry currently lives
MCP_CONFIG_FILE=""
EXISTING_ENTRY=""
# Check ~/.claude.json first (preferred location for global MCP servers)
if [ -f "$CLAUDE_JSON" ]; then
EXISTING_ENTRY=$(jq -r --arg name "$MCP_NAME" '.mcpServers[$name] // empty' "$CLAUDE_JSON" 2>/dev/null || true)
if [ -n "$EXISTING_ENTRY" ]; then
MCP_CONFIG_FILE="$CLAUDE_JSON"
fi
fi
# Check ~/.claude/settings.json (legacy location)
if [ -z "$EXISTING_ENTRY" ] && [ -f "$CLAUDE_SETTINGS" ]; then
EXISTING_ENTRY=$(jq -r --arg name "$MCP_NAME" '.mcpServers[$name] // empty' "$CLAUDE_SETTINGS" 2>/dev/null || true)
if [ -n "$EXISTING_ENTRY" ]; then
MCP_CONFIG_FILE="$CLAUDE_SETTINGS"
fi
fi
# Check for project-level .mcp.json files
FOUND_PROJECT_MCP=()
while IFS= read -r -d '' mcp_file; do
if jq -e --arg name "$MCP_NAME" '.mcpServers[$name] != null' "$mcp_file" &>/dev/null; then
FOUND_PROJECT_MCP+=("$mcp_file")
fi
done < <(find "$HOME" -maxdepth 3 -name ".mcp.json" \
-not -path "*/.claude/*" \
-not -path "*/.gitea-mcp/*" \
-not -path "*/node_modules/*" \
-print0 2>/dev/null || true)
# ── Step 4: Migrate / deduplicate ──────────────────────────────────────────
# If found in project-level files, migrate to global
if [ ${#FOUND_PROJECT_MCP[@]} -gt 0 ]; then
for mcp_file in "${FOUND_PROJECT_MCP[@]}"; do
warn "Found '$MCP_NAME' in project-level config: $mcp_file"
if [ -z "$EXISTING_ENTRY" ]; then
# No global config yet - migrate from project file
if ask_yes_no " Migrate to global config ($CLAUDE_JSON)?"; then
EXISTING_ENTRY=$(jq --arg name "$MCP_NAME" '.mcpServers[$name]' "$mcp_file")
MCP_CONFIG_FILE="$CLAUDE_JSON"
fi
fi
# Remove from project-level regardless
if ask_yes_no " Remove duplicate from $mcp_file?"; then
tmp=$(jq --arg name "$MCP_NAME" 'del(.mcpServers[$name])' "$mcp_file")
echo "$tmp" > "$mcp_file"
ok "Removed from $mcp_file"
fi
done
fi
# ── Step 5: Configure default server ───────────────────────────────────────
echo ""
# Extract existing values if we have a config
CURRENT_HOST=""
CURRENT_TOKEN=""
CURRENT_SERVERS=""
if [ -n "$EXISTING_ENTRY" ]; then
CURRENT_HOST=$(echo "$EXISTING_ENTRY" | jq -r '
(.args // []) | to_entries |
map(select(.value == "--host")) | first |
if . then (.key + 1) as $k | input | .[$k] else empty end
' 2>/dev/null || true)
# Simpler extraction
CURRENT_HOST=$(echo "$EXISTING_ENTRY" | jq -r '
.args as $a | ($a | index("--host")) as $i |
if $i then $a[$i + 1] else "" end
' 2>/dev/null || true)
CURRENT_TOKEN=$(echo "$EXISTING_ENTRY" | jq -r '.env.GITEA_ACCESS_TOKEN // ""' 2>/dev/null || true)
CURRENT_SERVERS=$(echo "$EXISTING_ENTRY" | jq -r '.env.GITEA_SERVERS // ""' 2>/dev/null || true)
ok "Existing config found"
echo " Default: ${CURRENT_HOST:-not set}"
if [ -n "$CURRENT_SERVERS" ] && [ "$CURRENT_SERVERS" != "" ]; then
echo "$CURRENT_SERVERS" | jq -r 'to_entries[] | " \(.key): \(.value.host)"' 2>/dev/null || true
fi
fi
# Determine default host and token (env vars > existing config; no built-in token).
# Per Infisical-only secrets policy, tokens MUST come from env vars at install time.
# Source values from Infisical /gitea (per environment) before re-running this script.
BUILTIN_HOST="https://gitea.dev.marcus-law.co.il"
DEFAULT_HOST="${GITEA_DEFAULT_HOST:-${CURRENT_HOST:-$BUILTIN_HOST}}"
DEFAULT_TOKEN="${GITEA_DEFAULT_TOKEN:-${CURRENT_TOKEN:-}}"
if [[ -z "$DEFAULT_TOKEN" ]]; then
err "No token resolved for default server. Set GITEA_DEFAULT_TOKEN env var (Infisical /gitea dev → API_TOKEN) and re-run."
exit 1
fi
ok "Default server: $DEFAULT_HOST"
# Built-in additional server hosts (no tokens — those come from env vars).
# Set GITEA_PROD_TOKEN and GITEA_NAUTILUS_TOKEN env vars (Infisical /gitea prod|nautilus → API_TOKEN).
BUILTIN_SERVERS_TEMPLATE='{"prod":{"host":"https://gitea.prod.marcus-law.co.il","token":"__PROD_TOKEN__","insecure":false},"nautilus":{"host":"https://gitea.nautilus.marcusgroup.org","token":"__NAUTILUS_TOKEN__","insecure":false}}'
if [[ -z "${GITEA_PROD_TOKEN:-}" || -z "${GITEA_NAUTILUS_TOKEN:-}" ]]; then
err "GITEA_PROD_TOKEN and GITEA_NAUTILUS_TOKEN env vars are required for the additional-servers template."
err "Source values from Infisical /gitea prod and /gitea nautilus, then re-run."
exit 1
fi
BUILTIN_SERVERS="${BUILTIN_SERVERS_TEMPLATE//__PROD_TOKEN__/$GITEA_PROD_TOKEN}"
BUILTIN_SERVERS="${BUILTIN_SERVERS//__NAUTILUS_TOKEN__/$GITEA_NAUTILUS_TOKEN}"
# ── Step 6: Add additional servers ─────────────────────────────────────────
SERVERS_JSON="${CURRENT_SERVERS:-$BUILTIN_SERVERS}"
while true; do
echo ""
if [ "$AUTO_YES" = "1" ]; then break; fi
if ! ask_yes_no "Add an additional Gitea server?"; then
break
fi
srv_name="" srv_host="" srv_token="" srv_insecure="false"
ask_input " Server name (e.g. prod, staging)" srv_name ""
if [ -z "$srv_name" ]; then break; fi
# Check if already exists
if [ -n "$SERVERS_JSON" ] && echo "$SERVERS_JSON" | jq -e --arg n "$srv_name" '.[$n]' &>/dev/null; then
existing_host=$(echo "$SERVERS_JSON" | jq -r --arg n "$srv_name" '.[$n].host')
warn "Server '$srv_name' already configured: $existing_host"
if ! ask_yes_no " Overwrite?"; then continue; fi
fi
ask_input " Host URL" srv_host ""
ask_input " Access token" srv_token ""
ask_input " Skip TLS verification? (y/n)" srv_insecure "n"
[[ "$srv_insecure" =~ ^[Yy] ]] && srv_insecure="true" || srv_insecure="false"
if [ -z "$SERVERS_JSON" ] || [ "$SERVERS_JSON" = "" ]; then
SERVERS_JSON="{}"
fi
SERVERS_JSON=$(echo "$SERVERS_JSON" | jq \
--arg n "$srv_name" --arg h "$srv_host" --arg t "$srv_token" --argjson i "$srv_insecure" \
'.[$n] = {host: $h, token: $t, insecure: $i}')
ok "Added server '$srv_name': $srv_host"
done
# ── Step 7: Write global MCP config ───────────────────────────────────────
echo ""
info "Writing global MCP configuration..."
# Build the MCP entry
if [ -n "$SERVERS_JSON" ] && [ "$SERVERS_JSON" != "{}" ] && [ "$SERVERS_JSON" != "" ]; then
MCP_ENTRY=$(jq -n \
--arg cmd "$BINARY_PATH" \
--arg host "$DEFAULT_HOST" \
--arg token "$DEFAULT_TOKEN" \
--arg servers "$SERVERS_JSON" \
'{
command: $cmd,
args: ["-t", "stdio", "--host", $host],
env: {
GITEA_ACCESS_TOKEN: $token,
GITEA_SERVERS: $servers
}
}')
else
MCP_ENTRY=$(jq -n \
--arg cmd "$BINARY_PATH" \
--arg host "$DEFAULT_HOST" \
--arg token "$DEFAULT_TOKEN" \
'{
command: $cmd,
args: ["-t", "stdio", "--host", $host],
env: {
GITEA_ACCESS_TOKEN: $token
}
}')
fi
# Write to ~/.claude.json (global MCP config location)
if [ ! -f "$CLAUDE_JSON" ]; then
echo '{}' > "$CLAUDE_JSON"
fi
tmp=$(jq --arg name "$MCP_NAME" --argjson entry "$MCP_ENTRY" \
'.mcpServers[$name] = $entry' "$CLAUDE_JSON")
echo "$tmp" > "$CLAUDE_JSON"
ok "Written to $CLAUDE_JSON"
# Clean up from settings.json if it exists there (legacy location)
if [ -f "$CLAUDE_SETTINGS" ]; then
if jq -e --arg name "$MCP_NAME" '.mcpServers[$name] != null' "$CLAUDE_SETTINGS" &>/dev/null; then
tmp=$(jq --arg name "$MCP_NAME" 'del(.mcpServers[$name])' "$CLAUDE_SETTINGS")
echo "$tmp" > "$CLAUDE_SETTINGS"
ok "Removed legacy entry from $CLAUDE_SETTINGS"
fi
fi
# ── Step 8: Clean up duplicates in other config files ──────────────────────
echo ""
info "Checking for duplicate configs..."
# Clean from project-level .mcp.json files
while IFS= read -r -d '' mcp_file; do
if jq -e --arg name "$MCP_NAME" '.mcpServers[$name] != null' "$mcp_file" &>/dev/null; then
warn "Duplicate found in $mcp_file"
if ask_yes_no " Remove '$MCP_NAME' from $mcp_file?"; then
tmp=$(jq --arg name "$MCP_NAME" 'del(.mcpServers[$name])' "$mcp_file")
echo "$tmp" > "$mcp_file"
ok "Removed"
fi
fi
done < <(find "$HOME" -maxdepth 3 -name ".mcp.json" \
-not -path "*/.claude/*" \
-not -path "*/.gitea-mcp/*" \
-not -path "*/node_modules/*" \
-print0 2>/dev/null || true)
# Clean from ~/.claude/settings.json if we wrote to ~/.claude.json
if [ -f "$CLAUDE_SETTINGS" ]; then
if jq -e --arg name "$MCP_NAME" '.mcpServers[$name] != null' "$CLAUDE_SETTINGS" &>/dev/null; then
warn "Duplicate found in $CLAUDE_SETTINGS"
if ask_yes_no " Remove from $CLAUDE_SETTINGS? (already in $CLAUDE_JSON)"; then
tmp=$(jq --arg name "$MCP_NAME" 'del(.mcpServers[$name])' "$CLAUDE_SETTINGS")
echo "$tmp" > "$CLAUDE_SETTINGS"
ok "Removed"
fi
fi
fi
# ── Done ───────────────────────────────────────────────────────────────────
echo ""
echo -e "${CYAN}=== Configured Servers ===${NC}"
echo " default: $DEFAULT_HOST"
if [ -n "$SERVERS_JSON" ] && [ "$SERVERS_JSON" != "{}" ] && [ "$SERVERS_JSON" != "" ]; then
echo "$SERVERS_JSON" | jq -r 'to_entries[] | " \(.key): \(.value.host)\(if .value.insecure then " (insecure)" else "" end)"'
fi
echo ""
echo -e "${GREEN}=== Installation complete! ===${NC}"
echo ""
echo " Binary: $BINARY_PATH ($VERSION)"
echo " Config: $CLAUDE_JSON"
echo " Source: $INSTALL_DIR"
echo ""
echo "Restart Claude Code to load the new MCP server."
echo ""
echo "To upgrade later:"
echo " curl -fsSL https://gitea.dev.marcus-law.co.il/mcp-servers/gitea-mcp/raw/branch/main/install.sh | bash"
echo ""