signal-mcp-server
Provides tools for interacting with Signal messenger, including sending messages, reading messages, listing contacts, groups, and conversations, and reacting to messages.
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-mcp-serverlist my recent conversations"
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-server
MCP (Model Context Protocol) server for Signal messenger. Wraps a running signal-cli daemon's JSON-RPC API as MCP tools.
⚠️ Known issue: message ingestion can silently drop inbound messages under a specific stuck-receiver condition. Read KNOWN_ISSUES.md before deploying this in production.
Prerequisites
signal-cli daemon running in HTTP mode with
--receive-mode=manual(see "Message ingestion" below for why this matters):signal-cli --account +491****6789 daemon --http 127.0.0.1:8080 --receive-mode=manualNode.js 22+
Related MCP server: signal-mcp-server
Usage
Stdio (for Hermes MCP, Claude Desktop, etc.)
SIGNAL_HTTP_URL=http://127.0.0.1:8080 npx @transmitt0r/signal-mcp-serverConfigure in Hermes:
mcp_servers:
signal:
command: "npx"
args: ["-y", "@transmitt0r/signal-mcp-server"]
env:
SIGNAL_HTTP_URL: "http://127.0.0.1:8080"
SIGNAL_ACCOUNT: "+491****6789"
timeout: 30HTTP transport
The HTTP transport can send/read Signal messages, so it requires a bearer
token and refuses to start without one. By default it binds to
127.0.0.1 only; set SIGNAL_MCP_HTTP_HOST to change that (e.g. to expose
it inside a Docker network).
SIGNAL_MCP_HTTP_TOKEN=$(openssl rand -hex 32) signal-mcp-server --http 3100Then call tools via JSON-RPC:
curl -X POST http://localhost:3100/ \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":"1"}'Docker
# Pull from GitHub Container Registry
docker run -e SIGNAL_HTTP_URL=http://host.docker.internal:8080 \
ghcr.io/transmitt0r/signal-mcp-server:latest
# Or build locally
docker build -t signal-mcp-server .
docker run -e SIGNAL_HTTP_URL=http://host.docker.internal:8080 \
signal-mcp-serverTools
Tool | Description |
| List all Signal contacts |
| List all Signal groups |
| List recent conversations |
| Read recent messages (optionally filtered by sender) |
| Send a message to a recipient or group |
| React to a message with an emoji |
Environment Variables
Variable | Default | Description |
|
| signal-cli HTTP endpoint |
|
| Phone number for display (optional) |
|
| Default number of messages returned per query when no explicit limit is given |
|
| Maximum number of messages retained in the database; oldest are pruned once exceeded |
|
| Directory holding the persisted message database ( |
| unset | Set to |
| none (required for | Bearer token clients must send as |
|
| Interface the HTTP transport binds to |
Message ingestion: why --receive-mode=manual is required
Earlier versions of this server ingested messages via signal-cli's SSE
stream (/api/v1/events), with an optional file-tailer for durability. Both
approaches shared a structural problem, rooted in how signal-cli's default
receive mode works:
By default (--receive-mode=on-start), signal-cli's daemon runs a permanent
background thread from the moment it starts that continuously drains
Signal's server-side message queue and deletes each envelope from it —
regardless of whether anything is listening. Once a message is drained
this way, the only copies that exist are whatever a connected handler
happened to catch at that instant:
The SSE endpoint (
/api/v1/events) registers as a "weak" listener: it only receives messages that arrive while its HTTP connection happens to be open. Disconnects (deploys, restarts, network blips) mean those messages are gone for good — signal-cli does not queue or replay them.A file-tailer reading signal-cli's redirected stdout (
-o json+StandardOutput=append:...) was intended to close this gap by acting as an always-on "strong" listener tied to the daemon process itself. In practice, on the systemd setup this was validated against, per-message envelope logging landed on stderr, not the redirected stdout stream — so the file only ever contained daemon startup/shutdown lines, and the "durable" path silently ingested nothing. This is exactly the kind of environment-specific log-routing assumption that's fragile to depend on.
⚠️ This is not fully solved yet. The poller can get stuck reporting "already being received" after an abort, silently dropping real inbound messages until it self-heals or the daemon is restarted. See KNOWN_ISSUES.md before relying on this in production.
The actual fix: run the signal-cli daemon with --receive-mode=manual.
This disables the automatic background drain thread entirely, so incoming
messages simply stay queued on Signal's own servers — not "buffered
somewhere on this box" — until this MCP server explicitly pulls them via the
JSON-RPC receive method (POST /api/v1/rpc, {"method":"receive"}),
which signal-cli's changelog documents as being available specifically "for
polling new messages." This server runs that call in a long-poll loop
(~55s per call) as its sole ingestion path. If the MCP server (or the whole
box) is down for hours, nothing is lost — the next successful receive call
just returns everything that queued up in the meantime.
Constraint to be aware of: signal-cli only allows one active receiver
per account at a time. If the daemon is still running in the default
on-start mode (or something else is calling receive/holding an SSE
connection concurrently), this server's poll calls will fail with "Receive command cannot be used if messages are already being received" — logged
distinctly so it's easy to diagnose. There is no way to run this server's
poller alongside an SSE consumer or the default on-start thread; pick one.
Example systemd unit:
[Service]
ExecStart=/usr/local/bin/signal-cli -o json --account +1XXXXXXXXXX daemon --http 127.0.0.1:8081 --receive-mode=manual
Restart=on-failure
RestartSec=5Architecture
The server connects to a running signal-cli daemon via its JSON-RPC HTTP API. It uses two main components:
Receive poller (sole ingestion path): long-polls the JSON-RPC
receivemethod (POST /api/v1/rpc, ~55s timeout per call) in a loop for as long as the process runs. Requires the daemon to be started with--receive-mode=manual(see "Message ingestion" above) — otherwise signal-cli's own background receive thread holds the account's single receiver slot and every poll call fails with "already being received". Because messages queue safely server-side until pulled, there's no reconnect-latency or backoff tuning to get right: a poll that returns empty just means nothing arrived in that window, and the very next poll picks up anything that landed while this process was down entirely.Message buffer: Backed by a local SQLite database (
$SIGNAL_MCP_STATE_DIR/messages.db, viabetter-sqlite3) rather than an in-memory ring buffer. Every message received is durably written as it arrives; aUNIQUE(source, ts)constraint handles deduplication, and indexes onsourceandtskeep sender/time-range lookups fast even as history grows.signal_read_messagesandsignal_list_conversationsquery this store directly.This means a restart of the MCP server, or of the signal-cli daemon it depends on, no longer loses message history — the whole point of keeping history around for tools that only see it "since last restart" otherwise. Set
SIGNAL_MCP_NO_PERSIST=1to opt out and run purely in-memory (data is lost on restart, as in earlier versions).
The MCP server registers six tools (signal_list_contacts, signal_list_groups,
signal_list_conversations, signal_read_messages, signal_send_message,
signal_send_reaction) and exposes them via stdio (default) or Streamable HTTP.
Development
# Install dependencies
pnpm install
# Build (TypeScript compilation)
pnpm build
# Run unit tests (vitest)
pnpm test:unit
# Run all tests (unit + integration; integration requires a local Docker
# daemon — it uses testcontainers to spin up signal-cli)
pnpm test
# Lint (tsc — also covered by build)
pnpm buildThe TypeScript source lives under src/. The main logic is in src/index.ts,
shared types and helpers are in src/lib.ts, and unit tests in
src/tests/unit.test.ts.
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for progressive tool usage at any scale (see https://klavis.ai)
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Your own WhatsApp as an MCP server: read, search and send from any MCP client.
Remote MCP server to read and manage your Atako AI agents, messages, files, and integrations.
Related MCP Servers
- FlicenseNot gradedqualityFmaintenanceAn MCP integration for signal-cli that allows AI agents to send and receive Signal messages, supporting direct messages, group messages, and async message handling.26-
- FlicenseNot gradedqualityDmaintenanceMCP Server for retrieving Signal messages using signal-export logic.8-
- AlicenseAqualityAmaintenanceLocal Signal MCP server: read via Signal Desktop, send via signal-cli8MIT
- MIT