fix: make Bash MCP host registration explicit
This commit is contained in:
+125
-134
@@ -5,12 +5,12 @@
|
||||
# Supports the same capability names and the same high-level modes:
|
||||
# - dependency expansion
|
||||
# - package / release / pipx / npm installation
|
||||
# - MCP registration hints / config writing
|
||||
# - optional, explicit MCP host registration
|
||||
# - optional service start with --start-services
|
||||
# - refresh tool index unless --skip-refresh
|
||||
#
|
||||
# Usage:
|
||||
# bash skills/scripts/bootstrap-reverse.sh <capability1> [capability2] ... [--start-services] [--skip-refresh]
|
||||
# bash skills/scripts/bootstrap-reverse.sh <capability1> [capability2] ... [--start-services] [--skip-refresh] [--mcp-host=none|claude|codex|both]
|
||||
# bash skills/scripts/bootstrap-reverse.sh --list
|
||||
|
||||
set -euo pipefail
|
||||
@@ -26,7 +26,9 @@ if [[ -z "$TOOLS_ROOT" || "$TOOLS_ROOT" == "/" || "$TOOLS_ROOT" == "$HOME" ]]; t
|
||||
echo "Unsafe REVERSE_SKILL_TOOLS_DIR: $TOOLS_ROOT" >&2
|
||||
exit 2
|
||||
fi
|
||||
MCP_CONFIG_PATH="${CLAUDE_MCP_CONFIG:-$HOME/.claude/mcp.json}"
|
||||
CLAUDE_MCP_CONFIG_PATH="${CLAUDE_MCP_CONFIG:-$HOME/.claude/mcp.json}"
|
||||
CODEX_MCP_CONFIG_PATH="${CODEX_CONFIG_PATH:-$HOME/.codex/config.toml}"
|
||||
MCP_HOST_TARGET="none"
|
||||
MANIFEST_PATH="$SCRIPT_DIR/bootstrap-manifest.json"
|
||||
|
||||
UNAME_S="$(uname -s 2>/dev/null || echo unknown)"
|
||||
@@ -42,6 +44,7 @@ LIST_ONLY=false
|
||||
MANUAL_REQUIRED=false
|
||||
FAILED=false
|
||||
LAST_CAPABILITY_MANUAL=false
|
||||
LAST_CAPABILITY_REGISTRATION_REQUIRED=false
|
||||
CAPABILITIES=()
|
||||
|
||||
for arg in "$@"; do
|
||||
@@ -50,6 +53,10 @@ for arg in "$@"; do
|
||||
--skip-refresh) SKIP_REFRESH=true ;;
|
||||
--list|-l) LIST_ONLY=true ;;
|
||||
--help|-h) CAPABILITIES+=("__help__") ;;
|
||||
--mcp-host=none|--mcp-host=claude|--mcp-host=codex|--mcp-host=both)
|
||||
MCP_HOST_TARGET="${arg#--mcp-host=}"
|
||||
;;
|
||||
--mcp-host=*) echo "Invalid MCP host target: ${arg#--mcp-host=}" >&2; exit 2 ;;
|
||||
-*) echo "Unknown option: $arg" >&2; exit 2 ;;
|
||||
*) CAPABILITIES+=("$arg") ;;
|
||||
esac
|
||||
@@ -126,8 +133,7 @@ make_temp_file() {
|
||||
local suffix="${1:-download}"
|
||||
local tmp_dir
|
||||
tmp_dir="$(mktemp -d /tmp/reverse-bootstrap-XXXXXX)"
|
||||
printf '%s/%s
|
||||
' "$tmp_dir" "$suffix"
|
||||
printf '%s/%s\n' "$tmp_dir" "$suffix"
|
||||
}
|
||||
|
||||
sudo_cmd() {
|
||||
@@ -170,7 +176,7 @@ platform_doc() {
|
||||
print_usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
bash skills/scripts/bootstrap-reverse.sh <capability1> [capability2] ... [--start-services] [--skip-refresh]
|
||||
bash skills/scripts/bootstrap-reverse.sh <capability1> [capability2] ... [--start-services] [--skip-refresh] [--mcp-host=none|claude|codex|both]
|
||||
bash skills/scripts/bootstrap-reverse.sh --list
|
||||
|
||||
Capabilities (parity with bootstrap-reverse.ps1):
|
||||
@@ -180,14 +186,16 @@ Capabilities (parity with bootstrap-reverse.ps1):
|
||||
|
||||
Examples:
|
||||
bash skills/scripts/bootstrap-reverse.sh jadx apktool frida
|
||||
bash skills/scripts/bootstrap-reverse.sh jshookmcp reqable-mcp
|
||||
bash skills/scripts/bootstrap-reverse.sh idapro --start-services
|
||||
bash skills/scripts/bootstrap-reverse.sh jshookmcp --mcp-host=codex
|
||||
bash skills/scripts/bootstrap-reverse.sh reqable-mcp --mcp-host=claude
|
||||
bash skills/scripts/bootstrap-reverse.sh idapro --start-services --mcp-host=both
|
||||
bash skills/scripts/bootstrap-reverse.sh burpsuite-mcp
|
||||
|
||||
Notes:
|
||||
- This script supports Linux and macOS.
|
||||
- It writes MCP config to ~/.claude/mcp.json by default.
|
||||
- Override with CLAUDE_MCP_CONFIG=/path/to/mcp.json.
|
||||
- MCP host registration is opt-in. The default is --mcp-host=none and does not write client-global config.
|
||||
- Explicit Claude registration uses CLAUDE_MCP_CONFIG or ~/.claude/mcp.json.
|
||||
- Explicit Codex registration uses CODEX_CONFIG_PATH or ~/.codex/config.toml.
|
||||
- Override install root with REVERSE_SKILL_TOOLS_DIR=~/tools.
|
||||
EOF
|
||||
}
|
||||
@@ -294,8 +302,6 @@ ensure_pnpm() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Args: repo regex [release_tag]
|
||||
# Prints: url\tdigest_or_empty
|
||||
latest_github_asset_meta() {
|
||||
local repo="$1"
|
||||
local regex="$2"
|
||||
@@ -326,8 +332,6 @@ latest_github_asset_url() {
|
||||
latest_github_asset_meta "$repo" "$regex" "$tag" | cut -f1
|
||||
}
|
||||
|
||||
# verify_sha256 file expected_or_empty [github_digest]
|
||||
# expected may be "hex" or "sha256:hex"; github_digest same. Prefer expected.
|
||||
verify_sha256() {
|
||||
local file="$1"
|
||||
local expected="$2"
|
||||
@@ -365,18 +369,9 @@ extract_archive() {
|
||||
safe_remove_install_dir "$dest" "$dest.tmp"
|
||||
mkdir -p "$dest"
|
||||
case "$archive" in
|
||||
*.zip)
|
||||
unzip -q "$archive" -d "$dest.tmp"
|
||||
;;
|
||||
*.tar.gz|*.tgz)
|
||||
mkdir -p "$dest.tmp"
|
||||
tar -xzf "$archive" -C "$dest.tmp"
|
||||
;;
|
||||
*)
|
||||
mkdir -p "$dest"
|
||||
cp "$archive" "$dest/"
|
||||
return 0
|
||||
;;
|
||||
*.zip) unzip -q "$archive" -d "$dest.tmp" ;;
|
||||
*.tar.gz|*.tgz) mkdir -p "$dest.tmp"; tar -xzf "$archive" -C "$dest.tmp" ;;
|
||||
*) mkdir -p "$dest"; cp "$archive" "$dest/"; return 0 ;;
|
||||
esac
|
||||
local top_count
|
||||
top_count=$(find "$dest.tmp" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')
|
||||
@@ -388,7 +383,6 @@ extract_archive() {
|
||||
case "$dest.tmp" in /tmp/reverse-bootstrap-*|"$TOOLS_ROOT"/*.tmp) rm -rf "$dest.tmp" ;; esac
|
||||
}
|
||||
|
||||
# install_github_release repo regex dest [release_tag] [expected_sha256]
|
||||
install_github_release() {
|
||||
local repo="$1"
|
||||
local regex="$2"
|
||||
@@ -490,11 +484,11 @@ PY
|
||||
fi
|
||||
}
|
||||
|
||||
write_mcp_server() {
|
||||
write_claude_mcp_server() {
|
||||
local name="$1"
|
||||
local json_payload="$2"
|
||||
ensure_dir "$(dirname "$MCP_CONFIG_PATH")"
|
||||
python3 - "$MCP_CONFIG_PATH" "$name" "$json_payload" <<'PY'
|
||||
ensure_dir "$(dirname "$CLAUDE_MCP_CONFIG_PATH")"
|
||||
python3 - "$CLAUDE_MCP_CONFIG_PATH" "$name" "$json_payload" <<'PY'
|
||||
import json, pathlib, sys
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
name = sys.argv[2]
|
||||
@@ -508,9 +502,80 @@ else:
|
||||
data = {}
|
||||
data.setdefault('mcpServers', {})[name] = payload
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print(path)
|
||||
PY
|
||||
log_ok "MCP server '$name' registered in $MCP_CONFIG_PATH"
|
||||
log_ok "MCP server '$name' registered for Claude in $CLAUDE_MCP_CONFIG_PATH"
|
||||
}
|
||||
|
||||
write_codex_mcp_server() {
|
||||
local name="$1"
|
||||
local json_payload="$2"
|
||||
ensure_dir "$(dirname "$CODEX_MCP_CONFIG_PATH")"
|
||||
python3 - "$CODEX_MCP_CONFIG_PATH" "$name" "$json_payload" <<'PY'
|
||||
import json, pathlib, re, sys
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
name = sys.argv[2]
|
||||
payload = json.loads(sys.argv[3])
|
||||
lines = path.read_text(encoding='utf-8').splitlines() if path.exists() else []
|
||||
header = re.compile(r'^\s*\[mcp_servers\.([^\].]+)(?:\.env)?\]\s*$')
|
||||
out = []
|
||||
skip = False
|
||||
for line in lines:
|
||||
match = header.match(line)
|
||||
if line.lstrip().startswith('['):
|
||||
if match and match.group(1) == name:
|
||||
skip = True
|
||||
continue
|
||||
if skip:
|
||||
skip = False
|
||||
if not skip:
|
||||
out.append(line)
|
||||
while out and not out[-1].strip():
|
||||
out.pop()
|
||||
if out:
|
||||
out.append('')
|
||||
|
||||
def literal(value):
|
||||
if isinstance(value, bool):
|
||||
return 'true' if value else 'false'
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
if isinstance(value, list):
|
||||
return '[' + ', '.join(literal(v) for v in value) + ']'
|
||||
text = str(value).replace('\\', '\\\\').replace('"', '\\"')
|
||||
return f'"{text}"'
|
||||
|
||||
out.append(f'[mcp_servers.{name}]')
|
||||
for key in ('type', 'url', 'command', 'args', 'bearer_token_env_var'):
|
||||
if key in payload:
|
||||
out.append(f'{key} = {literal(payload[key])}')
|
||||
for key in sorted(k for k in payload if k not in {'type','url','command','args','bearer_token_env_var','env','headers'}):
|
||||
out.append(f'{key} = {literal(payload[key])}')
|
||||
env = payload.get('env')
|
||||
if isinstance(env, dict) and env:
|
||||
out.append('')
|
||||
out.append(f'[mcp_servers.{name}.env]')
|
||||
for key in sorted(env):
|
||||
out.append(f'{key} = {literal(env[key])}')
|
||||
path.write_text('\n'.join(out) + '\n', encoding='utf-8')
|
||||
PY
|
||||
log_ok "MCP server '$name' registered for Codex in $CODEX_MCP_CONFIG_PATH"
|
||||
}
|
||||
|
||||
write_mcp_server() {
|
||||
local name="$1"
|
||||
local json_payload="$2"
|
||||
case "$MCP_HOST_TARGET" in
|
||||
none)
|
||||
LAST_CAPABILITY_REGISTRATION_REQUIRED=true
|
||||
log_warn "MCP registration skipped for '$name' (client-neutral default). Re-run with --mcp-host=claude, codex, or both."
|
||||
;;
|
||||
claude) write_claude_mcp_server "$name" "$json_payload" ;;
|
||||
codex) write_codex_mcp_server "$name" "$json_payload" ;;
|
||||
both)
|
||||
write_claude_mcp_server "$name" "$json_payload"
|
||||
write_codex_mcp_server "$name" "$json_payload"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
test_tcp_port() {
|
||||
@@ -518,9 +583,7 @@ test_tcp_port() {
|
||||
python3 - "$port" <<'PY' >/dev/null 2>&1
|
||||
import socket, sys
|
||||
port=int(sys.argv[1])
|
||||
s=socket.socket()
|
||||
s.settimeout(1)
|
||||
s.connect(('127.0.0.1', port))
|
||||
s=socket.socket(); s.settimeout(1); s.connect(('127.0.0.1', port))
|
||||
PY
|
||||
}
|
||||
|
||||
@@ -529,15 +592,9 @@ test_mcp_http() {
|
||||
local timeout_seconds="${2:-3}"
|
||||
python3 - "$port" "$timeout_seconds" <<'PY' >/dev/null 2>&1
|
||||
import sys, json, urllib.request
|
||||
port = int(sys.argv[1])
|
||||
timeout = int(sys.argv[2])
|
||||
port = int(sys.argv[1]); timeout = int(sys.argv[2])
|
||||
body = json.dumps({"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/mcp",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp", data=body, headers={"Content-Type": "application/json"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
exit(0 if resp.status == 200 else 1)
|
||||
@@ -564,10 +621,7 @@ manual_required() {
|
||||
log_warn "MANUAL_INSTALL_REQUIRED: $name — $hint"
|
||||
}
|
||||
|
||||
is_ready_cmd() {
|
||||
local cmd="$1"
|
||||
has_cmd "$cmd"
|
||||
}
|
||||
is_ready_cmd() { local cmd="$1"; has_cmd "$cmd"; }
|
||||
|
||||
ensure_jeb_pro() {
|
||||
if has_cmd jeb_wincon || has_cmd jeb; then
|
||||
@@ -602,8 +656,7 @@ ensure_apktool() {
|
||||
linux)
|
||||
if install_apt apktool; then return 0; fi
|
||||
ensure_dir "$TOOLS_ROOT/apktool"
|
||||
local meta url digest jar wrapper
|
||||
local repo tag sha re
|
||||
local meta url digest jar wrapper repo tag sha re
|
||||
repo=$(manifest_field apktool repo) || return 1
|
||||
tag=$(manifest_field apktool releaseTag) || return 1
|
||||
sha=$(manifest_field apktool assetSha256) || return 1
|
||||
@@ -669,9 +722,7 @@ ensure_anything_analyzer() {
|
||||
local repo commit
|
||||
repo=$(manifest_field anything-analyzer repoUrl) || return 1
|
||||
commit=$(manifest_field anything-analyzer pinnedCommit) || return 1
|
||||
if ! has_cmd git; then
|
||||
case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac
|
||||
fi
|
||||
if ! has_cmd git; then case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac; fi
|
||||
install_git_commit "$repo" "$commit" "$dir" || return 1
|
||||
ensure_node_runtime || return 1
|
||||
ensure_pnpm || return 1
|
||||
@@ -679,20 +730,9 @@ ensure_anything_analyzer() {
|
||||
if $START_SERVICES; then
|
||||
(cd "$dir" && pnpm install --frozen-lockfile) || return 1
|
||||
install_git_commit "$repo" "$commit" "$dir" || return 1
|
||||
(
|
||||
cd "$dir" || exit 1
|
||||
if has_cmd nohup; then
|
||||
nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 &
|
||||
else
|
||||
pnpm dev >/tmp/anything-analyzer.log 2>&1 &
|
||||
fi
|
||||
)
|
||||
(cd "$dir" || exit 1; if has_cmd nohup; then nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 & else pnpm dev >/tmp/anything-analyzer.log 2>&1 & fi)
|
||||
if wait_for_port 23816 120; then
|
||||
if test_mcp_http 23816; then
|
||||
log_ok "anything-analyzer MCP server ready on port 23816 (HTTP verified)"
|
||||
else
|
||||
log_warn "anything-analyzer port 23816 open but MCP HTTP handshake failed; check /tmp/anything-analyzer.log"
|
||||
fi
|
||||
if test_mcp_http 23816; then log_ok "anything-analyzer MCP server ready on port 23816 (HTTP verified)"; else log_warn "anything-analyzer port 23816 open but MCP HTTP handshake failed; check /tmp/anything-analyzer.log"; fi
|
||||
else
|
||||
log_warn "anything-analyzer did not open port 23816; see /tmp/anything-analyzer.log"
|
||||
fi
|
||||
@@ -704,19 +744,11 @@ ensure_idapro() {
|
||||
write_mcp_server "idapro" '{"url":"http://127.0.0.1:13337/mcp"}'
|
||||
if $START_SERVICES; then
|
||||
case "$PLATFORM" in
|
||||
linux)
|
||||
log_warn "Linux package does not include an IDA GUI launcher. Start IDA manually and ensure MCP listens on 127.0.0.1:13337."
|
||||
;;
|
||||
macos)
|
||||
log_warn "Start IDA Pro manually on macOS and confirm MCP port in the IDA Output window."
|
||||
;;
|
||||
linux) log_warn "Linux package does not include an IDA GUI launcher. Start IDA manually and ensure MCP listens on 127.0.0.1:13337." ;;
|
||||
macos) log_warn "Start IDA Pro manually on macOS and confirm MCP port in the IDA Output window." ;;
|
||||
esac
|
||||
wait_for_port 13337 45 || log_warn "idapro MCP service is not online on port 13337 yet."
|
||||
if test_mcp_http 13337 3; then
|
||||
log_ok "idapro MCP server ready (HTTP verified)"
|
||||
else
|
||||
log_warn "idapro port 13337 open but MCP HTTP handshake failed; check IDA Output window"
|
||||
fi
|
||||
if test_mcp_http 13337 3; then log_ok "idapro MCP server ready (HTTP verified)"; else log_warn "idapro port 13337 open but MCP HTTP handshake failed; check IDA Output window"; fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -724,10 +756,7 @@ ensure_r2() {
|
||||
if has_cmd r2; then log_ok "r2 ready: $(cmd_path r2)"; return 0; fi
|
||||
case "$PLATFORM" in
|
||||
macos) install_brew radare2 ;;
|
||||
linux)
|
||||
if install_apt radare2; then return 0; fi
|
||||
manual_required r2 "Install radare2 from GitHub/source: https://github.com/radareorg/radare2"
|
||||
;;
|
||||
linux) if install_apt radare2; then return 0; fi; manual_required r2 "Install radare2 from GitHub/source: https://github.com/radareorg/radare2" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -756,17 +785,8 @@ ensure_ghidra_mcp() {
|
||||
repo=$(manifest_field ghidra-mcp repo) || return 1
|
||||
regex=$(manifest_field ghidra-mcp assetRegex) || return 1
|
||||
case "$PLATFORM" in
|
||||
macos)
|
||||
if ! has_cmd ghidraRun && [[ ! -d /Applications/Ghidra.app ]]; then
|
||||
install_brew ghidra || brew install --cask ghidra || true
|
||||
fi
|
||||
;;
|
||||
linux)
|
||||
if ! has_cmd ghidraRun; then
|
||||
install_github_release "$repo" "$regex" "$TOOLS_ROOT/ghidra" || \
|
||||
manual_required ghidra-mcp "Install Ghidra from GitHub release or Flatpak, then configure ghidra-mcp if used."
|
||||
fi
|
||||
;;
|
||||
macos) if ! has_cmd ghidraRun && [[ ! -d /Applications/Ghidra.app ]]; then install_brew ghidra || brew install --cask ghidra || true; fi ;;
|
||||
linux) if ! has_cmd ghidraRun; then install_github_release "$repo" "$regex" "$TOOLS_ROOT/ghidra" || manual_required ghidra-mcp "Install Ghidra from GitHub release or Flatpak, then configure ghidra-mcp if used."; fi ;;
|
||||
esac
|
||||
log_warn "ghidra-mcp requires local Ghidra MCP plugin/server setup. See docs/platforms/$( [[ "$PLATFORM" == macos ]] && echo macos || echo linux ).md"
|
||||
}
|
||||
@@ -787,12 +807,7 @@ ensure_proxycat() {
|
||||
local repo commit
|
||||
repo=$(manifest_field proxycat repo) || return 1
|
||||
commit=$(manifest_field proxycat pinnedCommit) || return 1
|
||||
pipx install "git+${repo}@${commit}" || {
|
||||
manual_required proxycat "Clone/install ProxyCat manually; verify command 'proxycat'."
|
||||
LAST_CAPABILITY_MANUAL=true
|
||||
MANUAL_REQUIRED=true
|
||||
return 0
|
||||
}
|
||||
pipx install "git+${repo}@${commit}" || { manual_required proxycat "Clone/install ProxyCat manually; verify command 'proxycat'."; LAST_CAPABILITY_MANUAL=true; MANUAL_REQUIRED=true; return 0; }
|
||||
}
|
||||
|
||||
ensure_burpsuite_mcp() {
|
||||
@@ -825,28 +840,16 @@ PY
|
||||
ensure_pentestswarm() {
|
||||
local pentestswarm_path
|
||||
pentestswarm_path="$(cmd_path pentestswarm)"
|
||||
if [[ -n "$pentestswarm_path" ]]; then
|
||||
register_pentestswarm_mcp "$pentestswarm_path"
|
||||
log_ok "pentestswarm ready"
|
||||
return 0
|
||||
fi
|
||||
if ! has_cmd go; then
|
||||
case "$PLATFORM" in macos) install_brew go ;; linux) install_apt golang-go ;; esac
|
||||
fi
|
||||
if [[ -n "$pentestswarm_path" ]]; then register_pentestswarm_mcp "$pentestswarm_path"; log_ok "pentestswarm ready"; return 0; fi
|
||||
if ! has_cmd go; then case "$PLATFORM" in macos) install_brew go ;; linux) install_apt golang-go ;; esac; fi
|
||||
local go_package docker_image
|
||||
go_package=$(manifest_field pentestswarm goPackage) || return 1
|
||||
docker_image=$(manifest_field pentestswarm dockerImage) || return 1
|
||||
if go install "$go_package"; then
|
||||
local go_bin
|
||||
go_bin="$(go env GOBIN 2>/dev/null || true)"
|
||||
if [[ -z "$go_bin" ]]; then
|
||||
go_bin="$(go env GOPATH 2>/dev/null || true)/bin"
|
||||
fi
|
||||
if [[ -x "$go_bin/pentestswarm" ]]; then
|
||||
register_pentestswarm_mcp "$go_bin/pentestswarm"
|
||||
log_ok "pentestswarm installed and registered"
|
||||
return 0
|
||||
fi
|
||||
if [[ -z "$go_bin" ]]; then go_bin="$(go env GOPATH 2>/dev/null || true)/bin"; fi
|
||||
if [[ -x "$go_bin/pentestswarm" ]]; then register_pentestswarm_mcp "$go_bin/pentestswarm"; log_ok "pentestswarm installed and registered"; return 0; fi
|
||||
log_warn "pentestswarm installed but no executable was found in GOBIN/GOPATH; trying Docker fallback"
|
||||
fi
|
||||
if has_cmd docker; then
|
||||
@@ -855,7 +858,7 @@ import json, sys
|
||||
print(json.dumps({'command':'docker','args':['run','--rm','-i',sys.argv[1],'mcp','serve']}))
|
||||
PY
|
||||
)"
|
||||
log_warn "pentestswarm Go install failed or produced no runnable binary; registered Docker fallback $docker_image"
|
||||
log_warn "pentestswarm Go install failed or produced no runnable binary; prepared Docker fallback $docker_image"
|
||||
else
|
||||
manual_required pentestswarm "Install Go 1.24+ or Docker, then install Pentest-Swarm-AI and ensure pentestswarm is on PATH."
|
||||
fi
|
||||
@@ -863,18 +866,12 @@ PY
|
||||
|
||||
ensure_binwalk() {
|
||||
if has_cmd binwalk; then log_ok "binwalk ready: $(cmd_path binwalk)"; return 0; fi
|
||||
case "$PLATFORM" in
|
||||
macos) install_brew binwalk ;;
|
||||
linux) install_apt binwalk || manual_required binwalk "git clone https://github.com/ReFirmLabs/binwalk.git and install manually" ;;
|
||||
esac
|
||||
case "$PLATFORM" in macos) install_brew binwalk ;; linux) install_apt binwalk || manual_required binwalk "git clone https://github.com/ReFirmLabs/binwalk.git and install manually" ;; esac
|
||||
}
|
||||
|
||||
ensure_yara() {
|
||||
if has_cmd yara; then log_ok "yara ready: $(cmd_path yara)"; return 0; fi
|
||||
case "$PLATFORM" in
|
||||
macos) install_brew yara ;;
|
||||
linux) install_apt yara || manual_required yara "Install from source: https://github.com/VirusTotal/yara" ;;
|
||||
esac
|
||||
case "$PLATFORM" in macos) install_brew yara ;; linux) install_apt yara || manual_required yara "Install from source: https://github.com/VirusTotal/yara" ;; esac
|
||||
}
|
||||
|
||||
ensure_pwntools() {
|
||||
@@ -889,11 +886,7 @@ status_json_line() {
|
||||
local name="$1"
|
||||
local status="$2"
|
||||
local extra="${3:-}"
|
||||
if [[ -n "$extra" ]]; then
|
||||
printf '{"name":"%s","status":"%s","note":"%s"}\n' "$name" "$status" "$extra"
|
||||
else
|
||||
printf '{"name":"%s","status":"%s"}\n' "$name" "$status"
|
||||
fi
|
||||
if [[ -n "$extra" ]]; then printf '{"name":"%s","status":"%s","note":"%s"}\n' "$name" "$status" "$extra"; else printf '{"name":"%s","status":"%s"}\n' "$name" "$status"; fi
|
||||
}
|
||||
|
||||
cap_depends() {
|
||||
@@ -911,10 +904,7 @@ expand_capabilities() {
|
||||
local cap dep
|
||||
for cap in "$@"; do
|
||||
for dep in $(cap_depends "$cap"); do
|
||||
if [[ "$seen" != *" $dep "* ]]; then
|
||||
out+=("$dep")
|
||||
seen+="$dep "
|
||||
fi
|
||||
if [[ "$seen" != *" $dep "* ]]; then out+=("$dep"); seen+="$dep "; fi
|
||||
done
|
||||
done
|
||||
printf '%s\n' "${out[@]}"
|
||||
@@ -951,15 +941,13 @@ ensure_capability() {
|
||||
RESULTS_FILE="$(mktemp)"
|
||||
trap 'rm -f "$RESULTS_FILE"' EXIT
|
||||
|
||||
# macOS ships Bash 3.2, which has no mapfile/readarray. Keep this path portable
|
||||
# instead of requiring users to install a newer Bash just to run the bootstrapper.
|
||||
EXPANDED=()
|
||||
while IFS= read -r capability; do
|
||||
[[ -n "$capability" ]] || continue
|
||||
EXPANDED+=("$capability")
|
||||
done < <(expand_capabilities "${CAPABILITIES[@]}")
|
||||
|
||||
log_info "platform=$PLATFORM doc=$(platform_doc) tools_root=$TOOLS_ROOT"
|
||||
log_info "platform=$PLATFORM doc=$(platform_doc) tools_root=$TOOLS_ROOT mcp_host=$MCP_HOST_TARGET"
|
||||
|
||||
if ! ensure_python_interpreter; then
|
||||
log_err "Python 3 is required to read bootstrap-manifest.json; no capability was executed."
|
||||
@@ -969,9 +957,12 @@ fi
|
||||
for cap in "${EXPANDED[@]}"; do
|
||||
log_info "ensure $cap"
|
||||
LAST_CAPABILITY_MANUAL=false
|
||||
LAST_CAPABILITY_REGISTRATION_REQUIRED=false
|
||||
if ensure_capability "$cap"; then
|
||||
if $LAST_CAPABILITY_MANUAL; then
|
||||
status_json_line "$cap" "manual-required" "see $(platform_doc)" >> "$RESULTS_FILE"
|
||||
elif $LAST_CAPABILITY_REGISTRATION_REQUIRED; then
|
||||
status_json_line "$cap" "registration-required" "re-run with --mcp-host=claude, codex, or both" >> "$RESULTS_FILE"
|
||||
else
|
||||
status_json_line "$cap" "ready" >> "$RESULTS_FILE"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user