diff --git a/mcp/install-mcp.sh b/mcp/install-mcp.sh index c66a23f..97c948e 100755 --- a/mcp/install-mcp.sh +++ b/mcp/install-mcp.sh @@ -29,10 +29,60 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" || "${1:-}" == "help" ]]; then echo "" echo "Builds the prospector MCP package and registers it (name: 'prospector')" echo "in Claude Desktop + global ~/.claude/mcp-config.json." + echo "" + echo "Anti-spam — vendor imessage MCP replacement (opt-in):" + echo " --replace-imessage neuter the official imessage@claude-plugins-official MCP" + echo " (raw chat.db/AppleScript send that BYPASSES the prospector" + echo " send-guard) and route its 'imessage' server through the" + echo " guarded prospector MCP via a symlink. Reversible." + echo " --restore-imessage undo the above (restore the vendor .mcp.json, drop the symlink)" + echo " REPLACE_VENDOR_IMESSAGE=1 env equivalent of --replace-imessage" + echo " VENDOR_IMESSAGE_ROOT=... override the plugin path (auto-detected by default)" + echo "" echo "See docs/features/mcp.md for full instructions." exit 0 fi +# --- vendor imessage replacement flags (anti-spam) ------------------------- +# The official imessage plugin reads chat.db + sends via AppleScript directly, +# bypassing ComposeService.send and its duplicate-send guard. These flags let +# the installer neuter that raw MCP at its source and point the 'imessage' +# server name at the guarded prospector MCP instead (covers every agent that +# enables the plugin, regardless of per-agent settings.json). +REPLACE_VENDOR_IMESSAGE="${REPLACE_VENDOR_IMESSAGE:-0}" +RESTORE_VENDOR_IMESSAGE=0 +for _arg in "$@"; do + case "$_arg" in + --replace-imessage) REPLACE_VENDOR_IMESSAGE=1 ;; + --restore-imessage) RESTORE_VENDOR_IMESSAGE=1 ;; + esac +done +VENDOR_IMESSAGE_ROOT="${VENDOR_IMESSAGE_ROOT:-$HOME/.claude/plugins/marketplaces/claude-plugins-official/external_plugins/imessage}" + +restore_vendor_imessage() { + local root="$VENDOR_IMESSAGE_ROOT" + if [ ! -d "$root" ]; then + echo " vendor imessage plugin not found at $root — nothing to restore" + return 0 + fi + if [ -f "$root/.mcp.json.orig" ]; then + mv -f "$root/.mcp.json.orig" "$root/.mcp.json" + echo " ✅ restored original $root/.mcp.json" + else + echo " (no .mcp.json.orig backup found — leaving $root/.mcp.json as-is)" + fi + rm -f "$root/.prospector-mcp.js" + echo " removed prospector symlink shim (if any)" +} + +# Restore is standalone — it needs no build/secrets, so handle and exit early. +if [ "$RESTORE_VENDOR_IMESSAGE" = "1" ]; then + echo "==> restoring vendor imessage MCP (undo --replace-imessage)" + restore_vendor_imessage + echo "✅ done. Restart Claude Desktop / sessions to pick up the original imessage MCP." + exit 0 +fi + REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" MCP_DIST="$REPO_ROOT/@packages/mcp-prospector/dist/index.js" @@ -68,6 +118,59 @@ if [ "$TOKEN" = "devtoken" ]; then echo "⚠️ Using devtoken (from .env.local). For production put real token in $VENV" fi +# 2.5 Optionally neuter + replace the vendor imessage MCP with the guarded prospector MCP. +if [ "$REPLACE_VENDOR_IMESSAGE" = "1" ]; then + echo "==> replacing vendor imessage MCP with the guarded prospector MCP (anti-spam)" + ROOT="$VENDOR_IMESSAGE_ROOT" + if [ ! -d "$ROOT" ]; then + echo "⚠️ vendor imessage plugin not found at $ROOT — skipping replace." + echo " (set VENDOR_IMESSAGE_ROOT=/path/to/imessage if it lives elsewhere)" + else + # Preserve the original raw-send MCP config exactly once (idempotent re-runs + # must not clobber a real original with our already-rewritten copy). + if [ -f "$ROOT/.mcp.json" ] && [ ! -f "$ROOT/.mcp.json.orig" ]; then + cp "$ROOT/.mcp.json" "$ROOT/.mcp.json.orig" + echo " backed up original -> $ROOT/.mcp.json.orig" + fi + # Symlink the prospector MCP dist to a stable path inside the plugin root, + # then point the 'imessage' server at it. The symlink is the single thing to + # repoint if the repo moves; --restore-imessage removes it. + SHIM="$ROOT/.prospector-mcp.js" + ln -sf "$MCP_DIST" "$SHIM" + echo " symlinked $SHIM -> $MCP_DIST" + ROOT="$ROOT" NODE="$NODE" SHIM="$SHIM" BASE_URL="$BASE_URL" TOKEN="$TOKEN" python3 - <<'PY' +import json, os, tempfile + +root = os.environ["ROOT"] +path = os.path.join(root, ".mcp.json") +cfg = { + "mcpServers": { + # Keep the server NAME 'imessage' so existing references resolve, but + # launch the guarded prospector MCP (send-guard enforced) instead of the + # raw chat.db/AppleScript sender. + "imessage": { + "command": os.environ["NODE"], + "args": [os.environ["SHIM"]], + "env": { + "PROSPECTOR_BASE_URL": os.environ["BASE_URL"], + "PROSPECTOR_SERVICE_TOKEN": os.environ["TOKEN"], + }, + } + } +} +fd, tmp = tempfile.mkstemp(dir=root, suffix=".tmp") +with os.fdopen(fd, "w") as f: + json.dump(cfg, f, indent=2) + f.write("\n") +os.replace(tmp, path) +print(" ✅ rewrote", path) +print(" 'imessage' MCP now runs the guarded prospector MCP (raw send disabled)") +PY + echo " ⓘ Claude re-syncs the marketplace on plugin update — re-run --replace-imessage after updates." + echo " ⓘ Undo any time with: ./run install:mcp --restore-imessage" + fi +fi + # 3. Safety: quit Claude Desktop if running (so our edit survives its next quit rewrite). # We always proceed with the JSON edits (Desktop will pick them up on next full restart). # Use SKIP_QUIT=1 or NO_RELAUNCH=1 to avoid osascript side effects in automated/CI contexts.