#!/usr/bin/env bash # setup.sh — Auto-detect MT5/Wine paths and write config/mt5-quant.yaml # Usage: bash scripts/setup.sh [--yes] [--output /path/to/config.yaml] [--keep-last N] [--claude-code] # # --yes Overwrite existing config and register MCP without prompting # --output FILE Write to a custom path instead of config/mt5-quant.yaml # --keep-last N Keep only last N backtest reports (default: 20) # --claude-code Generate CLAUDE.md template and .claude/hooks/user-prompt-submit.sh # (skips main config wizard — run standalone or alongside normal setup) set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" CONFIG_OUT="${REPO_DIR}/config/mt5-quant.yaml" AUTO_YES=false KEEP_LAST=20 CLAUDE_CODE=false # ── Argument parsing ────────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --yes|-y) AUTO_YES=true ;; --output) CONFIG_OUT="$2"; shift ;; --keep-last) KEEP_LAST="$2"; shift ;; --claude-code) CLAUDE_CODE=true ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac shift done # ── Helpers ─────────────────────────────────────────────────────────────────── _green() { printf '\033[0;32m%s\033[0m\n' "$*"; } _yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; } _red() { printf '\033[0;31m%s\033[0m\n' "$*"; } _bold() { printf '\033[1m%s\033[0m\n' "$*"; } _ok() { printf ' \033[0;32m✓\033[0m %s\n' "$*"; } _warn() { printf ' \033[0;33m⚠\033[0m %s\n' "$*"; } _fail() { printf ' \033[0;31m✗\033[0m %s\n' "$*"; } _ask() { # _ask "Prompt text" default_value → echoes user input or default local prompt="$1" local default="${2:-}" if [[ -n "$default" ]]; then printf '%s [%s]: ' "$prompt" "$default" >&2 else printf '%s: ' "$prompt" >&2 fi local answer read -r answer echo "${answer:-$default}" } _pick() { # _pick "Label" item1 item2 ... → echoes chosen item local label="$1"; shift local items=("$@") if [[ ${#items[@]} -eq 1 ]]; then echo "${items[0]}" return fi printf '\n%s\n' "$label" >&2 local i for i in "${!items[@]}"; do printf ' [%d] %s\n' "$((i+1))" "${items[$i]}" >&2 done local choice while true; do printf ' Choose [1-%d]: ' "${#items[@]}" >&2 read -r choice if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#items[@]} )); then echo "${items[$((choice-1))]}" return fi printf ' Invalid choice.\n' >&2 done } # ── Wine candidate scanner ──────────────────────────────────────────────────── _scan_wine_candidates() { local candidates=() local os os="$(uname -s)" if [[ "$os" == "Darwin" ]]; then # Native MT5.app (bundled Wine — most common on macOS) local mt5_app_wine="/Applications/MetaTrader 5.app/Contents/SharedSupport/wine/bin/wine64" [[ -x "$mt5_app_wine" ]] && candidates+=("$mt5_app_wine") # CrossOver (classic install) local cxover="/Applications/CrossOver.app/Contents/SharedSupport/CrossOver/bin/wine64" [[ -x "$cxover" ]] && candidates+=("$cxover") # Homebrew wine for brew_wine in /opt/homebrew/bin/wine64 /usr/local/bin/wine64 \ /opt/homebrew/bin/wine /usr/local/bin/wine; do [[ -x "$brew_wine" ]] && candidates+=("$brew_wine") done fi # Linux / fallback for sys_wine in wine64 wine; do local found found="$(command -v "$sys_wine" 2>/dev/null || true)" [[ -n "$found" && -x "$found" ]] && candidates+=("$found") done # Deduplicate local seen=() local c for c in "${candidates[@]}"; do local dup=false local s for s in "${seen[@]:-}"; do [[ "$s" == "$c" ]] && dup=true && break; done $dup || seen+=("$c") done printf '%s\n' "${seen[@]:-}" } # ── MT5 prefix scanner ──────────────────────────────────────────────────────── _scan_mt5_prefixes() { # Returns list of terminal_dir paths (each containing terminal64.exe) local results=() local os os="$(uname -s)" if [[ "$os" == "Darwin" ]]; then local search_roots=( # Native MT5.app sandboxed prefix "$HOME/Library/Application Support" # CrossOver old "$HOME/.cxoffice" # CrossOver 24+ "$HOME/Library/Application Support/MetaQuotes" ) local root for root in "${search_roots[@]}"; do [[ -d "$root" ]] || continue while IFS= read -r -d '' terminal_exe; do results+=("$(dirname "$terminal_exe")") done < <(find "$root" -maxdepth 8 -name "terminal64.exe" -print0 2>/dev/null) done fi # Linux Wine prefixes local linux_roots=( "$HOME/.wine" "$HOME/.wine64" ) local root for root in "${linux_roots[@]}"; do [[ -d "$root" ]] || continue local mt5_dir="${root}/drive_c/Program Files/MetaTrader 5" [[ -f "${mt5_dir}/terminal64.exe" ]] && results+=("$mt5_dir") done # Deduplicate local seen=() local c for c in "${results[@]:-}"; do local dup=false local s for s in "${seen[@]:-}"; do [[ "$s" == "$c" ]] && dup=true && break; done $dup || seen+=("$c") done printf '%s\n' "${seen[@]:-}" } # Score an MT5 dir by activity: count .ex5 + .set files (higher = more active) _score_mt5_dir() { local dir="$1" local count=0 count=$(find "$dir" -name "*.ex5" -o -name "*.set" 2>/dev/null | wc -l | tr -d ' ') echo "$count" } # Pick the most active MT5 dir from a list _best_mt5_dir() { local best="" best_score=-1 local d while IFS= read -r d; do [[ -z "$d" ]] && continue local score score=$(_score_mt5_dir "$d") if (( score > best_score )); then best="$d" best_score=$score fi done echo "$best" } # ── MT5 auto-installer ─────────────────────────────────────────────────────── # # MetaTrader 5 provides official installers for both platforms: # macOS — DMG from MetaQuotes CDN (includes bundled Wine, no CrossOver needed) # Linux — Official bash installer from MetaQuotes (handles Wine + MT5 prefix) # # After install, MT5 must be launched once to unpack terminal64.exe into the # Wine prefix. _install_mt5() handles this automatically on Linux (headless via # Xvfb). On macOS the user must launch the app once manually (GUI required). MT5_DMG_URL="https://download.mql5.com/cdn/web/metaquotes.software.corp/mt5/MetaTrader5.dmg" MT5_LINUX_URL="https://download.mql5.com/cdn/web/metaquotes.software.corp/mt5/mt5ubuntu.sh" MT5_EXE_URL="https://download.mql5.com/cdn/web/metaquotes.software.corp/mt5/mt5setup.exe" _install_mt5() { local os os="$(uname -s)" if [[ "$os" == "Darwin" ]]; then _install_mt5_macos else _install_mt5_linux fi } _install_mt5_macos() { echo "" _bold "Installing MetaTrader 5 for macOS..." if [[ -d "/Applications/MetaTrader 5.app" ]]; then _ok "MetaTrader 5.app already present in /Applications" return 0 fi # Prefer mas (Mac App Store CLI) — avoids Gatekeeper issues if command -v mas &>/dev/null; then _ok "Using mas (Mac App Store CLI)..." # MT5 App Store ID: 413698442 mas install 413698442 && { _ok "Installed via Mac App Store" return 0 } || _warn "mas install failed — falling back to direct download" fi # Direct download from MetaQuotes CDN local dmg="/tmp/MetaTrader5.dmg" _ok "Downloading MetaTrader5.dmg from MetaQuotes CDN..." if ! curl -L --progress-bar --connect-timeout 30 "$MT5_DMG_URL" -o "$dmg"; then _fail "Download failed. Check your internet connection." echo "" echo " Manual install: open https://www.metatrader5.com/en/terminal/help/start_advanced/install_mac" return 1 fi _ok "Mounting DMG..." local mnt="/tmp/mt5_mount" if ! hdiutil attach "$dmg" -mountpoint "$mnt" -quiet -nobrowse; then _fail "Could not mount DMG: $dmg" return 1 fi _ok "Copying MetaTrader 5.app to /Applications..." cp -R "${mnt}/MetaTrader 5.app" /Applications/ 2>/dev/null || { _fail "Copy failed — try: sudo cp -R \"${mnt}/MetaTrader 5.app\" /Applications/" hdiutil detach "$mnt" -quiet 2>/dev/null return 1 } hdiutil detach "$mnt" -quiet 2>/dev/null rm -f "$dmg" _ok "MetaTrader 5.app installed to /Applications" echo "" _yellow "ACTION REQUIRED: Launch MetaTrader 5.app once to complete initialization." echo " It will create the Wine prefix and download terminal64.exe." echo " After it loads (shows the login screen), you can close it." echo "" if ! $AUTO_YES; then _ask "Press Enter when MT5 has been launched and closed..." "" fi } _install_mt5_linux() { echo "" _bold "Installing MetaTrader 5 for Linux..." # Check for existing Wine terminal64.exe anywhere local existing existing=$(find "$HOME/.wine" "$HOME/.wine64" \ "$HOME/Library/Application Support" 2>/dev/null \ -name "terminal64.exe" -print -quit 2>/dev/null || true) if [[ -n "$existing" ]]; then _ok "terminal64.exe already found at: $existing" return 0 fi # MetaQuotes provides an official Ubuntu installer that handles Wine + MT5 local has_wget has_curl has_wget=$(command -v wget &>/dev/null && echo yes || echo no) has_curl=$(command -v curl &>/dev/null && echo yes || echo no) local script="/tmp/mt5ubuntu.sh" _ok "Downloading MetaQuotes Linux installer..." if [[ "$has_wget" == "yes" ]]; then wget -q --show-progress "$MT5_LINUX_URL" -O "$script" || { _fail "Download failed. Check: $MT5_LINUX_URL" return 1 } elif [[ "$has_curl" == "yes" ]]; then curl -L --progress-bar "$MT5_LINUX_URL" -o "$script" || { _fail "Download failed. Check: $MT5_LINUX_URL" return 1 } else _fail "Neither wget nor curl found. Install one and retry." return 1 fi chmod +x "$script" _ok "Running MetaQuotes Linux installer (may prompt for sudo)..." bash "$script" || { _fail "Installer failed. Check output above." rm -f "$script" return 1 } rm -f "$script" # The MetaQuotes installer creates the Wine prefix and launches MT5 briefly. # On headless systems, we use Xvfb to allow MT5 to initialize. if [[ -z "${DISPLAY:-}" ]]; then _ok "Headless system — running MT5 init via Xvfb..." if ! command -v Xvfb &>/dev/null; then _warn "Xvfb not found. Install with: sudo apt install xvfb" _warn "Then launch MT5 manually: DISPLAY=:99 metatrader5" return 0 fi Xvfb :99 -screen 0 1024x768x16 &>/dev/null & local xvfb_pid=$! sleep 2 DISPLAY=:99 metatrader5 &>/dev/null & local mt5_pid=$! _ok "MT5 initializing (30s)..." sleep 30 kill "$mt5_pid" 2>/dev/null || true kill "$xvfb_pid" 2>/dev/null || true else _ok "Launching MT5 to initialize Wine prefix (closes in 20s)..." metatrader5 &>/dev/null & local mt5_pid=$! sleep 20 kill "$mt5_pid" 2>/dev/null || true fi _ok "MT5 initialization complete" } # ── Display mode detection ──────────────────────────────────────────────────── _detect_display_mode() { if [[ "$(uname -s)" == "Darwin" ]]; then echo "auto" # macOS always GUI elif [[ -n "${DISPLAY:-}" ]]; then echo "auto" # Linux with display else echo "auto" # headless Linux — auto will pick Xvfb fi } # ── YAML writer ─────────────────────────────────────────────────────────────── _write_yaml() { local wine="$1" local terminal_dir="$2" local display_mode="$3" local keep_last="${4:-20}" # Quote paths that contain spaces for YAML (wrap in double quotes) local wine_q="\"${wine}\"" local terminal_dir_q="\"${terminal_dir}\"" local experts_q="\"${terminal_dir}/MQL5/Experts\"" local tester_q="\"${terminal_dir}/MQL5/Profiles/Tester\"" local cache_q="\"${terminal_dir}/Tester\"" cat > "$CONFIG_OUT" </dev/null | wc -l | tr -d ' ') _ok "experts_dir: ${ea_count} .ex5 file(s) found" else _warn "experts_dir not found yet (will be created by MT5 on first run)" fi local tester_dir="${terminal_dir}/MQL5/Profiles/Tester" if [[ -d "$tester_dir" ]]; then local set_count set_count=$(find "$tester_dir" -name "*.set" 2>/dev/null | wc -l | tr -d ' ') _ok "tester_profiles_dir: ${set_count} .set file(s) found" else _warn "tester_profiles_dir not found yet (will be created by MT5 on first run)" fi $ok } # ── Claude Code generation ──────────────────────────────────────────────────── # # Writes two files to help users integrate Claude Code with their backtesting workflow: # # config/CLAUDE.template.md — copy to your EA project root as CLAUDE.md # .claude/hooks/user-prompt-submit.sh — injects production baseline into every prompt # # The hook reads config/baseline.json (gitignored, user-maintained). # baseline.json schema: # { # "symbol": "XAUUSD.cent", # "period": "2024-01-01/2024-12-31", # "net_profit": 1250.50, # "profit_factor": 1.43, # "max_drawdown_pct": 18.2, # "sharpe_ratio": 0.87, # "total_trades": 342, # "notes": "Best config as of 2024-12-15" # } _generate_claude_code() { echo "" _bold "Generating Claude Code integration files..." # ── CLAUDE.md template ──────────────────────────────────────────────────── local template_out="${REPO_DIR}/config/CLAUDE.template.md" if [[ -f "$template_out" ]] && ! $AUTO_YES; then local ans ans=$(_ask "config/CLAUDE.template.md already exists. Overwrite?" "no") if [[ ! "$ans" =~ ^[Yy] ]]; then _warn "Skipping CLAUDE.md template" else _write_claude_template "$template_out" fi else _write_claude_template "$template_out" fi # ── .claude/hooks/user-prompt-submit.sh ─────────────────────────────────── local hooks_dir="${REPO_DIR}/.claude/hooks" mkdir -p "$hooks_dir" local hook_out="${hooks_dir}/user-prompt-submit.sh" if [[ -f "$hook_out" ]] && ! $AUTO_YES; then local ans ans=$(_ask ".claude/hooks/user-prompt-submit.sh already exists. Overwrite?" "no") if [[ ! "$ans" =~ ^[Yy] ]]; then _warn "Skipping hook generation" return fi fi _write_baseline_hook "$hook_out" chmod +x "$hook_out" _ok "Written: $hook_out" echo "" _green "Claude Code files ready." echo "" echo " Next steps:" echo " 1. Copy config/CLAUDE.template.md to your EA project root as CLAUDE.md" echo " 2. Create config/baseline.json with your production backtest metrics" echo " (see comments in .claude/hooks/user-prompt-submit.sh for the schema)" echo " 3. The hook auto-injects your baseline into every Claude prompt" echo "" } _write_claude_template() { local out="$1" cat > "$out" <<'CLAUDE_MD' # CLAUDE.md — [Your EA Project Name] ## MT5 MCP Integration This project uses [mt5-quant](https://github.com/masdevid/mt5-quant) for backtesting and optimization via Claude Code MCP tools. Available MCP tools: `run_backtest`, `run_optimization`, `get_results`, `list_reports`, `get_report`, `get_analysis` ## Baseline Tracking Production baseline is in `config/baseline.json` (gitignored, user-maintained). - Always compare new backtest results against the baseline before calling a result an improvement. A result only counts as better if it beats the baseline on the primary metric without significantly degrading secondary metrics. - Update `baseline.json` only after confirming improvement in live/demo testing. - Key metrics: `net_profit`, `profit_factor`, `max_drawdown_pct`, `sharpe_ratio`, `total_trades`. ## Symbol Name Rules - Always use the **exact** symbol name from the broker (e.g., `"XAUUSD.cent"` not `"XAUUSD"` — they are different instruments). - The symbol is set in `config/mt5-quant.yaml` under `backtest.symbol`. - Never hardcode a symbol name in tool calls — read it from config. ## Backtest Rules - Model 0 (every tick) is required for martingale/grid EAs. - Never run two backtests in parallel — MT5 uses a single Wine prefix. - After optimization, reset `OptMode` in `terminal.ini` before the next backtest. ## Optimization Rules - Optimizations run in the background (`nohup+disown`) — never add a timeout. - Use Model 0 only — Model 1 overfits martingale/grid strategies. - `.set` files must be UTF-16LE; UTF-8 causes MT5 to strip `||Y` parameter flags. CLAUDE_MD _ok "Written: $out (copy to your EA project root as CLAUDE.md)" } _write_baseline_hook() { local out="$1" cat > "$out" <<'HOOK_SH' #!/usr/bin/env bash # .claude/hooks/user-prompt-submit.sh # Injects the current production baseline into every Claude Code prompt. # # Reads: config/baseline.json (gitignored — create and maintain manually) # # baseline.json schema: # { # "symbol": "XAUUSD.cent", # "period": "2024-01-01/2024-12-31", # "net_profit": 1250.50, # "profit_factor": 1.43, # "max_drawdown_pct": 18.2, # "sharpe_ratio": 0.87, # "total_trades": 342, # "notes": "Best config as of 2024-12-15" # } HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "${HOOK_DIR}/../.." && pwd)" BASELINE="${REPO_DIR}/config/baseline.json" [[ -f "$BASELINE" ]] || exit 0 python3 - "$BASELINE" <<'PY' import json, sys path = sys.argv[1] try: with open(path) as f: baseline = json.load(f) except Exception as e: sys.exit(0) # Malformed baseline — don't block the prompt context = ( "## Production Baseline (config/baseline.json)\n" "Compare all backtest results against these metrics. " "A result is an improvement only if it beats the baseline on the primary " "metric without significantly degrading secondary metrics.\n" "```json\n" + json.dumps(baseline, indent=2) + "\n```" ) print(json.dumps({"context": context})) PY HOOK_SH } # ── Main ────────────────────────────────────────────────────────────────────── main() { # --claude-code: skip the config wizard — only generate Claude Code integration files if $CLAUDE_CODE; then _generate_claude_code exit 0 fi echo "" _bold "MT5-Quant setup — auto-detecting Wine and MT5 paths" echo "────────────────────────────────────────────────────" # ── Check if config already exists ─────────────────────────────────────── if [[ -f "$CONFIG_OUT" ]] && ! $AUTO_YES; then _yellow "Config already exists: $CONFIG_OUT" local ans ans=$(_ask "Overwrite?" "no") if [[ ! "$ans" =~ ^[Yy] ]]; then echo "Aborted — existing config unchanged." exit 0 fi fi # ── Detect Wine ─────────────────────────────────────────────────────────── echo "" _bold "Scanning for Wine..." local wine_candidates=() while IFS= read -r line; do [[ -n "$line" ]] && wine_candidates+=("$line") done < <(_scan_wine_candidates) local wine="" if [[ ${#wine_candidates[@]} -eq 0 ]]; then _fail "No Wine installation found." local install_ans="yes" if ! $AUTO_YES; then install_ans=$(_ask "Download and install MetaTrader 5 automatically?" "yes") fi if [[ "$install_ans" =~ ^[Yy] ]]; then _install_mt5 || { _red "Auto-install failed. Install MT5 manually and re-run setup.sh."; exit 1; } # Re-scan after install while IFS= read -r line; do [[ -n "$line" ]] && wine_candidates+=("$line") done < <(_scan_wine_candidates) fi if [[ ${#wine_candidates[@]} -eq 0 ]]; then if ! $AUTO_YES; then wine=$(_ask "Enter wine64 path manually (or press Enter to abort)") fi [[ -z "$wine" ]] && { _red "Cannot continue without Wine."; exit 1; } fi fi if [[ -z "$wine" ]]; then if [[ ${#wine_candidates[@]} -eq 1 ]]; then wine="${wine_candidates[0]}" _ok "Found: $wine" else _ok "Found ${#wine_candidates[@]} Wine installations" if $AUTO_YES; then wine="${wine_candidates[0]}" _ok "Auto-selected: $wine" else wine=$(_pick "Select Wine executable:" "${wine_candidates[@]}") fi fi fi # ── Detect MT5 ──────────────────────────────────────────────────────────── echo "" _bold "Scanning for MetaTrader 5 installations..." local mt5_candidates=() while IFS= read -r line; do [[ -n "$line" ]] && mt5_candidates+=("$line") done < <(_scan_mt5_prefixes) local terminal_dir="" if [[ ${#mt5_candidates[@]} -eq 0 ]]; then _fail "No MetaTrader 5 installation found." # Wine was found but MT5 prefix doesn't exist yet — offer to run installer local install_ans2="yes" if ! $AUTO_YES; then install_ans2=$(_ask "Install MetaTrader 5 and initialize Wine prefix?" "yes") fi if [[ "$install_ans2" =~ ^[Yy] ]]; then _install_mt5 || true # Re-scan while IFS= read -r line; do [[ -n "$line" ]] && mt5_candidates+=("$line") done < <(_scan_mt5_prefixes) fi if [[ ${#mt5_candidates[@]} -eq 0 ]]; then if ! $AUTO_YES; then terminal_dir=$(_ask "Enter terminal_dir path manually (or press Enter to abort)") fi [[ -z "$terminal_dir" ]] && { _red "Cannot continue without terminal_dir."; exit 1; } fi fi if [[ -z "$terminal_dir" ]]; then if [[ ${#mt5_candidates[@]} -eq 1 ]]; then terminal_dir="${mt5_candidates[0]}" _ok "Found: $terminal_dir" else _ok "Found ${#mt5_candidates[@]} MT5 installations" # Auto-pick: highest activity score local best best=$(printf '%s\n' "${mt5_candidates[@]}" | _best_mt5_dir) if $AUTO_YES; then terminal_dir="$best" _ok "Auto-selected (most active): $terminal_dir" else # Show scores for each candidate echo "" local i for i in "${!mt5_candidates[@]}"; do local d="${mt5_candidates[$i]}" local score score=$(_score_mt5_dir "$d") printf ' [%d] %s (%d files)\n' "$((i+1))" "$d" "$score" >&2 [[ "$d" == "$best" ]] && printf ' ^ most active\n' >&2 done terminal_dir=$(_pick "Select MT5 installation:" "${mt5_candidates[@]}") fi fi fi # ── Display mode ────────────────────────────────────────────────────────── local display_mode display_mode=$(_detect_display_mode) # ── Validate ────────────────────────────────────────────────────────────── if ! _validate "$wine" "$terminal_dir"; then _red "Validation failed. Config NOT written." if ! $AUTO_YES; then local ans ans=$(_ask "Write config anyway?" "no") [[ ! "$ans" =~ ^[Yy] ]] && exit 1 else exit 1 fi fi # ── Write YAML ──────────────────────────────────────────────────────────── echo "" _bold "Writing config..." _write_yaml "$wine" "$terminal_dir" "$display_mode" "$KEEP_LAST" _ok "Written: $CONFIG_OUT" echo " Tip: see config/example.set for optimization .set file format" # ── Register with Claude Code ────────────────────────────────────────────── _offer_mcp_register echo "" _green "Setup complete!" echo "" } # ── MCP registration ────────────────────────────────────────────────────────── _offer_mcp_register() { echo "" _bold "Registering with Claude Code..." if ! command -v claude &>/dev/null; then _warn "claude CLI not found — register manually:" echo "" echo " claude mcp add mt5-quant -- python3 \"${REPO_DIR}/server/main.py\"" echo "" return fi local register=true if ! $AUTO_YES; then local ans ans=$(_ask "Register mt5-quant with Claude Code now?" "yes") [[ ! "$ans" =~ ^[Yy] ]] && register=false fi if $register; then local out out=$(claude mcp add mt5-quant -- python3 "${REPO_DIR}/server/main.py" 2>&1) || true if echo "$out" | grep -qi "already\|exists"; then _ok "Already registered (no change needed)" elif echo "$out" | grep -qi "error\|failed"; then _warn "Registration failed: $out" echo " Run manually: claude mcp add mt5-quant -- python3 \"${REPO_DIR}/server/main.py\"" else _ok "Registered: claude mcp add mt5-quant" fi else echo " Skipped. Run manually:" echo " claude mcp add mt5-quant -- python3 \"${REPO_DIR}/server/main.py\"" fi } main