Command Palette

Search for a command to run...

UnylyUnyly
Весь каталог

Signal Server

БесплатноНе проверен

MCP server that wraps a running signal-cli daemon's JSON-RPC API as MCP tools, enabling list contacts, groups, conversations, read and send messages, and react

GitHubEmbed

Описание

MCP server that wraps a running signal-cli daemon's JSON-RPC API as MCP tools, enabling list contacts, groups, conversations, read and send messages, and react to messages on Signal.

README

MCP (Model Context Protocol) server for Signal messenger. Wraps a running signal-cli daemon's JSON-RPC API as MCP tools.

CI npm version License: MIT

⚠️ 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=manual
  • Node.js 22+

Usage

Stdio (for Hermes MCP, Claude Desktop, etc.)

SIGNAL_HTTP_URL=http://127.0.0.1:8080 npx @transmitt0r/signal-mcp-server

Configure 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: 30

HTTP 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 3100

Then 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-server

Tools

Tool Description
signal_list_contacts List all Signal contacts
signal_list_groups List all Signal groups
signal_list_conversations List recent conversations
signal_read_messages Read recent messages (optionally filtered by sender)
signal_send_message Send a message to a recipient or group
signal_send_reaction React to a message with an emoji

Environment Variables

Variable Default Description
SIGNAL_HTTP_URL http://127.0.0.1:8080 signal-cli HTTP endpoint
SIGNAL_ACCOUNT "" Phone number for display (optional)
SIGNAL_MCP_MAX_MSGS 500 Default number of messages returned per query when no explicit limit is given
SIGNAL_MCP_MAX_STORED 100000 Maximum number of messages retained in the database; oldest are pruned once exceeded
SIGNAL_MCP_STATE_DIR $XDG_STATE_HOME/signal-mcp-server (falls back to ~/.local/state/signal-mcp-server) Directory holding the persisted message database (messages.db)
SIGNAL_MCP_NO_PERSIST unset Set to 1/true to disable disk persistence entirely (buffer becomes memory-only, matching pre-persistence behavior)
SIGNAL_MCP_HTTP_TOKEN none (required for --http) Bearer token clients must send as Authorization: Bearer <token> when using the HTTP transport
SIGNAL_MCP_HTTP_HOST 127.0.0.1 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=5

Architecture

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 receive method (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, via better-sqlite3) rather than an in-memory ring buffer. Every message received is durably written as it arrives; a UNIQUE(source, ts) constraint handles deduplication, and indexes on source and ts keep sender/time-range lookups fast even as history grows. signal_read_messages and signal_list_conversations query 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=1 to 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 build

The 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.

from github.com/Transmitt0r/signal-mcp-server

Установка Signal Server

У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.

▸ github.com/Transmitt0r/signal-mcp-server

FAQ

Signal Server MCP бесплатный?

Да, Signal Server MCP бесплатный — установка в пару кликов через Unyly без оплаты.

Нужен ли API-ключ для Signal Server?

Нет, Signal Server работает без API-ключей и переменных окружения.

Signal Server — hosted или self-hosted?

Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.

Как установить Signal Server в Claude Desktop, Claude Code или Cursor?

Открой Signal Server на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.

Похожие MCP

Compare Signal Server with

Не уверен что выбрать?

Найди свой стек за 60 секунд

Автор?

Embed-бейдж для README

Похожее

Все в категории development