From eaa92b4255d04717950e9b38f007aa751eec9af4 Mon Sep 17 00:00:00 2001 From: Tim Lingo <1timlingo@gmail.com> Date: Sat, 27 Jun 2026 14:19:47 -0500 Subject: [PATCH 1/2] feat: read-only gate around WhatsApp send tools (connector defense-in-depth) send_message/send_file/send_audio_message register unless WHATSAPP_READ_ONLY=true|1. Default keeps send available (soul consent is the primary gate). Validated via stdio tools/list: default exposes 14 tools incl send_message; read-only exposes 11 (send tools hidden). Co-Authored-By: Claude Opus 4.8 (1M context) --- whatsapp-mcp-server/helpers/mcp_tool.go | 31 +++++++++++++++---------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/whatsapp-mcp-server/helpers/mcp_tool.go b/whatsapp-mcp-server/helpers/mcp_tool.go index 73660ca..219608a 100644 --- a/whatsapp-mcp-server/helpers/mcp_tool.go +++ b/whatsapp-mcp-server/helpers/mcp_tool.go @@ -62,20 +62,27 @@ func InitMcpTool() { Description: "Get most recent WhatsApp message involving the contact.", }, getLastInteractionHandler) - mcp.AddTool[sendMessageInput, map[string]any](server, &mcp.Tool{ - Name: "send_message", - Description: "Send a text message to a person or group on WhatsApp. For groups use the group JID.", - }, sendMessageHandler) + // Send tools are registered unless the connector runs in read-only mode. + // WHATSAPP_READ_ONLY=true (or 1) hides all outbound tools — defense-in-depth on top of the + // soul's per-send consent gate. Default: send tools available (consent still required upstream). + readOnly := strings.ToLower(ReadEnv("WHATSAPP_READ_ONLY", "false")) == "true" || + ReadEnv("WHATSAPP_READ_ONLY", "0") == "1" + if !readOnly { + mcp.AddTool[sendMessageInput, map[string]any](server, &mcp.Tool{ + Name: "send_message", + Description: "Send a text message to a person or group on WhatsApp. For groups use the group JID.", + }, sendMessageHandler) - mcp.AddTool[sendFileInput, map[string]any](server, &mcp.Tool{ - Name: "send_file", - Description: "Send image, video, document or any file via WhatsApp.", - }, sendFileHandler) + mcp.AddTool[sendFileInput, map[string]any](server, &mcp.Tool{ + Name: "send_file", + Description: "Send image, video, document or any file via WhatsApp.", + }, sendFileHandler) - mcp.AddTool[sendAudioMessageInput, map[string]any](server, &mcp.Tool{ - Name: "send_audio_message", - Description: "Send audio/voice message (converted to Opus .ogg if needed).", - }, sendAudioMessageHandler) + mcp.AddTool[sendAudioMessageInput, map[string]any](server, &mcp.Tool{ + Name: "send_audio_message", + Description: "Send audio/voice message (converted to Opus .ogg if needed).", + }, sendAudioMessageHandler) + } mcp.AddTool[downloadMediaInput, map[string]any](server, &mcp.Tool{ Name: "download_media", -- 2.52.0 From 7fa204d755ede0648be69e1995c52bbc7d16cfd1 Mon Sep 17 00:00:00 2001 From: Tim Lingo <1timlingo@gmail.com> Date: Sat, 27 Jun 2026 14:56:26 -0500 Subject: [PATCH 2/2] feat: bundle build script + default API_BASE_URL to local bridge scripts/build-bundle.sh cross-builds whatsapp-mcp + whatsapp-bridge (stripped, darwin/arm64) into the Neuron app's bundled connector-servers dir. tools.go: default API_BASE_URL to http://localhost:8080/api (was a hardcoded LAN IP) so the MCP finds the connector-launched loopback bridge. Validated: stripped bundled whatsapp-mcp exposes 14 tools incl send_message. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/build-bundle.sh | 28 ++++++++++++++++++++++++++++ whatsapp-mcp-server/helpers/tools.go | 4 +++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100755 scripts/build-bundle.sh diff --git a/scripts/build-bundle.sh b/scripts/build-bundle.sh new file mode 100755 index 0000000..2c5908e --- /dev/null +++ b/scripts/build-bundle.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# build-bundle.sh — build the WhatsApp connector binaries for the Neuron app's bundled +# connector-servers dir. Produces stripped native binaries (default darwin/arm64). +# +# whatsapp-mcp — the MCP server connectd spawns (@bin/whatsapp-mcp) +# whatsapp-bridge — the whatsmeow sidecar that holds the paired session (@bin/whatsapp-bridge) +# +# Usage: scripts/build-bundle.sh [OUT_DIR] +# OUT_DIR defaults to the neuron-ui bundled servers dir. The bridge uses go-sqlite3 (CGO), so +# native builds need a C toolchain (present on macOS); cross-OS builds need a cross toolchain. +set -eu + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +OUT="${1:-$HOME/Development/neuron-technologies/neuron-ui/resources/macos-arm64/neuron-connectd/servers}" +GOOS="${GOOS:-darwin}" +GOARCH="${GOARCH:-arm64}" + +mkdir -p "$OUT" +echo "==> building whatsapp-mcp + whatsapp-bridge ($GOOS/$GOARCH, stripped) -> $OUT" + +( cd "$ROOT/whatsapp-mcp-server" && \ + CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" go build -ldflags="-s -w" -o "$OUT/whatsapp-mcp" . ) +( cd "$ROOT/whatsapp-bridge" && \ + CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" go build -ldflags="-s -w" -o "$OUT/whatsapp-bridge" . ) + +chmod +x "$OUT/whatsapp-mcp" "$OUT/whatsapp-bridge" +echo "==> done:" +ls -lh "$OUT/whatsapp-mcp" "$OUT/whatsapp-bridge" diff --git a/whatsapp-mcp-server/helpers/tools.go b/whatsapp-mcp-server/helpers/tools.go index 5c48924..396c0ec 100644 --- a/whatsapp-mcp-server/helpers/tools.go +++ b/whatsapp-mcp-server/helpers/tools.go @@ -12,7 +12,9 @@ import ( var apiBaseURL = readApiBaseURL() func readApiBaseURL() string { - if v := ReadEnv("API_BASE_URL", "http://192.168.178.119:30015/api"); v != "" { + // Default to the local bridge. The connector launches the bridge on loopback :8080; a dev/LAN + // override can still be supplied via API_BASE_URL. + if v := ReadEnv("API_BASE_URL", "http://localhost:8080/api"); v != "" { return v } const fallback = "http://localhost:8080/api" -- 2.52.0