219 lines
8.6 KiB
Markdown
219 lines
8.6 KiB
Markdown
# Relay Server Design
|
|
|
|
**Date:** 2026-05-02
|
|
**Status:** Approved
|
|
**Scope:** Dev tooling — not shipped in any product artifact
|
|
|
|
---
|
|
|
|
## Problem
|
|
|
|
Multi-agent development lifts (PM + Dev-A + Dev-B in parallel Claude Code sessions) require passing status updates, questions, and directives between terminals. Today the user manually copies and pastes every message block. This is error-prone, breaks flow, and scales poorly as lift complexity grows.
|
|
|
|
## Goal
|
|
|
|
A lightweight MCP server running on localhost that gives all three Claude Code sessions native tools to post and read messages. The user stops being the message bus.
|
|
|
|
---
|
|
|
|
## Repository layout
|
|
|
|
```
|
|
tools/relay/
|
|
├── package.json # private, not published; single dep: @modelcontextprotocol/sdk
|
|
├── tsconfig.json
|
|
├── server.ts # MCP SSE server entry point (~150 lines)
|
|
├── queue.ts # in-memory queue logic (~50 lines)
|
|
├── queue.test.ts # Node built-in test runner
|
|
└── start.sh # launcher script
|
|
```
|
|
|
|
Added to root `.gitignore`: `tools/relay/node_modules/`, `tools/relay/dist/`.
|
|
|
|
---
|
|
|
|
## Tech stack
|
|
|
|
- **Runtime:** Node.js (v25, already installed at `/usr/bin/node`)
|
|
- **Package manager:** npm (bun has known compat gaps with the MCP SDK's SSE transport)
|
|
- **Dependencies:** `@modelcontextprotocol/sdk`, `tsx` (devDependency, runs TypeScript directly — no compile step)
|
|
- **Transport:** SSE (`SSEServerTransport` from the SDK handles the HTTP layer — no Express or Hono needed)
|
|
- **Port:** `7331` (hardcoded; easy to remember, unlikely to collide)
|
|
|
|
---
|
|
|
|
## Queue model
|
|
|
|
Three named inboxes: `pm`, `dev-a`, `dev-b`. Each is a FIFO array in memory.
|
|
|
|
Message shape:
|
|
|
|
```ts
|
|
interface RelayMessage {
|
|
id: string; // uuid v4
|
|
from: string; // sender role name
|
|
to: string; // recipient role name
|
|
kind: "status" | "question" | "directive" | "free";
|
|
body: string; // freeform, typically the existing markdown block format
|
|
ts: string; // ISO 8601
|
|
}
|
|
```
|
|
|
|
`kind` maps to the existing coordination protocol:
|
|
|
|
| kind | existing block |
|
|
|-------------|-----------------------------|
|
|
| `status` | `## STATUS UPDATE — DEV-*` |
|
|
| `question` | `## QUESTION TO PM — DEV-*` |
|
|
| `directive` | `## DIRECTIVE TO DEV-*` |
|
|
| `free` | ad-hoc / unstructured |
|
|
|
|
Messages are **consume-once**: `read_messages` drains the inbox. There is no persistence — if the server restarts mid-lift, in-flight messages are lost. Acceptable for a dev tool; agents re-send on reconnect.
|
|
|
|
---
|
|
|
|
## MCP tool surface
|
|
|
|
All three tools are exposed to every connected session.
|
|
|
|
### `post_message`
|
|
|
|
```
|
|
post_message(from: "pm"|"dev-a"|"dev-b", to: "pm"|"dev-a"|"dev-b", kind: "status"|"question"|"directive"|"free", body: string) → { id: string }
|
|
```
|
|
|
|
Pushes one message onto the target's inbox. Returns the assigned message id. Errors if `to` or `from` is not a known role. Agents declare their own identity via `from` — the kickoff prompt tells each agent its role name.
|
|
|
|
### `read_messages`
|
|
|
|
```
|
|
read_messages(for: "pm"|"dev-a"|"dev-b") → RelayMessage[]
|
|
```
|
|
|
|
Pops and returns all pending messages for that recipient, in FIFO order. After this call the inbox is empty.
|
|
|
|
### `list_pending`
|
|
|
|
```
|
|
list_pending(for: "pm"|"dev-a"|"dev-b") → { count: number, kinds: string[] }
|
|
```
|
|
|
|
Returns count and kind breakdown of pending messages without consuming them. Lets an agent cheaply check "do I have anything to act on?" before committing to a `read_messages` call.
|
|
|
|
---
|
|
|
|
## Server terminal output
|
|
|
|
Every `post_message` call prints a one-liner to stdout in the dedicated relay terminal:
|
|
|
|
```
|
|
[14:32:01] dev-b → pm [status] "Task P4 DONE, last commit abc1234..."
|
|
[14:33:15] pm → dev-b [directive] "PROCEED to task B1"
|
|
```
|
|
|
|
This log is the operational value of keeping the server in a dedicated terminal rather than backgrounding it.
|
|
|
|
---
|
|
|
|
## Launcher script (`start.sh`)
|
|
|
|
`start.sh` accepts one optional flag:
|
|
|
|
| Flag | Behavior |
|
|
|------------|----------|
|
|
| *(default)*| `--manual` mode: prints three labeled prompt blocks (one per role) for copy-paste into fresh Claude Code sessions, then starts the server in the foreground |
|
|
| `--tmux` | Creates a new tmux window with four panes: relay server + PM + Dev-A + Dev-B, each pre-loaded with its kickoff command |
|
|
| `--kitty` | Same layout using kitty's `launch --new-tab` / `--new-window` |
|
|
|
|
Execution order (all modes):
|
|
|
|
1. `cd tools/relay && npm install --silent` (no-op if `node_modules` is current)
|
|
2. Print the session snippet (copy-paste blocks or multiplexer launch)
|
|
3. Foreground `npx tsx server.ts` — the terminal that ran `start.sh` becomes the relay terminal; no compile step needed
|
|
|
|
Port-already-in-use check before step 3: if `:7331` is bound, print `relay already running? kill it with: kill $(lsof -ti:7331)` and exit 1.
|
|
|
|
---
|
|
|
|
## Claude Code configuration
|
|
|
|
Add to project `.claude/settings.json`:
|
|
|
|
```json
|
|
"mcpServers": {
|
|
"relay": {
|
|
"type": "sse",
|
|
"url": "http://localhost:7331/sse"
|
|
}
|
|
}
|
|
```
|
|
|
|
This is project-scoped — the relay tools only appear in Relicario Claude Code sessions. When the server is not running, Claude Code shows a yellow MCP connection warning but does not break. Agents gracefully fall back to asking the user to relay manually (existing behavior).
|
|
|
|
---
|
|
|
|
## Kickoff prompt changes
|
|
|
|
One paragraph added near the top of each coordination prompt (`v0.5.0-pm-prompt.md`, `v0.5.0-dev-a-prompt.md`, `v0.5.0-dev-b-prompt.md` as template):
|
|
|
|
> **Relay server:** A message-bus MCP server is running. You have three tools: `post_message(to, kind, body)`, `read_messages(for)`, `list_pending(for)`. Recipients: `pm`, `dev-a`, `dev-b`. Use these instead of asking the user to copy-paste. Before starting each task call `read_messages(for="<your-role>")`. After emitting any status, question, or directive block, call `post_message` with `kind` set to the block type and `body` set to the formatted block.
|
|
|
|
The `multi-agent-kickoff` skill is updated to:
|
|
- Remind the user to run `tools/relay/start.sh` before opening the three sessions
|
|
- Inject the relay paragraph automatically into every generated kickoff prompt
|
|
|
|
---
|
|
|
|
## Error handling
|
|
|
|
| Scenario | Behavior |
|
|
|----------|----------|
|
|
| Unknown `to` in `post_message` | MCP error returned; message not queued |
|
|
| Server crash / restart | In-flight messages lost; agent re-sends |
|
|
| Port 7331 in use at startup | Startup exits 1 with a kill hint |
|
|
| Session connects before server starts | Claude Code shows MCP warning; agent falls back to manual relay |
|
|
|
|
No authentication. This is localhost-only, single-machine, dev-tool use.
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
`queue.test.ts` using Node's built-in `node:test` runner. No extra test dep.
|
|
|
|
Coverage:
|
|
- `post_message` + `read_messages` roundtrip (single and multiple messages)
|
|
- Consume-once: second `read_messages` on same inbox returns empty
|
|
- `list_pending` does not drain inbox
|
|
- FIFO ordering across multiple senders to the same inbox
|
|
- Unknown recipient returns an error
|
|
|
|
No integration test against the MCP SSE transport — that is the SDK's responsibility.
|
|
|
|
---
|
|
|
|
## Top-level README (`docs/superpowers/MULTI-AGENT.md`)
|
|
|
|
A durable reference document covering the whole development paradigm — not the relay server specifically, but the entire three-terminal workflow that the relay server enables. Lives in `docs/superpowers/` alongside the specs and plans it describes.
|
|
|
|
**Contents:**
|
|
|
|
1. **Overview** — the PM/Dev-A/Dev-B pattern: why three terminals, what each role owns, what the user's job is (authorize merges, resolve escalations)
|
|
2. **Starting a lift** — prerequisites checklist, then: `tools/relay/start.sh` → three sessions → paste kickoff prompts
|
|
3. **Coordination protocol reference** — the four message kinds (`status`, `question`, `directive`, `free`), when each is used, what a well-formed body looks like
|
|
4. **Using the relay tools** — `post_message`, `read_messages`, `list_pending` with one-liner examples
|
|
5. **If the relay server isn't running** — fallback to manual copy-paste; the coordination protocol still works, just with the user as bus
|
|
6. **Generating kickoff prompts** — point to the `multi-agent-kickoff` skill; note that the skill injects the relay paragraph automatically
|
|
7. **Ending a lift** — PM emits MERGE-APPROVED, devs push branches, user authorizes merges, Ctrl-C the relay terminal
|
|
|
|
This README is written for future-you opening the repo six months from now, not for the current lift.
|
|
|
|
---
|
|
|
|
## What this is not
|
|
|
|
- Not a product feature — never bundled with the extension or CLI
|
|
- Not persistent — no SQLite, no file queue, in-memory only
|
|
- Not authenticated — localhost dev tool, no threat model
|
|
- Not a general-purpose message bus — three hardcoded roles, no dynamic registration
|