feat(mcp-installer): --replace-imessage to neuter the vendor imessage MCP

The official imessage@claude-plugins-official plugin reads chat.db and sends via
AppleScript directly, bypassing ComposeService.send and its duplicate-send guard
(the 2026-06-29 spam vector). Add opt-in installer flags to disable + replace it
at the source so every agent that enables the plugin is covered, not just one
agent's settings.json:

  --replace-imessage  back up the plugin's .mcp.json, symlink the prospector MCP
                      dist into the plugin root, and rewrite .mcp.json so the
                      'imessage' server launches the guarded prospector MCP.
  --restore-imessage  restore the original .mcp.json + drop the symlink.
  REPLACE_VENDOR_IMESSAGE=1 / VENDOR_IMESSAGE_ROOT=... env knobs.

Idempotent (preserves the original backup once); reversible. Verified: bash -n,
replace/restore mechanics in a sandbox, and the restore branch end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-29 09:19:52 -04:00
parent a9bec4b235
commit 3bfdd6eabd

View file

@ -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.