signal-mcp
Provides tools to send Signal messages, questions, and permission requests, and to check for replies via the user's existing signal-cli daemon.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@signal-mcpSend Josh a Signal asking if he approved the budget, then check for his reply."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
signal-mcp
MCP server + reply subscriber that lets any Claude session message
Josh on Signal. Fronts the host's existing signal-cli daemon
(signal-cli-daemon.service, account +14013752241) without
re-registering or bind-mounting its data directory.
Architecture
Claude Desktop ──stdio──> docker exec ──> python3 /app/server.py ─┐
│ HTTP JSON-RPC
host: signal-cli-daemon.service ◀── 127.0.0.1:8080 (synchronous)─┤ (send, version)
◀── 127.0.0.1:7583 (TCP stream)─┤ (receive notifications)
│
docker container (signal-mcp, restart=unless-stopped) │
PID 1: subscriber.py ── persistent TCP socket ─────────────┘
└─> append /data/inbox.jsonl ◀── signal_check_replies readsNo bind-mount of signal-cli data. The container is purely a client of the host daemon. Signal identity keys never enter the container.
No re-registration. The Signal identity stays on the host.
Two-channel daemon. Synchronous calls (send, version, health) go over HTTP on
:8080. Incoming-message notifications stream over a persistent TCP socket on:7583. The HTTP daemon'sreceivemethod is permanently locked by signal-cli's internal auto-receive loop, so TCP is the only path that works for replies.One daemon flag added. The
--tcp 127.0.0.1:7583arg was added to the existingsignal-cli-daemon.serviceExecStart (see "Daemon configuration" below). HTTP behavior for any other consumer is unchanged.
Related MCP server: WhatsApp MCP Server
Setup
cd ~/Repos/signal-mcp
cp .env.example .env # edit if you need to change defaults
docker compose build
docker compose up -d
docker compose logs -f signal-mcp # watch the subscriber start upVerify the daemon is reachable from the container:
docker exec signal-mcp python3 -c \
"import rpc; print(rpc.ping())"
# expected: 0.14.3Register the MCP server with Claude Desktop
Merge claude_desktop_config.snippet.json into
~/.config/Claude/claude_desktop_config.json under mcpServers:
"signal-josh": {
"command": "sg",
"args": ["docker", "-c", "exec docker exec -i signal-mcp python3 /app/server.py"]
}Restart Claude Desktop.
Why the sg wrapper
sg docker -c "…" runs the command under the docker supplementary
group. This is required when the Claude Desktop process was started
before jwowk was added to the docker group — existing sessions
inherit a stale supplementary-group list and docker exec returns
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock, which surfaces in Claude Desktop's logs
as write EPIPE / Server transport closed unexpectedly.
The exec inside the -c string replaces the temporary bash spawned
by sg with the docker exec process, so SIGTERM/SIGPIPE from Claude
Desktop reach the MCP server cleanly when the connection closes.
If you log out + back in (or reboot), all new processes inherit the
docker group and the wrapper isn't strictly needed — but it's
harmless to keep, and it makes the config portable across hosts where
the group situation may vary. The signal-josh server will appear in the
MCP server list and its tools (signal_send_message,
signal_send_question, signal_send_permission_request,
signal_check_replies, signal_health) become available to any
session.
Install the companion skill
The skill lives in skill/SKILL.md. Symlink it into the user skills
directory so it's discovered alongside Josh's other skills:
SKILLS=~/.config/Claude/local-agent-mode-sessions/skills-plugin/60ec4dd2-88cd-4679-a53c-1593176d1ac9/58636bed-2152-4b76-8f90-55b982ef8e3d/skills
mkdir -p "$SKILLS/signal-josh"
ln -sf ~/Repos/signal-mcp/skill/SKILL.md "$SKILLS/signal-josh/SKILL.md"(That path is session-bound, so an alternative is to copy the skill folder there directly and update it from the repo whenever it changes.)
End-to-end test from a future Claude session
Use signal_health to confirm the daemon is up, then signal_send_message
with text="signal-mcp end-to-end test from session XYZ" and
session_id="signal-mcp-test-001". Then wait 30 seconds and call
signal_check_replies with session_id="signal-mcp-test-001".Josh replies with anything; the next signal_check_replies call
should return his message.
Tools
Tool | When to call |
| Task-done pings, FYI updates. No reply expected. |
| Blocked, need an answer. |
| About to do something irreversible. Wait for explicit YES. |
| Poll for replies. Filter by session_id. |
| Full health: send path (daemon HTTP), receive path (TCP port + subscriber process liveness), and inbox status. |
Reliability notes (Jun 2026)
The inbox file (
/data/inbox.jsonl) is created at subscriber startup and is never deleted by retention pruning — an empty inbox stays as a 0-byte file. Previously, pruning removed the file entirely once all records aged out (14-day retention), which madesignal_check_replies/signal_healthlook like the subscriber was down.signal_check_repliesandsignal_healthalso ensure the file exists on every call.signal_healthandsignal_check_repliesreportsubscriber_running(scans/procforsubscriber.py) andreceive_channel_ok(TCP connect to the daemon's:7583), so a fresh session can distinguish "no replies yet" from "receive path broken".signal_send_questiontolerates clients that serialize theoptionslist into a string (see tools table).
Config (.env)
Variable | Meaning |
| E.164 of the registered signal-cli account. Default: |
| E.164 to ping by default. Default: Josh's number. |
| Daemon endpoint as seen from the container. With |
| Subscriber poll cadence in seconds. |
| Days of replies kept in |
| Inbox file path inside the container. |
Operational
docker compose ps # status
docker compose logs -f signal-mcp # subscriber output
docker exec signal-mcp tail -f /data/inbox.jsonl # raw replies
docker compose restart signal-mcp # bounce the container
docker compose down # stopIf the host daemon is restarted, the subscriber will reconnect on its next poll (errors back off up to 60 s).
Notes & limitations
Linux host only.
network_mode: hostis a no-op on Docker Desktop for macOS/Windows. To move it: switch to a bridge network, setSIGNAL_RPC_URL=http://host.docker.internal:8080/api/v1/rpc, and addextra_hosts: ["host.docker.internal:host-gateway"].HTTP daemon polling. signal-cli's HTTP mode does not stream receive events; we poll. A TCP-mode daemon with WebSocket subscription would be lower-latency, but would require Josh to change his existing systemd service.
Attachments are skipped. The subscriber only persists text bodies. Reactions, typing indicators, and read receipts are ignored.
The phone number is not memorized. It lives only in
.envand the registration snippet, both inside this project.
Daemon configuration
This project requires the host's signal-cli-daemon.service to expose
both HTTP (for synchronous calls) and TCP (for streaming receive
notifications). The unit file at
~/.config/systemd/user/signal-cli-daemon.service should have:
ExecStart=/usr/local/bin/signal-cli -a +14013752241 daemon --http 127.0.0.1:8080 --tcp 127.0.0.1:7583 --receive-mode on-startTo apply changes:
systemctl --user daemon-reload
systemctl --user restart signal-cli-daemon.serviceA backup of the original unit file is written next to the live one
with a .bak.YYYYMMDD-HHMMSS suffix the first time signal-mcp is set
up.
Why not just use HTTP for receive?
signal-cli's HTTP daemon mode is synchronous-only. receive over
HTTP always errors with "Receive command cannot be used if messages are already being received" because the daemon's internal receive
loop owns the queue and HTTP has no way to deliver push events. TCP
mode opens a persistent newline-delimited JSON-RPC stream and pushes
receive notifications as they arrive — that's the path
subscriber.py uses.
This server cannot be deployed
Maintenance
Related MCP Connectors
Let Claude or ChatGPT search, read and send your WhatsApp messages over MCP. OAuth sign-in.
WhatsMCP connects Claude and other MCP-compatible AI agents directly to WhatsApp. Send and receive text, images, documents, and voice notes; manage groups (create, add/remove members, promote admins); look up contacts and profiles; follow channels; and read call and message history — all through a standard MCP interface. For voice use cases, WhatsMCP offers SIP-based calling plans (inbound-only, or full inbound/outbound) so AI voice agents can answer and place WhatsApp calls, plus low-latency WebSocket integrations with voice agent providers like ElevenLabs. Multiple WhatsApp accounts can be paired and managed per workspace, with webhook support for real-time inbound message delivery to your own infrastructure.
- platform7nOAuthtech.p7n
Connect Claude to your Platform7n workspaces — chat, links, and tasks. One-click OAuth.
WHOOP recovery, strain, sleep and workouts in Claude via official WHOOP OAuth. Free, open source.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables Claude Code to interact with WhatsApp for reading messages, sending replies, and searching contacts through the Model Context Protocol. It uses whatsapp-web.js to facilitate local connection management with QR code authentication and session persistence.4MIT
- AlicenseNot gradedqualityDmaintenanceEnables Claude to read and send WhatsApp messages, including media and call history, via a local bridge.MIT
- AlicenseNot gradedqualityCmaintenanceIntegrates Claude with the AGXP real-time signal network, enabling timeline polling, thread streaming, and identity-to-identity signaling via channel events and CLI skills.MIT
- AlicenseNot gradedqualityDmaintenanceEnables bidirectional communication with Claude via WhatsApp, allowing commands, file sharing, and persistent context without needing Claude Code open.11 npm1MIT