2 Commits

Author SHA1 Message Date
Tim Lingo 7fa204d755 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) <noreply@anthropic.com>
2026-06-27 14:56:26 -05:00
Tim Lingo eaa92b4255 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) <noreply@anthropic.com>
2026-06-27 14:19:47 -05:00
3 changed files with 50 additions and 13 deletions
+28
View File
@@ -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"
+19 -12
View File
@@ -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",
+3 -1
View File
@@ -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"