a1 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited a1 (MCP Server) and scored it 92/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 2 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 2 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
An MCP channel server that bridges NATS pub/sub into Claude Code sessions.
Publish a message to a NATS subject → it arrives in Claude as a <channel> tag → Claude acts on it and can publish back to any subject using the built-in reply tool.
This is the primitive for wiring Claude agents together via NATS.
you → nats pub → NATS → channel-server (MCP subprocess) → <channel> tag → Claude acts
↓
you ← nats sub ← NATS ← channel-server ← reply(to, text) tool call ←─────────┘Install these before anything else:
| Tool | Version | Install | |
|---|---|---|---|
| Bun | ≥ 1.0 | `curl -fsSL https://bun.sh/install \ | bash` |
| NATS server | any | download binary | |
| Claude Code | ≥ 2.1.80 | npm install -g @anthropic-ai/claude-code | |
| nats CLI (optional) | any | for testing/publishing |
Claude Code must be authenticated with a claude.ai account (claude auth login). This uses OAuth — not an API key.
git clone <this-repo>
cd nats-channel-mcp
bun installThe `--instructions` string is your agent's standing orders. It is injected directly into Claude's system prompt. It tells Claude:
<channel> messages meanGet this right and everything else is plumbing.
Create a markdown file with the agent's instructions:
# AGENT.md
You are **Aria**, a code reviewer specializing in TypeScript.
When you receive a <channel> message:
1. Read the code or diff provided
2. Write a concise review (3-5 bullet points max)
3. Use the reply tool to publish your review to `reviews.done`
Be direct. No preamble.Pass it with --instructions-file:
"args": ["--instructions-file", "./AGENT.md", "--name", "aria", "--subscribe", "agents.aria"]For simple agents, inline in .mcp.json:
"args": ["--instructions", "You are aria. Review code in <channel> messages and reply to reviews.done.", "--name", "aria", "--subscribe", "agents.aria"]⚠️ Long inline instructions in JSON are hard to read and edit. Use --instructions-file for anything real.
1. Start NATS:
nats-server2. Create your agent directory:
my-agent/
AGENT.md ← your instructions (the important part)
.mcp.json ← MCP server config3. Write your instructions (AGENT.md):
You are my-agent. When you receive a <channel> message, respond to it
and use the reply tool to publish your response to `agents.done`.4. Configure the MCP server (.mcp.json):
{
"mcpServers": {
"nats": {
"command": "bun",
"args": [
"/absolute/path/to/claude-nats-channel/channel-server.ts",
"--name", "my-agent",
"--subscribe", "agents.my-agent",
"--instructions-file", "./AGENT.md"
]
}
}
}Note: The path to channel-server.ts must be absolute.5. Start Claude with channel support:
cd my-agent
claude --mcp-config .mcp.json --dangerously-load-development-channels server:natsYou'll see a one-time confirmation prompt — choose option 1 to proceed. After that:
Listening for channel messages from: server:nats6. Send a message:
nats pub agents.my-agent "Please respond."Claude receives it, acts on it, and can reply via the reply tool.
bun channel-server.ts [options]| Flag | Required | Description |
|---|---|---|
--name <name> | ✅ | Agent name. Used in channel source attribute and default instructions. |
--subscribe <subject> | ★ | NATS subject to subscribe to. Repeatable: --subscribe a --subscribe b. |
--topics-file <path> | ★ | Path to a topics file (one subject per line, # = comment). Use this for anything beyond one subject. |
--instructions-file <path> | ☆ | Path to a markdown file whose contents become the MCP instructions (system prompt). Recommended. |
--instructions <string> | ☆ | Inline instructions string. Falls back to a minimal default if neither is given. |
--nats <url> | — | NATS server URL. Default: nats://localhost:4222 |
--control-socket <path> | — | Enable the Unix-socket control channel for hot subscription management. See Control Socket below. |
--allow-self-echo | — | Disable self-echo suppression. By default the server stamps an x-from: <name> header on every publish and drops any inbound message whose x-from matches its own name, so agents never see their own messages bounce back on shared subjects. |
--jetstream | — | Enable JetStream-backed durable delivery and the replay MCP tool. See JetStream Mode below. Requires nats-server -js. |
★ At least one of --subscribe or --topics-file is required. ☆ At least one of --instructions-file or --instructions is strongly recommended.
Long-running agents often need to join and leave channels without restarting. Pass --control-socket <path> and the server will listen on a Unix-domain socket that accepts newline-delimited JSON commands:
{"action": "subscribe", "subject": "rooms.breakout-42"}
{"action": "unsubscribe", "subject": "rooms.breakout-42"}
{"action": "delete-durable", "subject": "rooms.breakout-42"}delete-durable is only meaningful when --jetstream is on. It unsubscribes and removes the durable consumer for the subject, scoped to durables this server created.
This is intended for orchestrators (session managers, dashboards) that coordinate a fleet of agents and need to attach or detach channels at runtime. The socket is created on startup and unlinked on SIGTERM / SIGINT. Any existing file at the path is unlinked first.
Example:
# Start an agent with a control socket
bun channel-server.ts \
--name aria \
--subscribe agents.aria \
--control-socket /tmp/nats-ctrl-aria.sock
# From another process, hot-subscribe
echo '{"action":"subscribe","subject":"rooms.breakout-42"}' \
| nc -U -q 1 /tmp/nats-ctrl-aria.sockThe flag is purely opt-in: if you don't pass --control-socket, no socket is created and behavior is unchanged from prior versions. Errors in the control channel never affect NATS or MCP message flow — malformed JSON, unknown actions, and client disconnects are logged to stderr and the server keeps running.
Security: the socket is created with the filesystem permissions of the user running the server. Use a directory only that user can reach (e.g. $XDG_RUNTIME_DIR) if you need stronger isolation. The protocol has no authentication — anyone who can connect() to the path can mutate the subscription list.
Pass --jetstream and the server uses NATS JetStream for durable delivery instead of core pub/sub. This fixes the main fragility of the default mode: messages published while the subscriber is down are buffered and delivered on reconnect.
Requires nats-server to be running with JetStream enabled (nats-server -js).
What changes:
--subscribe SUBJ creates a durable consumer named ${name}__${slug(SUBJ)}. On reconnect (network blip, process restart, session resume) the consumer resumes from its last-acked sequence — messages buffered while the channel-server was offline get delivered automatically. A freshly-created durable starts at "now" (DeliverPolicy=New), matching the empty-inbox expectation of a freshly-spawned agent; DeliverPolicy is frozen at consumer creation, so subsequent rebinds always resume from last-acked regardless.channel-mcp on startup if it doesn't exist, with 3h retention and file storage. Subjects are the union of every --subscribe arg any channel-server in the fleet has seen; the subject set only grows, and retention/storage is never modified after creation. To change retention, use nats stream edit channel-mcp out of band.replay(subject, since?, limit?) to fetch historical messages. Results land as tool output, not as a channel injection, so they don't double-fire into the live inbox. The response is a JSON array of {subject, from, ts, seq, text} per message. seq is the NATS stream sequence — useful if an agent wants to dedupe across replay/live overlap.delete-durable for any name outside that set; on restart the set is lost, but the consumer's own InactiveThreshold reaps anything that leaks.Limitations:
InactiveThreshold is 1h (hardcoded). If a subscriber is gone for longer, JetStream deletes the durable; the next bind starts at "now" and messages published during the gap are skipped. Raise the constant in channel-server.ts if you need to survive longer pauses.process.stdout.write() accepts the buffer for the MCP notification. If Claude Code crashes between pipe receive and consumption, the ack has already fired and the message is lost to that session. This is an MCP-protocol limitation, not a JetStream one.Example:
# Start NATS with JetStream
nats-server -js
# Start the channel-server in JetStream mode
bun channel-server.ts \
--name aria \
--subscribe agents.aria \
--jetstreamreply ToolClaude uses this to publish back to NATS:
reply(to: "<nats-subject>", text: "<message>")Your instructions should tell Claude exactly which subject to publish to and when. Example:
"When you finish your analysis, use reply(to='pipeline.done', text='<your summary>')."
On first use, Claude will ask for permission. Choose "Yes, and don't ask again" to suppress future prompts for that session.
<channel source="nats" subject="agents.my-agent">
the message content here
</channel>The subject attribute shows which subscription delivered the message — useful when an agent subscribes to multiple subjects.
The second thing worth getting right (after instructions) is which subjects your agent subscribes to.
"args": ["--subscribe", "agents.my-agent", ...]Create topics.txt alongside AGENT.md:
# topics.txt — one subject per line, # = comment, blank lines ignored
# Direct (messages specifically for this agent)
agents.my-agent
# Team channel (shared with other agents on the same task)
myapp.project-01.epic-xyz.task-abc.*
# Epic-level broadcast
myapp.project-01.epic-xyz.>
# Workspace-wide announcements
myapp.>
# Breakout rooms (add/remove as needed)
# myapp.breakout.sprint-planningPass it with --topics-file:
"args": ["--topics-file", "./topics.txt", ...]NATS wildcards work as you'd expect:
| Pattern | Matches |
|---|---|
agents.my-agent | Exactly that subject |
agents.* | Any single token after agents. |
agents.> | Any subject starting with agents. (including nested) |
A message published to agents.team is received by any agent subscribed to agents.team, agents.*, or agents.>.
When a message arrives via a wildcard subscription, the <channel> tag shows the actual subject it was published to:
<channel source="nats" subject="agents.team">
broadcast to the whole team
</channel>Your instructions can tell Claude to behave differently based on which subject a message came from.
Each agent subscribes to its own subject. You "introduce" agents by telling each one about the next step in their instructions:
Agent 1 (`AGENT.md`):
You are step-1. When you receive a task in a <channel> message:
1. Process it
2. Use reply(to="agents.step-2", text="<your output>") to pass it forwardAgent 2 (`AGENT.md`):
You are step-2. When you receive input in a <channel> message:
1. Build on it
2. Use reply(to="pipeline.done", text="<final output>") when completeStart both agents before dispatching. Messages published before an agent is subscribed are lost (fire-and-forget). For durability, use NATS JetStream.
See examples/intro-chain/ for a working end-to-end example.
| Issue | Details | |
|---|---|---|
| Tool approval prompts | Claude asks permission before calling reply. Choose "don't ask again" to suppress. In sandboxed environments use --dangerously-skip-permissions. | |
| Fire-and-forget by default | Without --jetstream, no subscriber = lost message. Start subscribers before dispatching, or pass --jetstream for durable delivery (see below). | |
| One-time startup confirmation | --dangerously-load-development-channels prompts once per session. Automate: `echo 1 \ | claude ...` |
| Research preview | Requires Claude Code ≥ v2.1.80. The --dangerously-load-development-channels flag is for local development. Approved channels use --channels plugin:name@marketplace. | |
| Absolute path in `.mcp.json` | The path to channel-server.ts must be absolute — relative paths don't resolve correctly when Claude Code spawns the subprocess. | |
| NATS auth not implemented | --nats only accepts a URL. For authenticated NATS servers, credentials file support (--nats-creds) is on the roadmap. For now: local NATS only. |
The MCP server key in .mcp.json and the --dangerously-load-development-channels server:<key> flag must match exactly. If they don't, Claude starts silently — no channel listener, no error.
{ "mcpServers": { "nats": { ... } } }
// ^^^^
// This must match ──────────────────────────────────────┐claude --dangerously-load-development-channels server:nats
# ^^^^Convention: always use nats as the key name. The examples follow this convention.
If you need a different key name (e.g. running multiple channel servers per session), use a CHANNEL_KEY variable in your launch script so both places stay in sync automatically — see examples/intro-chain/run.sh for the pattern.
┌──────────────────────────────────────────────────────────┐
│ Claude Code session │
│ │
│ System prompt includes: <instructions from AGENT.md> │
│ │
│ Receives: <channel source="nats" subject="agents.x"> │
│ message content │
│ </channel> │
│ │
│ Sends: reply(to="agents.y", text="response") ──────┼──→ NATS
└──────────────────────────────────────────────────────────┘ ↑
↑ notifications/claude/channel (MCP) │
│ │
┌───────────────────────────────┐ │
│ channel-server.ts │ ←── NATS ────────────────────┘
│ (MCP subprocess) │
│ │
│ nc.subscribe(subject) │
│ → mcp.notification() │
│ │
│ reply tool │
│ → nc.publish(to, text) │
└───────────────────────────────┘The channel server runs as a subprocess spawned by Claude Code (via .mcp.json). It owns the NATS connection. Claude Code never touches NATS directly.
--subscribe repeatable for multiple initial subjects--jetstream)--nats-creds)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.