#!/bin/bash # install.sh - Install/upgrade gitea-mcp and configure global MCP server # # Usage: # curl -fsSL https://gitea.dev.marcus-law.co.il/Chaim/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/Chaim/gitea-mcp.git" INSTALL_DIR="${GITEA_MCP_DIR:-$HOME/.gitea-mcp}" BRANCH="${GITEA_MCP_BRANCH:-main}" AUTO_YES="${AUTO_YES:-0}" 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 global settings.json first (preferred location) if [ -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 ~/.claude.json if [ -z "$EXISTING_ENTRY" ] && [ -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 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_SETTINGS)?"; then EXISTING_ENTRY=$(jq --arg name "$MCP_NAME" '.mcpServers[$name]' "$mcp_file") MCP_CONFIG_FILE="$CLAUDE_SETTINGS" 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 > built-in defaults) BUILTIN_HOST="https://gitea.dev.marcus-law.co.il" BUILTIN_TOKEN="90acb3ebc0f98edfb7a373ef9902bde1cc8eff96" DEFAULT_HOST="${GITEA_DEFAULT_HOST:-${CURRENT_HOST:-$BUILTIN_HOST}}" DEFAULT_TOKEN="${GITEA_DEFAULT_TOKEN:-${CURRENT_TOKEN:-$BUILTIN_TOKEN}}" ok "Default server: $DEFAULT_HOST" # Built-in additional servers BUILTIN_SERVERS='{"prod":{"host":"https://gitea.prod.marcus-law.co.il","token":"44254bf41fb8eba17b467d46bbac792db5b57333","insecure":false},"nautilus":{"host":"https://gitea.nautilus.marcusgroup.org","token":"17c8a6ea2af0b9a1d27d6fdceaf65b2d8ebc4a66","insecure":false}}' # ── 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 settings.json (global/user level) mkdir -p "$(dirname "$CLAUDE_SETTINGS")" if [ ! -f "$CLAUDE_SETTINGS" ]; then echo '{}' > "$CLAUDE_SETTINGS" fi tmp=$(jq --arg name "$MCP_NAME" --argjson entry "$MCP_ENTRY" \ '.mcpServers[$name] = $entry' "$CLAUDE_SETTINGS") echo "$tmp" > "$CLAUDE_SETTINGS" ok "Written to $CLAUDE_SETTINGS" # Also register via claude CLI if available (belt and suspenders) if command -v claude &>/dev/null; then # Check if claude mcp already has this entry to avoid duplicates if claude mcp list 2>/dev/null | grep -q "$MCP_NAME"; then claude mcp remove "$MCP_NAME" --scope user 2>/dev/null || true 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.json if we wrote to settings.json if [ -f "$CLAUDE_JSON" ]; then if jq -e --arg name "$MCP_NAME" '.mcpServers[$name] != null' "$CLAUDE_JSON" &>/dev/null; then warn "Duplicate found in $CLAUDE_JSON" if ask_yes_no " Remove from $CLAUDE_JSON? (already in settings.json)"; then tmp=$(jq --arg name "$MCP_NAME" 'del(.mcpServers[$name])' "$CLAUDE_JSON") echo "$tmp" > "$CLAUDE_JSON" 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_SETTINGS" 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/Chaim/gitea-mcp/raw/branch/main/install.sh | bash" echo ""