signal-tools
Provides coding agents with tools for interacting with a locally linked Signal account: listing and searching chats and reading their messages (including messages around a given one), reading local contacts and checking up to three phone numbers against Signal, saving received attachments to a private download folder (with optional archive inspection and unpacking), sending one text message (optionally quoting) or up to 10 files to a chat, and editing, deleting, or reacting to the user's own messages. It also reports per-account link and sync-service state, and includes destructive/outbound toolsets disabled by default.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
signal-tools
Signal tools for coding agents: chats, messages, contacts, received files and sends.
Harness | Delivery | Pinned by |
OpenCode V2 | native plugin (npm package, with skills) | the version in the package |
OpenCode V1 | native plugin (npm package, no skills) | the version in the package |
Claude Code | plugin: MCP server and skills | the release tag, as the ref |
Codex | plugin: MCP server and skills | the release tag, as the ref |
Grok Build | plugin: MCP server and skills | the release tag, as |
Antigravity | plugin: MCP server and skills | the release tag's tree URL |
Hermes Agent | native plugin (Python, calls the command line) | the release commit, as the SHA |
other MCP host | MCP server from npm | the version in the package |
The tools need Node.js 24 or later.
docs/conventions.md describes how the tools are named
and how they behave.
Install
Install from a release, never from the default branch: a release is a tag whose
commit carries the built dist/, and the default branch does not. In the
commands below, <version> is a release's version (1.2.3) and <sha> is the
commit its tag names:
gh api repos/craft-agent/signal-tools/commits/v<version> --jq .shaOpenCode
OpenCode V2 takes the package and its options in opencode.json:
{
"plugins": [{ "package": "@craftsamo/signal-tools@<version>", "options": { "read_only": false } }],
"permissions": [{ "action": "signal_*", "resource": "*", "effect": "ask" }]
}OpenCode V1 takes the package as a string, or as a pair with its options:
{
"plugin": [["@craftsamo/signal-tools@<version>", { "read_only": false }]],
"permission": { "signal_*": "ask" }
}OpenCode allows any action no rule names, so the plugin's request is granted
silently unless a rule says ask. Read tools never ask, so the signal_* glob
is safe.
V1 registers no skills. Unpack the package and copy its skills/* folders into
~/.config/opencode/skills/:
npm pack @craftsamo/signal-tools@<version>
tar -xzf craftsamo-signal-tools-<version>.tgz
mkdir -p ~/.config/opencode/skills
cp -r package/skills/* ~/.config/opencode/skills/
rm -rf package craftsamo-signal-tools-<version>.tgzClaude Code
claude plugin marketplace add craft-agent/signal-tools#v<version>
claude plugin install signal-tools@craft-agentCodex
codex plugin marketplace add craft-agent/signal-tools --ref v<version>
codex plugin add signal-tools@craft-agentCodex turns - into _ in server names, so it shows the tools as
mcp__signal_tools__signal_*. It starts the MCP server in the plugin's
install directory, which is never the caller's working tree.
Grok Build
grok plugin install craft-agent/signal-tools@v<version> --trustGrok starts the MCP server in the session's working directory.
Antigravity
Install from the tree of the release tag, never from the default branch, which
has no built dist/; Antigravity clones the tag into
~/.gemini/config/plugins/signal-tools.
agy plugin install https://github.com/craft-agent/signal-tools/tree/v<version>Hermes Agent
Hermes installs a commit, not a tag, and the plugin installs disabled:
hermes plugins install craft-agent/signal-tools --ref <sha>
hermes plugins enable signal-toolsIf a Hermes gateway is already running, restart it (hermes gateway restart) to
load the plugin.
Any other MCP host
Add the server to the host's MCP configuration. Flags go in args:
{
"mcpServers": {
"signal-tools": {
"command": "npx",
"args": ["-y", "@craftsamo/signal-tools@<version>", "--read-only"]
}
}
}Related MCP server: telegram-mcp
Tools and skills
Each toolset comes with a skill that tells an agent how to use its tools.
Account (account)
Tool | What it does | Class |
| each label: linked or not, the sync service's state, the last receive and next step | read |
It reads local state only: no daemon, no network, and no number, account id
or key in its result. The skill is
signal-account, shown as
signal-tools:signal-account in Claude Code and signal-tools:account in
Hermes.
Chats (chats)
Tool | What it does | Class |
| by | read |
It reads the account's local mirror only: no daemon, no network, and no number
of the account's own, attachment id or key in its result. History starts when
the device was linked. Times are local with their offset, a chat is always its
id, a long result is paged with next, and everything it returns is data
written by other people. The skill is
signal-chats, shown as
signal-tools:signal-chats in Claude Code and signal-tools:chats in Hermes.
Contacts (contacts)
Tool | What it does | Class |
| lists local contacts or checks up to three numbers with Signal | read |
Listing needs no daemon; checking numbers needs the running sync service.
Neither writes the mirror. The skill is
signal-contacts, shown as
signal-tools:signal-contacts in Claude Code and signal-tools:contacts in
Hermes.
Files (files)
Tool | What it does | Class |
| saves one message's files, and unpacks its archives when asked for | add |
| sends 1 to 10 files (100 MiB together), with a caption, to a chat | add |
The toolset is off by default: it writes to disk, opens archives and sends
files. Turn it on with toolsets or tools. Files are sent only from the
working directory (not when that is the home directory or a file system root)
and the directories the send_dirs setting names; keys, secrets, programs and
installers are refused, archives are checked entry by entry, and a file's
sha256 can be given to be sure the file sent is the one approved. Sending
needs the sync service running. Received files go to the download_dir setting, in a folder
per message, private to the user (mode 700 and 600) and never executable;
nothing is overwritten. Programs and installers are never saved, and an archive
(zip, tar, gzipped tar) is saved only after its contents pass inspection, then
listed; unpack: true unpacks it into <name>.unpacked/ beside it. A
view-once message's files are not saved. On macOS every saved file carries the
quarantine flag. Saving needs no daemon. The skill is
signal-files, shown as
signal-tools:signal-files in Claude Code and signal-tools:files in Hermes.
Sends (sends)
Tool | What it does | Class |
| sends one text of at most 4,000 characters to a chat, optionally quoting a message | add |
A chat is always its id; the user's own account id is Note to Self, and a group
the user left is refused. A result means Signal accepted the message, with
reached and not_reached for a group; any other failure after the request
went out is uncertain: check the phone before sending again. The message is
recorded in the mirror, and a record that fails is listed under errors
without undoing the send. It needs the running sync service. The skill is
signal-sends, shown as
signal-tools:signal-sends in Claude Code and signal-tools:sends in Hermes.
Actions (actions)
Tool | What it does | Class |
| replaces the text of one of the user's own messages | overwrite |
| deletes one of the user's own messages for everyone; it cannot be undone | administrative |
| sets, replaces or removes the user's emoji reaction on a message | overwrite |
The toolset is off by default: the actions change what others have already
read. Turn it on with toolsets or tools. Editing and deleting take only the
user's own messages, within 24 hours of sending, and a message is edited at most
10 times; only a text message can be edited. A reaction goes to any message, one
per person, so a new one replaces the user's earlier one. Outcomes follow the
sends': a result means Signal accepted it, with reached and not_reached for
a group, and any other failure after the request went out is uncertain: check
the phone before repeating it. Each is recorded in the mirror as the sync
service would record it from another device, and a record that fails is listed
under errors. No tool sends read receipts or typing indicators. It needs the
running sync service. The skill is
signal-actions, shown as
signal-tools:signal-actions in Claude Code and signal-tools:actions in
Hermes.
Adding a tool
Write the spec in
src/core/<domain>/<verb>.tswithdefineTool: an ID<prefix>_<domain>_<verb>, a title and description, the Zod argument and output schemas, its toolset and write class, andexecute(args, context). A write tool also givestargets(args, context): what the call writes to, as a permission rule should name it.Test it beside the spec (
<verb>.test.ts), calling it throughinvokeas the entry points do. A tool that runs a program is tested against a fake of that program onPATH; no test reaches a real service.List it in
src/tools.ts. A new toolset goes intotoolsetsinsrc/project.ts(anddefaultToolsetswhen it is on by default).Add or update the toolset's skill,
skills/<prefix>-<toolset>/SKILL.md.Run
pnpm run build && pnpm run manifeststo list the tool inplugin.yaml, then describe it under "Tools and skills".
docs/conventions.md has the rules each step follows.
Reads and writes
Every read tool is named <prefix>_<domain>_read. Reads can run unasked;
writes are confirmed by the harness.
Harness | Reads without asking |
OpenCode | always; the plugin asks only before a write |
Claude Code | the rule |
Codex | always; Codex runs tools that declare |
Grok Build |
|
Antigravity | the rule |
Hermes | always; the plugin asks only before a write |
Where a write is asked about, and where it is not:
OpenCode grants any action no rule names, so a write runs unasked unless a rule asks for it: add the
signal_*rule shown under OpenCode.Codex under
codex exec, and Grok Build undergrok -p, refuse a write they cannot ask about, unless it is allowed beforehand.Grok Build calls the plugin's tools through its
use_tooltool, and the agent often names a tool without its server first. Grok checks the rules against the name as given, so the server-qualified rule alone does not match that first call. The second rule matches the short name; a write still matches neither.Antigravity asks before any MCP tool no rule allows, reads included, and names the plugin's server
signal-tools_signal-tools(plugin, then server).agy -prefuses a tool it cannot ask about.Hermes
hermes chat -qrefuses a write it cannot ask about;hermes -zand--yoloskip approval, so writes then run unasked.
Settings
Every entry point takes the same settings. A flag wins over its environment variable, and an option or setting wins over the variable too.
Setting | Flag | Variable | Meaning |
|
|
| the toolsets that are on, or |
|
|
| registers no tool that writes; wins over every other setting |
|
|
| single tools to add by ID, whatever their toolset |
|
|
| single tools to remove by ID; wins over |
|
|
| MCP server only: |
|
|
| where every label's state lives; default |
|
|
| where received files are saved; default |
|
|
| the directories files may be sent from, besides the working directory |
Lists are comma-separated in a flag or a variable. Where each harness reads them:
OpenCode takes the plugin
optionsshown above, with lists as arrays,read_onlyas a boolean,state_diranddownload_diras paths andsend_dirsas an array of paths, and falls back to the variables. V2 runs plugins in its background service, which keeps the environment it started with: a variable set whereopencode runstarts has no effect until the service is restarted in that environment (opencode service restart). V1 runs plugins in its own process.Hermes takes
plugins.entries.signal-tools.settingsinconfig.yaml:plugins: entries: signal-tools: settings: read_only: false exclude_tools: []The plugin passes its own environment to the command line it runs, so the variables work too. A running gateway keeps the environment it started with; restart it (
hermes gateway restart) after changing one.Claude Code, Grok Build and Antigravity start the plugin's server without flags and pass it the client's environment, so set the variables in the environment the harness runs in.
Codex passes a plugin's MCP server only a few basic variables (such as
PATHandHOME), so the variables have no effect there. To change settings, or to give the server a variable it needs, add the server under[mcp_servers]in Codex'sconfig.tomlinstead of using the plugin: flags go inargs,env_varsnames the variables to forward from Codex's environment, andenvsets fixed values.[mcp_servers.signal-tools] command = "npx" args = ["-y", "@craftsamo/signal-tools@<version>", "--read-only"] env_vars = ["SIGNAL_TOOLS_STATE_DIR"]
Accounts and the sync service
The tools use a Signal account as a linked device of the user's phone, the way Signal Desktop does, through signal-cli, which you install yourself. They never read another client's data.
Each linked device has a label, 1 to 24 lowercase letters, digits and
_(defaultwhen linking names none). A tool'saccountargument picks the label; leave it out when only one device is linked.Each label keeps its state in one directory,
<state_dir>/<label>/(mode 700): signal-cli's data and keys, the daemon's socket, the mirror of received messages, and the copies of files being sent.state_dirdefaults to$XDG_STATE_HOME/signal-tools, or~/.local/state/signal-tools. Keep it out of backups you share: the keys read and send as you.A sync service receives for each label and keeps its mirror, and the read tools read only the mirror. History starts when the device is linked; messages that arrive while the service is down wait on Signal's server, for as long as Signal keeps undelivered messages.
Signal unlinks a device unused for 45 days, and every linked device when the phone has been offline for 30; a running service keeps the device in use. An unlinked label is linked again from the phone.
signal-cli releases age out. A release more than about three months old may stop working with Signal's servers, so keep it updated.
Linking a device and running the sync service
The tools are tested against signal-cli 0.14. Install it first: on macOS
brew install signal-cli; elsewhere a
release (the JVM build needs
Java 25 or later). macOS and Linux are supported; Windows is
#11.
# Link: on the phone, Signal > Settings > Linked devices > Link new device,
# then scan the code this prints.
signal-tools-cli link --account work
# Start receiving at logon, and restart after a failure.
signal-tools-cli service install --account work
signal-tools-cli service status --account work--account may be left out when only one label exists (link names a new
one default), and every command takes --state-dir like the tools.
Command | What it does |
| links this machine as a device of the phone, showing the code to scan; |
| runs the sync service in the foreground, logging to standard error, until it is stopped |
| registers the sync service as a launchd agent (macOS) or a systemd user unit (Linux), removes it, or reports it |
| uninstalls the service and deletes the label's keys, signal-cli's data, history and service log, after the label is typed back |
The service records this package's paths, so run
service installagain after updating the package. On macOS its log is~/Library/Logs/io.github.craft-agent.signal-tools/<label>.log; on Linux,journalctl --user -u <unit>asservice statusshows, andloginctl enable-lingerkeeps it running while you are logged out.A phone takes at most five linked devices, and each label is one of them.
Removing a label does not unlink it on the phone: a linked device cannot take itself off, so remove it under Settings > Linked devices as well.
To link a label again after Signal unlinked it, run
linkagain: the old keys are moved aside and the history is kept, as long as the phone holds the same account. A different account gets a label of its own.signal_account_readtells an agent which of these steps a label needs.
Layout
src/project.ts— the package's name, tool ID prefix, setting prefix and toolsets.src/core/— the tool contract and the tools, free of any harness and of the project's names.src/tools.ts— the list of every tool, which each entry point registers.src/mcp/— the MCP server, built todist/mcp.jsand run bysignal-tools.src/opencode/— the OpenCode plugin, built todist/opencode.js.src/cli/— the command line the Hermes plugin calls, built todist/cli.jsand run bysignal-tools-cli.src/hermes/,plugin.yamland__init__.py— the Hermes plugin.skills/— one skill per toolset,skills/<name>/SKILL.md, which every harness that takes skills loads from the package or plugin root.plugin.json,mcp.json,.mcp.json,.claude-plugin/,.agents/plugins/,.antigravity-plugin/andmcp_config.json— the plugin manifests of the other harnesses. The root.mcp.jsonis the configuration Grok Build reads (with${CLAUDE_PLUGIN_ROOT}, the variable both Grok Build and Claude Code expand); when working on this repository in Claude Code, decline the project MCP server it offers from that file.
How a release is made is in RELEASING.md.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
Messaging tools for AI agents: send messages, manage chats, groups and channels.
Read, search and reply to your WhatsApp chats from your AI, using your own number.
WhatsApp for Claude, ChatGPT or any AI agent: search, reply and send. Personal or Business API.
WhatsApp CRM for AI agents: search contacts, read chats, manage the sales pipeline, send messages.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to read, send, and organize Telegram messages and chats. Supports tools for listing chats, fetching messages, sending/reply, archiving, muting, and folder management.1MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to interact with a user's Telegram account: list chats, read history, search, and send messages through Telegram's MTProto API.2MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to work with Telegram through explicitly selected user accounts and bots, supporting search, reading, sending, and message management with consent-based access.27 PyPI1Apache 2.0
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to search and read locally stored chat history, look up contacts, send messages and attachments, and retrieve media through a self-hosted local daemon with encrypted SQLite storage.MIT