- Python 91.6%
- Shell 8.4%
|
|
||
|---|---|---|
| .claude | ||
| .gitea | ||
| assets | ||
| examples | ||
| scripts | ||
| src/signal_mcp_bridge | ||
| tests | ||
| .gitignore | ||
| .python-version | ||
| .woodpecker.yml | ||
| AGENTS.md | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
Signal MCP Bridge
A local Claude Code channel server that bridges Signal messaging into a persistent Claude session via MCP (Model Context Protocol — the open standard for connecting AI assistants to external tools and data sources).
Built for a personal assistant use case: a DM-first Signal bot backed by Claude Code, running locally on macOS with signal-cli.
Features
- DM-first personal assistant on Signal
- Trusted sender allowlist with optional pairing flow
- Remote permission relay (approve Claude tool use from Signal)
- Persistent tmux-backed Claude session with launchd watchdog
- Headless one-shot
claude -pjobs for proactive/scheduled sends - Signal reactions support
- Attachment forwarding (images, PDFs, etc.)
- Outbound message deduplication (prevents self-echo reinjection)
- Group support with mention-pattern filtering
- Auto-approve permissions mode for fully unattended operation
- Local macOS setup — no Docker required
Architecture
Signal App <--> signal-cli daemon <--> Signal MCP Bridge MCP <--> Claude Code
(JSON-RPC + SSE) (channel + tools) (tmux session)
The bridge talks directly to a local signal-cli daemon --http instance via JSON-RPC and SSE:
- RPC:
http://127.0.0.1:8080/api/v1/rpc(send messages, reactions, list groups) - SSE:
http://127.0.0.1:8080/api/v1/events(receive inbound messages) - Health:
http://127.0.0.1:8080/api/v1/check
Prerequisites
- macOS (for launchd; the core bridge works on Linux too)
- Python 3.12+
- uv (Python package manager)
- signal-cli installed and linked to a Signal account
- Claude Code CLI installed
Quick Start
1. Install dependencies
git clone https://codeberg.org/abdoughnut/signal-mcp-bridge.git
cd signal-mcp-bridge
uv sync --locked
2. Start signal-cli daemon
If you don't already have signal-cli running as a daemon:
signal-cli -a +1YOURACCOUNT daemon --http 127.0.0.1:8080 --no-receive-stdout
3. Bootstrap the bridge
./scripts/bootstrap.sh \
--account +1YOURACCOUNT \
--default-chat +1TRUSTEDUSER \
--workspace ~/your-workspace
This will:
- Initialize the bridge config at
~/.claude/channels/signal-mcp-bridge/config.json - Generate
.mcp.jsonfor Claude Code - Generate launchd plists in
launchd/generated/ - Probe the daemon to verify connectivity
4. Allowlist your phone number
uv run signal-mcp-bridge-manage access allow +1TRUSTEDUSER
5. Start the Claude session
./scripts/start-session.sh --workspace ~/your-workspace
tmux attach -t signal-mcp-bridge-claude
Send a Signal DM to your bot's number. It should appear in the Claude session as a channel notification. Reply using the reply tool.
Repo Layout
src/signal_mcp_bridge/
server.py # MCP channel server (tools + inbound routing)
signal_api.py # Direct signal-cli daemon client (JSON-RPC + SSE)
routing.py # DM/group routing, permission interception, attachments
access.py # Allowlist, pairing, gate logic
delivery.py # Message formatting, chunking, permission prompts
send.py # Outbound send with dedup logging
signal_target.py # Group ID resolution
config.py # Config load/save
outbound_log.py # Dedup log reader
tools.py # MCP tool definitions (reply, react)
cli/
manage.py # Config, trust, probe, group discovery CLI
proactive_send.py # Direct send for scheduled jobs
scripts/
bootstrap.sh # One-command local setup
start-session.sh # Persistent tmux Claude session launcher
start-signal-daemon.sh # signal-cli daemon launcher
run-headless-job.sh # One-shot Claude job runner with optional Signal send
generate-launchd.sh # Generates macOS launchd plists
generate-systemd.sh # Generates Linux systemd user units
examples/
config.signal-mcp-bridge.json # Example bridge config
claude.json # Example MCP config template
jobs/ # Example prompt files for headless jobs
launchd/
generated/ # (gitignored) Machine-specific launchd plists
systemd/
generated/ # (gitignored) Machine-specific systemd units
tests/ # pytest test suite
Configuration
The bridge config lives at ~/.claude/channels/signal-mcp-bridge/config.json. See examples/config.signal-mcp-bridge.json for the full schema.
Key settings:
| Setting | Description |
|---|---|
signal.daemonUrl |
URL of the signal-cli daemon (default: http://127.0.0.1:8080) |
signal.accountNumber |
Your Signal bot account number |
signal.cliPath |
Path to signal-cli binary |
access.dmPolicy |
allowlist (default) or open |
access.allowFrom |
Array of trusted phone numbers |
delivery.defaultChatId |
Default recipient for proactive sends |
delivery.replyPrefix |
Prefix added to outbound messages (e.g., [Claude]) |
delivery.autoApprovePermissions |
Auto-approve tool permission requests (default: false) |
Proactive Sends
Send messages from outside the Claude session (e.g., from cron jobs):
uv run signal-mcp-bridge-send --chat-id +1RECIPIENT --text "Hello from a scheduled job"
Or use the headless job runner for Claude-powered scheduled tasks:
./scripts/run-headless-job.sh \
--workspace ~/your-workspace \
--prompt-file ./examples/jobs/example.prompt.md \
--chat-id +1RECIPIENT \
--job-name my-job
If the Claude output is empty or NO_MESSAGE, no Signal message is sent.
launchd (macOS)
Generate launchd plists for automatic startup:
./scripts/generate-launchd.sh \
--workspace ~/your-workspace \
--default-chat-id +1RECIPIENT
This creates three plists in launchd/generated/:
- signal-daemon — Keeps signal-cli daemon running (KeepAlive)
- session — Ensures the tmux Claude session is alive (checks every 60s)
- example-job — Example scheduled job (daily at 9 AM)
Install them:
cp launchd/generated/*.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.signal-mcp-bridge.*.plist
systemd (Linux)
Generate systemd user units:
./scripts/generate-systemd.sh \
--workspace ~/your-workspace \
--default-chat-id +1RECIPIENT
This creates units in systemd/generated/:
- signal-mcp-bridge-daemon.service — Keeps signal-cli daemon running (Restart=always)
- signal-mcp-bridge-session.timer — Ensures the tmux Claude session is alive (checks every 60s)
- signal-mcp-bridge-example-job.timer — Example scheduled job (daily at 9 AM)
Install them:
mkdir -p ~/.config/systemd/user
cp systemd/generated/*.service systemd/generated/*.timer ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now signal-mcp-bridge-daemon
systemctl --user enable --now signal-mcp-bridge-session.timer
systemctl --user enable --now signal-mcp-bridge-example-job.timer
Group Support
Discover groups from the live daemon:
uv run signal-mcp-bridge-manage discover groups
Add a group:
uv run signal-mcp-bridge-manage group add \
--id group.family \
--internal-id raw-base64-group-id \
--name "Family" \
--allow +1TRUSTEDUSER \
--mention-pattern "\\bclaude\\b"
Groups require a mention pattern match by default — the bot only responds when mentioned.
Troubleshooting
No messages arrive
uv run signal-mcp-bridge-manage probe
Verify the daemon URL, account number, and that the sender is allowlisted.
Replies fail in groups
Confirm the group is configured with the correct internalId from discover groups.
Own replies are reinjected
Ensure both the live session and proactive sends share the same config/state directory. Check ~/.claude/channels/signal-mcp-bridge/outbound-log.jsonl.
Permission prompts don't appear
The default session uses bypassPermissions. Start with --no-skip-permissions if you want remote approval via Signal.
Development
uv sync --locked --all-groups
uv run ruff check
uv run pytest -q