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:
parent
a9bec4b235
commit
3bfdd6eabd
1 changed files with 103 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue