xdebug-mcp/install.sh
thibaud-leclere 8137f4045f docs: add install.sh and README.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 10:55:12 +02:00

577 lines
13 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
BINARY_NAME="xdebug-mcp"
RELEASE_BASE_URL="https://forge.lclr.dev"
RELEASE_REPOSITORY="AI/xdebug-mcp"
INSTALLED_BINARY_PATH=""
PREFILL_SERVER_NAME=""
PREFILL_COMMAND_PATH=""
if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then
C_RESET="$(printf '\033[0m')"
C_BOLD="$(printf '\033[1m')"
C_DIM="$(printf '\033[2m')"
C_RED="$(printf '\033[31m')"
C_GREEN="$(printf '\033[32m')"
C_YELLOW="$(printf '\033[33m')"
C_BLUE="$(printf '\033[34m')"
C_MAGENTA="$(printf '\033[35m')"
C_CYAN="$(printf '\033[36m')"
else
C_RESET=""
C_BOLD=""
C_DIM=""
C_RED=""
C_GREEN=""
C_YELLOW=""
C_BLUE=""
C_MAGENTA=""
C_CYAN=""
fi
ui_line() {
printf "%b%s%b\n" "$C_DIM" "------------------------------------------------------------" "$C_RESET" >&2
}
ui_title() {
printf "\n%b%s%b\n" "$C_BOLD$C_CYAN" "$1" "$C_RESET" >&2
}
ui_info() {
printf "%b[info]%b %s\n" "$C_BLUE" "$C_RESET" "$1" >&2
}
ui_success() {
printf "%b[ok]%b %s\n" "$C_GREEN" "$C_RESET" "$1" >&2
}
ui_warn() {
printf "%b[warn]%b %s\n" "$C_YELLOW" "$C_RESET" "$1" >&2
}
ui_error() {
printf "%b[error]%b %s\n" "$C_RED" "$C_RESET" "$1" >&2
}
prompt() {
local label="$1"
local default_value="$2"
local answer=""
if [ -n "$default_value" ]; then
printf "%b%s%b [%s]: " "$C_BOLD" "$label" "$C_RESET" "$default_value" >&2
else
printf "%b%s%b: " "$C_BOLD" "$label" "$C_RESET" >&2
fi
if [ -t 2 ] && [ -r /dev/tty ]; then
if ! IFS= read -r answer < /dev/tty 2>/dev/null; then
IFS= read -r answer || answer=""
fi
else
IFS= read -r answer || answer=""
fi
if [ -z "$answer" ]; then
printf "%s" "$default_value"
return
fi
printf "%s" "$answer"
}
tty_prompt_available() {
[ -t 2 ] && [ -r /dev/tty ] && [ -w /dev/tty ]
}
menu_select() {
local title="$1"
shift
local options=("$@")
local count="${#options[@]}"
local index=0
local key=""
local i=0
local rows=$((count + 3))
local rendered=0
if [ "$count" -eq 0 ]; then
return 1
fi
if ! tty_prompt_available; then
ui_title "$title"
i=0
while [ "$i" -lt "$count" ]; do
printf " %d) %s\n" "$((i + 1))" "${options[$i]}" >&2
i=$((i + 1))
done
while true; do
local raw_choice
raw_choice="$(prompt "Choix" "1")"
case "$raw_choice" in
''|*[!0-9]*)
ui_warn "Choix invalide: $raw_choice"
;;
*)
if [ "$raw_choice" -ge 1 ] && [ "$raw_choice" -le "$count" ]; then
printf "%s" "${options[$((raw_choice - 1))]}"
return 0
fi
ui_warn "Choix invalide: $raw_choice"
;;
esac
done
fi
while true; do
if [ "$rendered" -eq 1 ]; then
printf "\033[%dA\033[J" "$rows" >&2 2>/dev/null || true
fi
ui_title "$title"
i=0
while [ "$i" -lt "$count" ]; do
if [ "$i" -eq "$index" ]; then
printf " %b %s%b\n" "$C_BOLD$C_CYAN" "${options[$i]}" "$C_RESET" >&2
else
printf " %s\n" "${options[$i]}" >&2
fi
i=$((i + 1))
done
printf "%bUtilise ↑/↓ puis Entrée.%b\n" "$C_DIM" "$C_RESET" >&2
rendered=1
if ! IFS= read -rsn1 key < /dev/tty; then
continue
fi
case "$key" in
"")
printf "%s" "${options[$index]}"
return 0
;;
$'\x1b')
if IFS= read -rsn2 key < /dev/tty; then
case "$key" in
"[A")
if [ "$index" -eq 0 ]; then
index=$((count - 1))
else
index=$((index - 1))
fi
;;
"[B")
if [ "$index" -eq $((count - 1)) ]; then
index=0
else
index=$((index + 1))
fi
;;
esac
fi
;;
esac
done
}
sanitize_server_name() {
local raw="$1"
local sanitized
sanitized="$(printf "%s" "$raw" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g; s/--*/-/g; s/^-*//; s/-*$//')"
if [ -z "$sanitized" ]; then
sanitized="$BINARY_NAME"
fi
printf "%s" "$sanitized"
}
toml_escape() {
printf "%s" "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
go_bin_dir() {
local gobin
gobin="$(go env GOBIN 2>/dev/null || true)"
if [ -n "$gobin" ]; then
printf "%s\n" "$gobin"
return
fi
go env GOPATH 2>/dev/null | awk '{print $1 "/bin"}'
}
resolve_binary_path() {
if [ -n "$INSTALLED_BINARY_PATH" ] && [ -x "$INSTALLED_BINARY_PATH" ]; then
printf "%s\n" "$INSTALLED_BINARY_PATH"
return
fi
if command -v "$BINARY_NAME" >/dev/null 2>&1; then
command -v "$BINARY_NAME"
return
fi
if command -v go >/dev/null 2>&1; then
local bin_dir
bin_dir="$(go_bin_dir)"
if [ -n "$bin_dir" ] && [ -x "$bin_dir/$BINARY_NAME" ]; then
printf "%s\n" "$bin_dir/$BINARY_NAME"
return
fi
fi
printf "%s\n" "$HOME/.local/bin/$BINARY_NAME"
}
ensure_cli() {
local cli_name="$1"
if command -v "$cli_name" >/dev/null 2>&1; then
return
fi
ui_error "Commande introuvable: $cli_name"
exit 1
}
detect_os() {
case "$(uname -s | tr '[:upper:]' '[:lower:]')" in
linux) printf "linux" ;;
darwin) printf "darwin" ;;
*)
ui_error "OS non supporte: $(uname -s)"
exit 1
;;
esac
}
detect_arch() {
case "$(uname -m)" in
x86_64|amd64) printf "amd64" ;;
aarch64|arm64) printf "arm64" ;;
*)
ui_error "Architecture non supportee: $(uname -m)"
exit 1
;;
esac
}
release_api_url() {
printf "%s/api/v1/repos/%s/releases/latest\n" "${RELEASE_BASE_URL%/}" "$RELEASE_REPOSITORY"
}
extract_asset_url() {
local json="$1"
local asset_name="$2"
printf "%s\n" "$json" \
| grep -o '"browser_download_url":"[^"]*"' \
| sed 's/"browser_download_url":"//;s/"$//' \
| sed 's#\\/#/#g' \
| grep "/${asset_name}$" \
| head -n 1
}
verify_checksum() {
local binary_path="$1"
local checksum_path="$2"
if command -v sha256sum >/dev/null 2>&1; then
( cd "$(dirname "$binary_path")" && sha256sum -c "$(basename "$checksum_path")" )
return
fi
if command -v shasum >/dev/null 2>&1; then
local expected
local actual
expected="$(awk '{print $1}' "$checksum_path")"
actual="$(shasum -a 256 "$binary_path" | awk '{print $1}')"
if [ "$expected" != "$actual" ]; then
ui_error "Checksum invalide pour $binary_path"
exit 1
fi
return
fi
ui_warn "Aucun utilitaire checksum trouve (sha256sum/shasum), verification ignoree."
}
download_latest_release_binary() {
if ! command -v curl >/dev/null 2>&1; then
ui_error "curl est requis pour telecharger la release."
exit 1
fi
local os_name
local arch_name
local ext=""
os_name="$(detect_os)"
arch_name="$(detect_arch)"
if [ "$os_name" = "windows" ]; then
ext=".exe"
fi
local asset_name
local checksum_name
asset_name="${BINARY_NAME}-${os_name}-${arch_name}${ext}"
checksum_name="${asset_name}.sha256"
ui_info "Recuperation de la derniere release..."
local release_json
release_json="$(curl -fsSL "$(release_api_url)")"
local asset_url
asset_url="$(extract_asset_url "$release_json" "$asset_name")"
if [ -z "$asset_url" ]; then
ui_error "Asset introuvable dans la derniere release: $asset_name"
exit 1
fi
local checksum_url
checksum_url="$(extract_asset_url "$release_json" "$checksum_name" || true)"
local tmp_dir
tmp_dir="$(mktemp -d)"
trap 'if [ -n "${tmp_dir:-}" ] && [ -d "${tmp_dir:-}" ]; then rm -rf "${tmp_dir}"; fi' EXIT INT TERM
curl -fsSL "$asset_url" -o "$tmp_dir/$asset_name"
chmod +x "$tmp_dir/$asset_name"
if [ -n "$checksum_url" ]; then
curl -fsSL "$checksum_url" -o "$tmp_dir/$checksum_name"
verify_checksum "$tmp_dir/$asset_name" "$tmp_dir/$checksum_name"
fi
local target_dir
target_dir="$(prompt "Repertoire d'installation" "$HOME/.local/bin")"
mkdir -p "$target_dir"
install -m 0755 "$tmp_dir/$asset_name" "$target_dir/$BINARY_NAME"
INSTALLED_BINARY_PATH="$target_dir/$BINARY_NAME"
ui_success "Binaire installe: $INSTALLED_BINARY_PATH"
ui_info "Ajoute ce dossier au PATH si necessaire."
}
install_binary() {
if command -v "$BINARY_NAME" >/dev/null 2>&1; then
INSTALLED_BINARY_PATH="$(command -v "$BINARY_NAME")"
ui_success "Binaire detecte: $INSTALLED_BINARY_PATH"
local reinstall
reinstall="$(prompt "Forcer une reinstall depuis la derniere release ? (y/N)" "N")"
case "$reinstall" in
y|Y|yes|YES)
download_latest_release_binary
return
;;
*)
return
;;
esac
fi
download_latest_release_binary
}
collect_server_inputs() {
local default_name
default_name="$(sanitize_server_name "${PREFILL_SERVER_NAME:-$BINARY_NAME}")"
SERVER_NAME="$(prompt "Nom du serveur MCP" "$default_name")"
SERVER_NAME="$(sanitize_server_name "$SERVER_NAME")"
local default_command
if [ -n "${PREFILL_COMMAND_PATH:-}" ]; then
default_command="$PREFILL_COMMAND_PATH"
else
default_command="$(resolve_binary_path)"
fi
COMMAND_PATH="$(prompt "Chemin du binaire serveur MCP" "$default_command")"
}
choose_scope() {
local selected
selected="$(menu_select "Scope de configuration" "global (user)" "project (projet courant)")"
case "$selected" in
"global (user)")
printf "global"
;;
*)
printf "project"
;;
esac
}
apply_claude_mcp() {
ensure_cli "claude"
collect_server_inputs
local scope_choice
scope_choice="$(choose_scope)"
local claude_scope
if [ "$scope_choice" = "global" ]; then
claude_scope="user"
else
claude_scope="project"
fi
ui_info "Application de la configuration Claude ($claude_scope)..."
claude mcp remove --scope "$claude_scope" "$SERVER_NAME" >/dev/null 2>&1 || true
claude mcp add \
--transport stdio \
--scope "$claude_scope" \
"$SERVER_NAME" \
-- "$COMMAND_PATH" mcp
ui_success "Serveur \"$SERVER_NAME\" configure dans Claude ($claude_scope)."
}
rewrite_codex_project_config() {
local project_dir="$1"
local config_file="$project_dir/.codex/config.toml"
local section_prefix="[mcp_servers.${SERVER_NAME}"
mkdir -p "$project_dir/.codex"
touch "$config_file"
local tmp_file
tmp_file="$(mktemp)"
awk -v prefix="$section_prefix" '
function is_target(line, p, next_char) {
if (index(line, p) != 1) {
return 0
}
next_char = substr(line, length(p) + 1, 1)
return next_char == "]" || next_char == "."
}
/^\[.*\]$/ {
if (is_target($0, prefix)) {
skip = 1
next
}
if (skip == 1) {
skip = 0
}
}
{
if (skip != 1) {
print $0
}
}
' "$config_file" > "$tmp_file"
mv "$tmp_file" "$config_file"
{
printf "\n[mcp_servers.%s]\n" "$SERVER_NAME"
printf "command = \"%s\"\n" "$(toml_escape "$COMMAND_PATH")"
printf "args = [\"mcp\"]\n"
} >> "$config_file"
}
apply_codex_mcp() {
ensure_cli "codex"
collect_server_inputs
local scope_choice
scope_choice="$(choose_scope)"
if [ "$scope_choice" = "global" ]; then
ui_info "Application via codex mcp add (scope global)..."
codex mcp remove "$SERVER_NAME" >/dev/null 2>&1 || true
codex mcp add \
"$SERVER_NAME" \
-- "$COMMAND_PATH" mcp
ui_success "Serveur \"$SERVER_NAME\" configure dans le scope global Codex."
return
fi
local default_project_dir
default_project_dir="$(pwd)"
local project_dir
project_dir="$(prompt "Dossier projet cible pour .codex/config.toml" "$default_project_dir")"
rewrite_codex_project_config "$project_dir"
ui_success "Configuration projet ecrite dans $project_dir/.codex/config.toml"
}
print_mcp_json() {
collect_server_inputs
cat <<JSON
{
"mcpServers": {
"${SERVER_NAME}": {
"command": "${COMMAND_PATH}",
"args": ["mcp"]
}
}
}
JSON
}
print_header() {
ui_line
printf "%bMCP Install Wizard%b for %b%s%b\n" "$C_BOLD$C_MAGENTA" "$C_RESET" "$C_BOLD" "$BINARY_NAME" "$C_RESET" >&2
printf "%bRelease repo:%b %s\n" "$C_DIM" "$C_RESET" "$RELEASE_REPOSITORY" >&2
ui_line
printf "%bSelectionne une action dans le menu interactif.%b\n" "$C_DIM" "$C_RESET" >&2
}
main() {
print_header
local action
action="$(menu_select \
"Choisis une action" \
"Installer/mettre a jour le binaire" \
"Configurer Claude Code (apply direct)" \
"Configurer Codex (apply direct)" \
"Generer JSON MCP manuel" \
"Quitter")"
printf "\n" >&2
case "$action" in
"Installer/mettre a jour le binaire")
install_binary
PREFILL_SERVER_NAME="$(sanitize_server_name "$BINARY_NAME")"
PREFILL_COMMAND_PATH="$(resolve_binary_path)"
ui_title "Configuration MCP apres installation"
local next_action
next_action="$(menu_select \
"Configurer le MCP maintenant ?" \
"Configurer Claude Code (apply direct)" \
"Configurer Codex (apply direct)" \
"Generer JSON MCP manuel" \
"Terminer sans config MCP")"
printf "\n" >&2
case "$next_action" in
"Configurer Claude Code (apply direct)")
apply_claude_mcp
;;
"Configurer Codex (apply direct)")
apply_codex_mcp
;;
"Generer JSON MCP manuel")
ui_info "JSON MCP genere sur stdout."
print_mcp_json
;;
*)
ui_info "Installation terminee sans configuration MCP additionnelle."
;;
esac
;;
"Configurer Claude Code (apply direct)")
apply_claude_mcp
;;
"Configurer Codex (apply direct)")
apply_codex_mcp
;;
"Generer JSON MCP manuel")
ui_info "JSON MCP genere sur stdout."
print_mcp_json
;;
*)
ui_warn "Annule."
;;
esac
}
main "$@"