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 "Install 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 installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Transmitt0r/signal-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server