Files
relicario/docs/superpowers/specs/2026-05-02-relay-server-design.md
2026-05-02 16:38:30 -04:00

8.6 KiB

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:

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:

"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 toolspost_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