nats-channel-mcp

except-pass/nats-channel-mcp
0 starsMITCommunity

Install to Claude Code

This server doesn't publish a one-line install command. Follow the setup in the source repository.

Summary

Bridges NATS pub/sub into Claude Code sessions, enabling agents to receive and respond to NATS messages as channel tags with a reply tool.

README.md

nats-channel-mcp

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 ←─────────┘

---

Prerequisites

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.

---

Install

git clone <this-repo>
cd nats-channel-mcp
bun install

---

The Most Important Part: Instructions

The --instructions string is your agent's standing orders. It is injected directly into Claude's system prompt. It tells Claude:

  • Who it is (its name, role)
  • What the incoming <channel> messages mean
  • What to do with them
  • Where to send results (which NATS subject to publish to when done)

Get this right and everything else is plumbing.

Option A: Instructions file (recommended)

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: ``json "args": ["--instructions-file", "./AGENT.md", "--name", "aria", "--subscribe", "agents.aria"] ``

Option B: Inline instructions

For simple agents, inline in .mcp.json: ``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.

---

Quick Start

1. Start NATS: ``bash nats-server ``

2. Create your agent directory: `` my-agent/ AGENT.md ← your instructions (the important part) .mcp.json ← MCP server config ``

3. Write your instructions (AGENT.md): ``markdown 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): ``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: ``bash cd my-agent claude --mcp-config .mcp.json --dangerously-load-development-channels server:nats ``

You'll see a one-time confirmation prompt — choose option 1 to proceed. After that: `` Listening for channel messages from: server:nats ``

6. Send a message: ``bash nats pub agents.my-agent "Please respond." ``

Claude receives it, acts on it, and can reply via the reply tool.

---

CLI Reference

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.

---

Control Socket

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

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

---

JetStream Mode

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:

  • Durable per subscription. Each --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.
  • Auto-managed stream. The server creates a stream called 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 MCP tool. A second tool appears on the MCP surface. Claude can call 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 socket action. Orchestrators that manage a fleet of agents can remove a subject's durable immediately via the control socket (see above). Each channel-server tracks the durable names it created in memory and refuses 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:

  • Pauses longer than 1h lose messages. The per-consumer 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.
  • At-least-once. A duplicate delivery is possible after a crash/ack-timeout. If a message triggers a non-idempotent side-effect, the producer should include its own dedup key in the payload; this server is content-blind.
  • msg.ack() is OS-pipe-level. channel-server acks once 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 \
    --jetstream

---

The reply Tool

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

---

How Messages Appear in Claude

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

---

Topics / Subscriptions

The second thing worth getting right (after instructions) is which subjects your agent subscribes to.

Option A: Single subject (simple)

"args": ["--subscribe", "agents.my-agent", ...]

Option B: Topics file (recommended for multi-level setups)

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-planning

Pass it with --topics-file: ``json "args": ["--topics-file", "./topics.txt", ...] ``

Wildcard subjects

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

The channel source attribute

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.

---

Multi-Agent Chains

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): ```markdown 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 forward

**Agent 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 complete

**Start 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/`](./examples/intro-chain/) for a working end-to-end example.

---

## Known Limitations & Gotchas

| 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 Key Name Coupling (important)

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 ──────────────────────────────────────┐ `` ``bash 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.

---

## Architecture

┌──────────────────────────────────────────────────────────┐ │ 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.

---

Roadmap

  • [ ] --subscribe repeatable for multiple initial subjects
  • [ ] Hot subscription management via Unix socket (add/remove without restart)
  • [x] NATS JetStream support for durable delivery (--jetstream)
  • [ ] NATS authentication via credentials file (--nats-creds)

Related MCP servers

Browse all →