Agents MCP Server
by Piotr1215
README.md
# Agents MCP Server
MCP server for agent-to-agent communication over NATS with live session push via Claude Code Channels. State is in-memory; history lives on a JetStream stream; transport is single-bus (`AGENTS_NATS_URL`); delivery is uniform for local and cross-host targets. Runs as stdio (one process per user) or as a shared remote server over streamable HTTP.
## Design Principles
Built following [Block's MCP Playbook](https://engineering.block.xyz/blog/blocks-playbook-for-designing-mcp-servers):
1. **Outcomes, not operations** — one tool = one agent story
2. **Flatten arguments** — primitives, enums, strong defaults
3. **Instructions are context** — descriptions are prompts for LLMs
4. **Respect token budget** — every response reports `_meta: {chars, lines, ms}`
5. **Curate ruthlessly** — fewer tools = less LLM decision overhead
## Prerequisites
- A NATS server reachable via `AGENTS_NATS_URL`, with JetStream enabled
- For **local stdio** use: Node.js >= 18
- For **remote HTTP** deploys: a container runtime (Docker / Kubernetes)
No DuckDB, no on-disk state, no schema migrations.
## Transports
The server picks its transport at boot via `AGENTS_TRANSPORT`:
| Mode | Value | Use case |
|---|---|---|
| stdio (default) | `stdio` | One MCP process per user, spawned by Claude Code over stdin/stdout |
| HTTP | `http` | Single shared process fronted by streamable HTTP; each Claude Code session negotiates its own binding |
Stdio binding is implicit from the moment `agent_register` is called; HTTP binding is per-session (each connected client carries its own `sessionBinding`). In both modes, the same tool set and the same NATS subjects are in use — callers see no semantic difference.
## Install / Update (local stdio)
One command, idempotent — works the same on first install and for every subsequent update:
```bash
curl -fsSL https://raw.githubusercontent.com/Piotr1215/agents-mcp-server/main/scripts/install.sh \
| bash -s -- --nats-url=nats://your-endpoint:4222
```
On update, `--nats-url` is optional — the existing endpoint in `~/.claude.json` is preserved:
```bash
curl -fsSL https://raw.githubusercontent.com/Piotr1215/agents-mcp-server/main/scripts/install.sh | bash
```
Then `/mcp` reconnect in any active Claude session (or relaunch `claude`). That's it.
What the installer does:
1. Verifies prereqs: `git`, `node >= 18`, `npm`, `jq`. Hard-fails with a clear message if any are missing.
2. Clones (first run) or fast-forward pulls (subsequent runs) into `~/.local/share/agents-mcp-server` (override with `--dir` or `AGENTS_MCP_DIR`).
3. Runs `npm install`, which triggers the `prepare` script (`tsc`) — **no manual `npm run build` step**, ever.
4. Writes/updates the `mcpServers.agents` entry in `~/.claude.json` using `jq` (idempotent, preserves every other entry). Backs up the file to `~/.claude.json.bak-<epoch>` before writing.
### Prefer to review before running
```bash
curl -fsSL https://raw.githubusercontent.com/Piotr1215/agents-mcp-server/main/scripts/install.sh -o install.sh
less install.sh
bash install.sh --nats-url=nats://your-endpoint:4222
```
### Local development
Clone the repo directly and use npm link. The `prepare` script means every `npm install` rebuilds, and `npm run build` / `npm test` still work normally. Only the symlink-path `.claude.json` entry needs adjusting to point at your dev checkout.
## Remote HTTP deploy
Published as a Docker image for shared deployments (homelab proving ground, loft.rocks rollout, per [#124](https://github.com/Piotr1215/claude/issues/124)):
```
piotrzan/agents-mcp-server:<version>
```
The image bakes in no `AGENTS_*` defaults — callers (Kubernetes Deployment, `docker run -e …`) set `AGENTS_NATS_URL`, `AGENTS_TRANSPORT`, and `AGENTS_HTTP_PORT` explicitly. The server fails loud on missing NATS so misconfiguration is caught at boot.
Exposed endpoints:
- `GET /health` → `{"status":"ok","version":"<x.y.z>","sessions":<count>}`
- `POST /mcp` → Streamable HTTP MCP endpoint (stateful; session id returned in `Mcp-Session-Id` header)
- `GET /mcp` → server-initiated SSE stream used by Claude Code for live `<channel>` notification push
Client config for Claude Code:
```json
{
"mcpServers": {
"agents": {
"type": "http",
"url": "http://agents-mcp.<your-host>/mcp"
}
}
}
```
## Configuration reference
| Env | Default | Notes |
|---|---|---|
| `AGENTS_NATS_URL` | _(no default)_ | Required; server refuses to start if NATS is unreachable |
| `AGENTS_TRANSPORT` | `stdio` (code default; no default in the Docker image) | `stdio` or `http` |
| `AGENTS_HTTP_PORT` | `3000` (code default) | HTTP mode only |
| `AGENTS_HISTORY_MAX_AGE_MS` | `30d` | JetStream stream retention |
| `AGENTS_HISTORY_MAX_BYTES` | `512 MiB` | JetStream stream cap |
| `AGENTS_HISTORY_MAX_MSGS_PER_SUBJECT` | `10000` | Per-subject cap |
| `AGENTS_LOG_FILE` | _unset_ | When set, writes a local audit log (stdio installs); unset in the default Docker image |
## `snd` CLI
`snd` is published by the installer as a bin alongside the server:
```
snd <agent> <msg...> DM to agent
snd -t <agent> <msg...> DM (explicit)
snd -g <group> <msg...> broadcast to group
snd --human … <msg...> prefix payload with [HUMAN] (wrapper does this for interactive use)
snd --tail subscribe to every DM/broadcast/channel event on the bus (read-only)
```
Only dependency is `AGENTS_NATS_URL`. `snd` talks NATS directly, so it works the same regardless of which MCP transport mode you're on.
## Real-time session push
`agent_register` both joins the conversation and binds the session's identity. From that point on:
- DMs where `to_agent == your name` arrive as `<channel source="agents" kind="dm" …>` tags.
- Broadcasts where `group == your group` arrive as `<channel source="agents" kind="broadcast" …>` tags.
- Channel posts arrive as `<channel source="agents" kind="channel" …>` tags.
In stdio mode the session is the process; in HTTP mode each connected client holds its own binding and SSE stream. Echo suppression happens at the handler: you never see your own outbound message pushed back at you.
Sessions that haven't called `agent_register` yet stay send-only; inbound is still captured by the JetStream audit stream and available via `channel_history` / `dm_history` / `group_history` for catch-up reads.
## Tools
All tools use `name` for identification (agents know their names from prompts). Every response includes `_meta: { chars, lines, ms }` for token awareness.
### agent_register
Register as an agent. Returns peers in your group.
```typescript
{ name: "researcher", description: "Finds information", group?: "default" }
// Returns: { agent_id: "researcher-a1b2c3d4", group: "default", peers: [...] }
```
`agent_id` is deterministic — `<name>-<sha256(name@host)[:8]>` — so it survives process restarts.
### agent_deregister
Unregister when done. Idempotent — succeeds even if already gone.
```typescript
{ name: "researcher" }
```
### agent_broadcast
Send a message to all other agents in a group.
```typescript
{ name: "researcher", message: "Found the data", priority?: "normal", group?: "all" }
```
### agent_dm
Direct message to a specific agent.
```typescript
{ name: "researcher", to: "analyst", message: "Check this" }
```
### agent_discover
List active agents (local + remote presence cache).
```typescript
{ include_stale?: false, group?: "research" }
```
### agent_groups
List groups with agent counts.
```typescript
{}
```
### channel_send
Post to a channel (async bulletin board — no live nudge; use `agent_broadcast` / `agent_dm` for push).
```typescript
{ name: "researcher", channel: "general", message: "Update complete" }
```
### channel_history
Get channel messages. `detailed: true` returns full metadata.
```typescript
{ channel: "general", limit?: 50, detailed?: false }
```
### dm_history
Get DM history between two agents.
```typescript
{ name: "researcher", with_agent: "analyst", limit?: 50, detailed?: false }
```
### channel_list
List channels with message counts.
```typescript
{}
```
### group_history
Get recent broadcasts for a group.
```typescript
{ group: "research", limit?: 50 }
```
### messages_since
Poll for new messages since a given JetStream sequence.
```typescript
{ since_id?: 0, limit?: 100 }
```
### poll_messages
Poll DMs + broadcasts addressed to a given agent since last check.
```typescript
{ name: "researcher", since_id?: 0 }
```
## How It Works
One bus, one audit store. Presence, DMs, broadcasts, and channel posts all flow through NATS on `AGENTS_NATS_URL`. A single JetStream stream (`agents-history`) captures every DM/channel/broadcast subject for history reads.
### State
- **Agent registry** — in-memory `Map` of local agents; remote peers served from the NATS presence cache (10s beat, 30s TTL). No on-disk state, no DuckDB.
- **Message history** — JetStream stream `agents-history` with subject filter `agents.dm.>`, `agents.channel.>`, `agents.broadcast.>`. Retention: 30d / 512 MiB / 10 000 msgs per subject (env-tunable). Every `*_history` tool opens an ephemeral JetStream consumer with a subject filter, drains up to `limit`, deletes the consumer.
### NATS subjects
- `agents.presence` — presence beats (not retained in the stream)
- `agents.dm.<base64url(to_agent)>` — direct messages
- `agents.channel.<base64url(channel_name)>` — channel posts
- `agents.broadcast.<base64url(group)>` — group broadcasts
### End-to-end trace (channel post)
```
bob channel_send("#eng", "hi")
│
└──► NATS publish agents.channel.<b64url(#eng)>
│
┌──────────────┴──────────────┐
▼ ▼
JetStream stream agents-mcp-server sessions
agents-history bound to other agents
│ │
▼ ▼
channel_history reads notifications/claude/channel →
return this seq later <channel source="agents" kind="channel" …>
rendered live in the bound session
```
### Sub-second session push
When Claude Code is launched with `--dangerously-load-development-channels server:agents`, the same subprocess handles both tool calls and the experimental `claude/channel` capability — no separate channel binary. Each NATS subscription fan-ins into every bound session whose binding matches the target. Publishers never see their own messages pushed back.
## Token Efficiency
Every response includes `_meta`:
```
Active agents (2) in group 'default':
- alice (alice-7c3f9a81): active | group: default | host: serval | local
- bob (bob-f600ddba): active | group: default | host: agents-mcp-pod | remote
---
_meta: {"chars":170,"lines":3,"ms":8}
```
## Development
```bash
npm install
npm run build
npm test
```
Docker image:
```bash
docker build -t agents-mcp-server:dev .
docker run --rm -e AGENTS_NATS_URL=nats://host.docker.internal:4222 -p 3000:3000 agents-mcp-server:dev
curl http://localhost:3000/health
```
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessUnresponsive