Skip to main content
Glama
NathanDai5287

signal-mcp

README.md
# signal-mcp

A read-only [Model Context Protocol](https://modelcontextprotocol.io/) server for Signal Desktop messages. It supports local stdio and authenticated remote Streamable HTTP.

It gives an MCP-capable agent four narrowly scoped tools:

- `signal_list_conversations` — find recent direct and group conversations without returning message bodies.
- `signal_get_messages` — retrieve a bounded chronological window from one exact conversation.
- `signal_search_messages` — literal substring search, optionally scoped by conversation and time.
- `signal_get_message` — retrieve one exact message with quote and attachment metadata.

The server never sends messages or executes write statements. Remote mode is opt-in, binds to localhost by default, and requires authentication. See [Read-only boundary](#read-only-boundary) for an important native-driver limitation.

## Important privacy warning

This server can expose your private Signal history to whichever MCP host and model you connect it to. Review that host's data-handling policy, keep requests narrowly scoped, and do not configure this server for agents you do not trust.

Signal Desktop's local database is an implementation detail, not a public API. Signal schema updates may temporarily break this project. This project is unofficial and is not affiliated with or endorsed by Signal Messenger LLC.

## Current support

- Windows and Linux
- Signal Desktop installed for the same operating-system user
- Node.js 20 or newer
- Local stdio or remote Streamable HTTP MCP hosts

On Windows, automatic key recovery uses the current user's DPAPI credentials. On Linux, Signal's direct `config.json` key is read and validated without copying it into this project's configuration.

## Install

```powershell
git clone https://github.com/NathanDai5287/signal-mcp.git
cd signal-mcp
npm install
npm run build
```

By default the server discovers Signal under `%APPDATA%\Signal`. No configuration is normally needed.

To test the connection without configuring an MCP host:

```powershell
npx @modelcontextprotocol/inspector node dist/src/index.js
```

The Inspector can call tools that return real private messages. Treat its browser session accordingly.

## MCP host configuration

Build the project, then configure your host to launch the compiled entry point with an absolute path:

```json
{
  "mcpServers": {
    "signal": {
      "command": "node",
      "args": ["C:\\path\\to\\signal-mcp\\dist\\src\\index.js"]
    }
  }
}
```

Restart the host after changing its MCP configuration. The exact configuration file location depends on the host.

## Remote HTTP deployment

Set `SIGNAL_MCP_TRANSPORT=http`, an HTTPS `SIGNAL_MCP_PUBLIC_URL` ending in `/mcp`, and one authentication mode. The listener defaults to `127.0.0.1:8788` and is intended to sit behind a TLS tunnel or reverse proxy.

- `static` validates a fixed Bearer token. It works with Claude Code and API integrations that can supply an Authorization header.
- `oidc` validates issuer-signed JWT access tokens and publishes MCP protected-resource metadata. Use it for normal ChatGPT and hosted Claude account linking. The issuer must expose OAuth or OpenID Connect discovery metadata, a JWKS URI, authorization-code flow with S256 PKCE, and a client-registration mechanism accepted by the target host.

The Minmus instance runs as a hardened systemd service with its environment in the root-only `/etc/signal-mcp.env` file. It remains bound to localhost behind the Cloudflare Tunnel; the one-time deployment script is intentionally not retained.

## Optional configuration

Environment variables are passed through the MCP host's server configuration:

| Variable | Purpose | Default |
|---|---|---|
| `SIGNAL_MCP_DB` | Signal SQLCipher database path | `%APPDATA%\Signal\sql\db.sqlite` |
| `SIGNAL_MCP_CONFIG` | Signal `config.json` path | `%APPDATA%\Signal\config.json` |
| `SIGNAL_MCP_LOCAL_STATE` | Signal Chromium `Local State` path | `%APPDATA%\Signal\Local State` |
| `SIGNAL_MCP_KEY` | Explicit 64-character SQLCipher key | Recovered with DPAPI |
| `SIGNAL_MCP_MAX_MESSAGES` | Per-call message cap (cannot exceed 200) | `200` |
| `SIGNAL_MCP_MAX_CONVERSATIONS` | Conversation cap (cannot exceed 100) | `100` |
| `SIGNAL_MCP_TRANSPORT` | `stdio` or authenticated `http` | `stdio` |
| `SIGNAL_MCP_HOST` / `SIGNAL_MCP_PORT` | Remote listener | `127.0.0.1:8788` |
| `SIGNAL_MCP_PUBLIC_URL` | Canonical public HTTPS URL ending in `/mcp` | Required for HTTP |
| `SIGNAL_MCP_AUTH_MODE` | `static` or `oidc` | `static` |
| `SIGNAL_MCP_BEARER_TOKEN` | Fixed token, at least 32 characters | Required for static mode |
| `SIGNAL_MCP_OAUTH_ISSUER` | OAuth/OIDC issuer URL | Required for OIDC mode |
| `SIGNAL_MCP_OAUTH_AUDIENCE` | Expected JWT audience | Required for OIDC mode |
| `SIGNAL_MCP_REQUIRED_SCOPES` | Comma-separated access-token scopes | `signal:read` |
| `SIGNAL_MCP_ALLOWED_ORIGINS` | Additional permitted browser origins | None |
| `SIGNAL_MCP_MAX_BODY_BYTES` | HTTP request cap, up to 4 MiB | `1048576` |
| `SIGNAL_MCP_RATE_LIMIT_PER_MINUTE` | Per-client request limit | `120` |

Avoid putting `SIGNAL_MCP_KEY` in a committed file. Automatic recovery is preferable on Windows.

## How database access works

On Windows, Signal Desktop stores an AES-256-GCM-encrypted SQLCipher key in `config.json`; its wrapping key is in Chromium's `Local State`, protected with DPAPI. Linux Signal stores a direct key in the same config directory. At startup this server:

1. Reads and validates the Linux direct key, or DPAPI-unprotects and decrypts the Windows key in memory.
2. Opens the database with SQLCipher compatibility level 4.
3. Disables automatic WAL checkpoints and enables SQLite `query_only` mode before serving any tool calls.

The encrypted DPAPI payload is sent to the system PowerShell process over stdin, never on its command line. Subprocess output buffers and cryptographic buffers are cleared after use. Raw subprocess, filesystem, and startup errors are not printed or returned through MCP. The final SQLCipher key must briefly exist in managed memory so it can be passed to the addon's PRAGMA API; it is never intentionally logged, returned through MCP, or persisted by this project.

All search inputs are bound SQL parameters. `%`, `_`, and `\` are escaped so search terms are literal substrings rather than caller-controlled SQL patterns. Every result set and serialized tool response is capped. Repeated calls can still page through all available history; connecting a host grants it that capability.

### Read-only boundary

The official `@signalapp/sqlcipher` package currently opens files with SQLite's read-write/create flags and does not expose a read-only open option. This server immediately enables and verifies `PRAGMA query_only = ON`, contains no write queries, and disables automatic WAL checkpoints. Those controls prevent SQL writes through this connection, but they are not the same as an OS/file-level read-only handle: SQLite may still open or manage `-wal`/`-shm` sidecar state. A truly file-level read-only open requires an upstream change or maintained fork of the native addon.

## Data returned

Message results can contain:

- Signal message and conversation identifiers
- millisecond timestamps
- incoming/outgoing direction
- sender service ID and display name (phone numbers are not used as name fallbacks)
- message body
- denormalized quoted-reply text
- attachment metadata and local-download availability

Tool responses use compact JSON. Fields whose value would be `null`, empty, or a default are omitted when their absence is unambiguous. Message `rowId` values are internal and never returned. Conversation-scoped tools omit each message's redundant `conversationId`; global search results retain it so matches can be attributed to a conversation. Conversation types are normalized to `direct` or `group`.

This milestone does not decrypt or return attachment bytes, create a durable archive, perform semantic search, or send messages. Disappearing messages that Signal has already deleted cannot be recovered.

View-once attachment details are intentionally reduced to `{ "isViewOnce": true }`. Filenames, media types, sizes, and download state are not returned for them.

## Pagination and ordering

Conversation lists and message tools return `hasMore`. For conversations, increase `limit` up to the configured maximum when `hasMore` is true; conversations beyond that cap are not returned. Message tools return chronological rows and, when applicable, `sizeTruncated: true` plus an opaque `nextCursor`; absent values mean the result was not size-truncated and there is no next cursor. Pass `nextCursor` back as `cursor` with the same `direction`:

- `direction: "older"` starts at the newest matching page and walks backward.
- `direction: "newer"` starts at the oldest row after the supplied time bound and walks forward.

`before` and `after` are exclusive millisecond bounds on Signal's receive-order timestamp (falling back to the sent timestamp for legacy nulls). Composite receive-time/row-ID cursors prevent messages with tied timestamps from being skipped. Results are rendered chronologically in either direction.

Global substring search scans Signal's message table and may be slower than conversation-scoped search on large histories.

## Development

```powershell
npm test
npm run check
npm run build
```

The test suite uses fake MCP readers and temporary encrypted SQLCipher fixture databases. It does not access your Signal data. A manual local smoke test is still required to detect changes in Signal's private database schema.

Operational logging goes to stderr because stdout is reserved for MCP JSON-RPC traffic.

## Security

Please do not open public issues containing message text, database files, keys, service IDs, phone numbers, or local paths. See [SECURITY.md](SECURITY.md) for private vulnerability reporting guidance.

## License

The project source is MIT licensed. Dependencies retain their own licenses; notably, `@signalapp/sqlcipher` is distributed under AGPL-3.0-only.