Local MCP channel server bridging Signal messaging into Claude Code via signal-cli
  • Python 91.6%
  • Shell 8.4%
Find a file
abdo 39f736dacd
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix(ci): use list-style steps + manual trigger event for Woodpecker 2.x
2026-05-02 16:41:39 -04:00
.claude docs: migrate project instructions to .claude/ and add AGENTS.md 2026-04-10 12:00:00 -04:00
.gitea chore: add Gitea pull request template 2026-04-10 12:05:00 -04:00
assets docs: add documentation, examples, and assets 2026-04-07 12:25:00 -04:00
examples docs: add documentation, examples, and assets 2026-04-07 12:25:00 -04:00
scripts feat: add Linux systemd unit generation alongside macOS launchd 2026-04-07 12:30:00 -04:00
src/signal_mcp_bridge feat: add CLI tools for management and proactive sends 2026-04-07 12:10:00 -04:00
tests test: add test suite for bridge components 2026-04-07 12:15:00 -04:00
.gitignore chore: initialise project scaffold 2026-04-07 12:00:00 -04:00
.python-version chore: initialise project scaffold 2026-04-07 12:00:00 -04:00
.woodpecker.yml fix(ci): use list-style steps + manual trigger event for Woodpecker 2.x 2026-05-02 16:41:39 -04:00
AGENTS.md docs: migrate project instructions to .claude/ and add AGENTS.md 2026-04-10 12:00:00 -04:00
LICENSE chore: initialise project scaffold 2026-04-07 12:00:00 -04:00
pyproject.toml chore: bump version to 0.1.1 2026-04-10 12:10:00 -04:00
README.md docs: add Woodpecker CI badge to README 2026-04-07 12:45:00 -04:00
uv.lock chore: initialise project scaffold 2026-04-07 12:00:00 -04:00

Signal MCP Bridge

Build Status

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 -p jobs 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.json for 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

License

MIT