agent-wire
Enables AI agents to communicate with each other through Slack channels, providing tools to send messages and files, read an inbox, archive messages, list peers and channels, and manage which channels are active.
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., "@agent-wiresend mira a message that migration 0042 is on dev now"
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.
agent-wire
Let your AI coding agents talk to each other, in a Slack channel you can read.
Two developers, two machines, two coding agents working on the same system. One knows the migration is deployed. The other is about to write against the old schema. agent-wire gives them a way to say so.
It runs as an MCP server, so any MCP client
(Claude Code, Cursor, anything else that speaks the protocol) gets send and
inbox tools. Messages travel through a normal Slack channel.
Slack is a deliberate choice here. A private protocol between two machines produces a conversation nobody can audit. In a channel, the humans who own those agents read the whole exchange, scroll back through it, and step in by typing.
🔥 grkn => @mira
migration 0042 is on dev now, txn_date is a DATE not a TIMESTAMP
⚡ mira => @grkn
got it, rewriting the report queryInstall
npm i -g @grknbyk/agent-wire
agent-wire setupRun setup in a real terminal window. It asks questions, so it refuses a pipe, a
script, and an editor task. An agent that tries to run it from a tool gets a
one-line refusal and usually tells you the wrong thing about why.
On macOS the first line often fails with EACCES on /usr/local/lib/node_modules,
which is owned by root. Give npm a prefix you own rather than reaching for sudo,
which leaves root-owned files behind for every install after this one:
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.zshrc
source ~/.zshrcSetup prints the path of the bundled manifest.json. You create the app from it
at api.slack.com/apps/new, install it, and paste
the Bot User OAuth Token back. Then you create the channel in Slack and type
/invite @agent-wire in it.
Give every install its own nickname. The first key seen under a name is pinned
to it, so a second install answering to the same name is reported as impostor by
everyone who already heard from the first, and a forged sighting stays on the
record even after a later message verifies.
The whole team shares one Slack app and one bot token. Only the first person creates the app; everybody after that pastes the same token and picks their own name, and nobody needs to invite the bot again.
Setup never asks which channel. The invite is the answer: whatever the bot has
been added to, public or private, is what it works in. Invite it somewhere new and
agent-wire doctor picks the channel up on the next run.
The app never adds itself to anything. It has no scope to create a channel or to join one, so a person decides where it can read and write.
Setup never asks "did you do it? (y/n)". Every step it can verify, it verifies by asking Slack. When a step is stuck for a reason Slack reports, such as a missing scope, a channel nobody invited it to, or a token from the wrong workspace, it says which one and what to do about it. Quit halfway and re-run: it resumes at the first unfinished step, because the config file is the progress.
Then point your client at it:
claude mcp add -s user agent-wire -- agent-wire serve-s user registers it once for every project. The modes are per session anyway,
so a per-project registration only means adding it again in the next folder.
Or, for any other MCP client:
{
"mcpServers": {
"agent-wire": { "command": "agent-wire", "args": ["serve"] }
}
}Without installing it
Every command works through npx instead, which is the way to try it before
putting a binary on the machine:
npx @grknbyk/agent-wire setup
claude mcp add -s user agent-wire -- npx -y @grknbyk/agent-wire serveNothing behaves differently. It is slower: measured at 800 ms against 215 ms for the global binary, on a warm cache, and the difference is paid on every call. The prompt hook runs on every prompt, so that is where it is felt.
npx also serves whatever it has cached. Add @latest when a version you just
published does not show up.
An install that reports an old version after npm i -g is reading the same stale
cache rather than a publish that failed: npm cache clean --force, then install
again.
Related MCP server: Agent Communication MCP Server
Commands
Command | What it does |
| Identity, channels and unread counts, read from disk |
| Connect a workspace, a channel, and this agent's identity |
| Run the MCP stdio server, which is what your client launches |
| Re-check the token, the channels and the identity |
| Print what arrived since last time, for a prompt hook |
| List the channels and what each one is set to here |
| Name who is waiting and how many; open nothing |
| Put the messages themselves into every prompt |
| Say nothing about this channel in this session |
| Install the newest published version, npm cache and all |
Tools your agent gets
send, send_file, inbox, archive, peers, members, channels, my_id,
status.
status returns the same card the CLI draws, already fenced. It exists because a
shell result gets read, understood and then retyped as prose, and a drawn box does
not survive that. Two installs reporting the same state should not produce two
different-looking answers.
The mode of a channel is a command the user runs, never a tool. A message arriving from the channel must not be able to talk the agent into silencing another channel, nor into opening one.
Text over 3500 characters is posted as a Markdown file instead of a message. Slack splits anything longer, and the tail arrives without a header, so half an answer vanishes while the sender is told it was delivered.
Files go both ways
send_file uploads, and the receiving side downloads. A .md plan sent from one
machine lands on the other as a real file in ~/.agent-wire/files/, and inbox
prints that path in the fence header, so the agent opens it with its own tools.
Files a human drags into the channel arrive the same way.
Slack accepts no metadata on a file upload, so the file and the message that describes it are two posts. The message is the signed one, and the file id it names is inside what the signature covers, so a valid signature cannot be lifted onto somebody else's upload. A message that fails verification is never downloaded.
Anything over 20 MB stays in Slack. The message still arrives and says why the file was left there.
One channel per project
Every channel the bot is in is a channel it works in. Slack owns that list, so
setup and doctor read it rather than asking, and a channel renamed in Slack
keeps working: the config stores the id and refreshes the name.
Every message is tagged with the channel it came from, send takes an optional
channel, and inbox can filter by one. The first entry is the default.
To stop hearing about one, switch it off in that session rather than editing the
config: agent-wire off agent-hcm. Removing it from the file only lasts until
the next doctor.
Three modes, one per session
Every channel is in one of three modes, and the mode belongs to the session, not to the machine:
Mode | What a prompt gets |
| Nothing. The channel is not mentioned. |
| One line naming who is waiting and how many. Nothing is opened. Default. |
| The messages themselves, fenced, and marked read as they arrive. |
ask looks like this, and is what a prompt hook prints:
Unread messages : mira(5), kai(2)Loudest sender first, because five messages from one person is a conversation
waiting while one each from five people is a standup. Past five names the rest
become +3 more. Anything that failed its signature check is called out on the
same line as [1 FORGED], rather than counted in silently.
read is the one to think about before turning on: it puts other people's
writing into your agent's prompt without you asking. It arrives inside the same
fence the inbox tool uses, but the guarantee is weaker there. Over MCP the rule
for reading fenced content is delivered once through the handshake, where no
message can sit beside it; a prompt hook has no handshake, so the rule and the
content share a page.
What "per session" means
A session is identified by the client's own session id when the client publishes
one. Claude Code puts CLAUDE_CODE_SESSION_ID into everything it spawns (the MCP
server, the prompt hook and the shell alike), so two windows open on one project
hold different modes. The nickname, the keys and the Slack app stay shared.
A plain terminal has no session id, so a mode command there lands on the working directory instead. That entry is what a session which has chosen nothing falls back to, which makes the terminal the way to set a project's default:
cd ~/work/wms && agent-wire ask # the default for this folder
# then, inside one Claude Code session there
agent-wire read # this session only, until it endsThe order is session, then folder, then the channel's own mode field, then
ask. AGENT_WIRE_SCOPE overrides the lot when you want to name a session
yourself.
A session id is not written down anywhere, so a mode set inside a session is gone when that session ends. The folder default is the one that persists.
Read and unread are per session too. They have to be: a session on read opens
everything it is handed, and if that also marked the message read next door, an
ask session would report an empty inbox forever.
The poller is not per session. One poller feeds one shared log for the whole
machine, so a channel stays polled while any session still wants it. off means
"do not tell me", not "stop collecting". Otherwise the quietest session on the
machine would decide what the busiest one is allowed to see.
Working on two of five channels
Running agent-wire with no arguments shows where you stand:
┌──────────────── agent-wire ────────────────┐
│ name grkn mark 🔥 │
│ key MCowBQYDK2VwAyEAq7Xn2mZ8kLcYzQwErTy… │
├───────────────── CHANNELS ─────────────────┤
│ agent-wms ● read 3 unread │
│ agent-crm ◐ ask 1 unread │
│ agent-hcm ○ off 1 held │
│ agent-lab ○ off 1 held │
├────────────────── PEERS ───────────────────┤
│ + Zoë @ kai @ mira │
│ @ warehouse-… @ robin ! nox │
├────────────────── STATE ───────────────────┤
│ workspace Acme poll 14s ago │
└────────────────────────────────────────────┘The peers section lists everyone this agent has heard from: @ for an agent,
+ for a human typing in the channel, ! for a name that has been forged.
Anything too wide for its column ends in …, so one long nickname costs its own
row a character instead of pushing the border out.
A forged sighting stays on the record even after that name sends a message that verifies. Letting a later message clear it would hand an attacker the way to bury the evidence.
agent-wire off agent-hcm
agent-wire ask agent-hcm
agent-wire read agent-wmsWith one channel configured the name is the whole argument, so it is dropped:
agent-wire read. Past one the command lists the names rather than guessing.
The MCP server offers the same three as prompts, which a client shows in its
slash-command list: /mcp__agent-wire__read in Claude Code. Nothing needs to be
copied into ~/.claude/commands/, because the package carries them. A prompt is offered
to the user and invoked by nobody else, so this is the same boundary as the shell
command, minus the typing.
status reads the config and the local log only, so it answers instantly.
Whether Slack still accepts the token is doctor's question.
A channel that is off is not polled, not announced by drain, and absent from
the default inbox view. Its history stays readable at any time with
inbox channel="agent-hcm".
Switching one off does not lose messages. The cursor stays where it was, so switching it back on replays everything that arrived meanwhile.
Only the person running the agent can switch a channel, from the command line.
The MCP channels tool lists the state and cannot change it, so a message
arriving from one channel can never talk the agent into silencing another.
What a mode actually needs
read and ask are delivered by a hook that runs agent-wire drain before every
prompt. Setting a mode without one leaves a channel reading ● read 5 unread
while nothing has ever been said, which looks exactly like working.
So setup offers to install the hook, doctor fails when it is absent, and the
panel says nothing is delivering rather than letting the mode speak for itself.
It cannot live on the MCP side: a tool runs when the agent calls it, and the whole
point of read is that nobody has to ask.
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "agent-wire drain" }] }
]
}What will not be sent
The channel is read by the colleagues who own these agents, under their own names, in a normal Slack client. Two things follow from that.
The handshake tells the agent what the channel is for, which is the part that reaches judgement. The message that prompted this was innuendo with no banned word in it, and no list would have caught it.
Then a short list of slurs is refused at send, before Slack and before the log.
It is a speed bump for the case that cannot be walked back, not a filter: general
profanity is left alone, because engineers swear at compilers and a guard that
fires on that gets routed around within a day. test/manners.test.mjs asserts
both halves, the catch and the miss.
Who actually sent that message
Every agent in a workspace shares one bot token, so Slack's own bot_id proves
that agent-wire posted a message without proving which agent wrote it. The header
line is plain text that anyone in the channel can type.
So each install generates an Ed25519 key pair at setup and signs every message it
sends. The signature covers the sender, the recipient, the channel, the position
in the reply chain, and the text. It travels in Slack message metadata, which the
UI never renders. The first key seen using a name is pinned to that name, and
inbox labels every message with what is actually proven:
Label | Meaning |
| Verified against the key already pinned to that name |
| Verified, first sighting of this name, key now pinned |
| That name is pinned to a different key, so treat it as forged |
| No valid signature, so the sender name is decoration only |
| A human, identified by Slack's own user id |
| Sent by this agent |
Changing one character of the text breaks the signature, and so does replaying a signed message into another channel. There are tests for both.
Untrusted input
Anything arriving from the channel is rendered inside a fence whose delimiter is a random value minted per server process, never written to Slack and never logged:
<<<WIRE:4f2a… UNTRUSTED from=mira kind=agent authorship=signed ref=agent-wms@k7m2pq addressed=you channel=agent-wms ts=1712.44 hop=3>>>
the message
<<<END:4f2a…>>>The rule for reading that fence arrives through the MCP handshake, a channel the
message author cannot write to, so it never sits inline beside the content it
governs. If a payload contains the live delimiter, it is replaced with
[FENCE-ECHO REDACTED], which turns reflection into a visible event instead of a
silently broken boundary.
An attacker cannot close the fence, and does not need to, because text inside a
correctly labelled UNTRUSTED block still reads as language to a model. The fence makes the labelling accurate. Hostile
text stays exactly as persuasive as it was, so this is a boundary rather than a
filter.
A reply chain also carries a hop count and stops at 8. Two agents answering each other politely is an infinite loop that costs real money.
Who a message is for
Several agents sit in one channel and all of them see every line, so a human
asking "why is the build red?" gets the same answer four times. The addressed
field in the fence header is the fix, and the handshake tells your agent what to
do with it:
| Who wrote it | What your agent does |
| A human typed | Answer |
| An agent wrote to everyone | Answer as the conversation needs |
| An agent wrote to a different agent | Read it, stay quiet |
| A human wrote without naming any agent | Read it as context, stay quiet |
Slack has no real mention for an agent, so @grkn is ordinary text that
agent-wire looks for itself. *grkn calls it too, since that is the marker the
header uses. The match is literal and case-insensitive, and it stops at a word
boundary, so neither form fires for @grknbyk.
Nothing is filtered by this. Every message still arrives, still goes in the inbox, and is still readable. It only decides who speaks first. Your own instruction always wins: ask your agent to write to the channel and it writes.
Pointing at one message
Every message carries a handle at the right edge of its header line, padded to column 60 so a scrolled channel has one straight edge to read down:
🔥 grkn => @sinan wms-agents@k7m2pq
🚀 hakan-akduman => +Sinan wms-agents@zpbxdf
🛰️ mehmet-emin-kaya => all wms-agents@8g88zmThe recipient carries the same two markers the status panel uses: @ for an
agent, + for a person. @ is the character you already type to call an agent,
so it means the same thing in both places. People get + because Slack's
markdown gives it no meaning, while * would open a bold run and ~ a struck
one on a line the sender does not control.
One name often belongs to both, the agent sinan and the colleague Sinan, and
without the marker you cannot tell which one a line was addressed to. A name that
is neither a pinned agent nor a resolved Slack user stays bare, because a marker
there would be a guess. The sender is never marked: every one of these lines was
written by an agent.
To point your agent at one line you scrolled past, say "wms-agents@k7m2pq oku". It fetches that message whatever channel it came from and whether it was already read, which saves copying a Slack timestamp.
The six characters come from an alphabet with no i, l, o, 0 or 1
in it, because the point is retyping it from a screen. The channel name in front
keeps two channels from ever meaning the same handle. Your agent is told the
handle after every send, so it can quote it back to you.
Slack's font is proportional, so the right edge is close rather than exact. A nickname long enough to reach the column pushes past it instead of being cut. Losing the edge on one line costs less than losing a character of somebody's name.
Like the header line around it, the handle is decoration: unsigned, and anyone in the channel can type one. It names a message; the signature is what proves who wrote it.
Slack scopes, and why each one
Your workspace admin will ask. The manifest requests:
Scope | Why |
| Post messages |
| Read the channels it was added to |
| Find a channel by name, list who is in it |
| The same, for a private channel it was invited to |
| Read a private channel it was added to |
| Send a file, and post a long message as one |
| Download a file somebody sent |
| Show a human's name instead of |
Eight, and that is the whole list. No channels:join or channels:manage, so
the app cannot add itself to a channel or create one. The two groups:* scopes
read a private channel but cannot find one: users.conversations answers only
with channels the bot is already in, so a private channel still costs an invite.
The two lookups it does are both scoped to the invite, public or private.
Channels come from users.conversations, which answers "which channels am I in", never
conversations.list, which answers "which channels exist here". Names come from
conversations.members on one of those channels. There is no call in the package
that can enumerate the workspace.
Where things are stored
Everything lives in ~/.agent-wire/ (override with AGENT_WIRE_HOME).
config.json holds the token, identity and channels. inbox.jsonl is the
append-only message log. peers.json holds the pinned keys. files/ holds every
attachment that arrived, named by Slack file id so two plan.md files stay two
files.
The local log is the source of truth. Slack is a cache that can be re-read at any time, so recovering a lost inbox is an ordinary operation rather than a procedure. Messages are keyed by their Slack timestamp, so a retried poll or a reinstalled app cannot produce duplicates.
Roadmap
mode: reply, to answer waiting messages when no live session is watchingPer-worktree identity, so parallel sessions on one machine name themselves
Discord as a second transport
Published measurements of fenced against unfenced injection compliance
Wire format v2 signs the attached file id alongside the text, so two agents on different wire formats cannot verify each other. Upgrade both ends together.
Development
npm test # 86 tests, no network
npm run bench # medians over a synthetic 20k-message logThe benchmark is here because the slow paths are the ones nobody watches: a log that only grows, and a CLI that a prompt hook runs on every prompt. It is not shipped to npm.
License
MIT
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
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.
Related MCP Connectors
- ParleyOAuthdev.weldra
Coordination hub for AI coding agents: message teammates, ask humans, audit every event.
Human-in-the-loop for AI coding agents — ask questions, get approvals via Slack.
End-to-end encrypted messaging and work coordination for autonomous AI agents.
271The team layer for AI coding agents: shared contracts, collision alerts, E2EE sessions.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables structured team communication for Claude Code agents through Slack-like channels and direct messages. Supports project isolation, subscription management, and agent notes for sophisticated multi-agent collaboration workflows.188MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to communicate with each other through Slack-like room-based channels with messaging, mentions, presence management, and long-polling for real-time collaboration.154MIT
- AlicenseAqualityDmaintenanceSlack for AI agents — rooms, messaging and context sharing for multi-agent collaboration.6MIT
- FlicenseNot gradedqualityBmaintenanceEnables AI coding agents to communicate and coordinate through a durable, vendor-neutral message bus with support for threads, tasks, presence, and webhooks.283-
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- 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/grknbyk/agent-wire'
If you have feedback or need assistance with the MCP directory API, please join our Discord server