@fusedio/relay
v0.1.0
Published
JSON-UI bridge for agent↔human interaction — render structured questions and output as real UI (browser or inline in chat via MCP Apps) and get the answer back as JSON
Downloads
158
Readme
Relay
A JSON-UI bridge for agent↔human interaction. Relay lets an AI agent render a real UI to ask a human a structured question (radio, checkboxes, validated fields, forms) or show structured output (tables, charts) — and get the answer back as JSON. The agent authors a small JSON config; the human interacts with a live UI; the response is handed back to the agent.
Relay works in two environments from one product, sharing the same component catalog, config format, and result contract:
| Environment | How the UI appears | How the agent calls it |
|---|---|---|
| CLI agents (Claude Code, Cursor, Gemini, any shell/Bash tool) | A browser tab on localhost | relay render <config> — blocks, prints the answer as JSON on stdout |
| Chat assistants (MCP Apps hosts: Claude Desktop, Claude Code, …) | Inline in the conversation (sandboxed iframe, via the MCP Apps standard) | the relay_render MCP tool — the human's response returns as a follow-up message |
Install
npm install -g relayThis installs two commands:
relay— the CLI used by shell/Bash agents and by you directly.relay-mcp— the MCP server used by chat assistants (and local MCP hosts like Claude Desktop / Claude Code).
Prefer not to install globally? Every command also works through
npx relay …/npx relay-mcp ….
Requirements: Node.js 18+. The CLI works in any shell/Bash agent; the inline chat experience needs an MCP Apps host (e.g. Claude Desktop or Claude Code).
Quick start (CLI)
Ask the human a question — the answer prints to stdout as JSON:
echo '{"type":"radio","props":{"param":"env","options":["staging","prod"]}}' \
| relay render - --title "Deploy where?"
# opens a browser tab; after the human picks:
# -> {"env":"prod"}Show the human structured output — a table/chart with a "Done" button:
relay render dashboard.json
# -> {} (returned once the human clicks Done)Discover a component's props before authoring it:
relay schema tableBecause the Bash tool blocks until the command exits, the JSON answer lands directly back in the agent's context — no polling, no second call.
The config format
The agent authors a nested node: { "type", "props", "children" }.
- Every input has a
paramprop. It binds to the data model, and that data model is the response:{ "<param>": <value>, … }. - A Submit button (or Done, for displays with no inputs) is appended automatically if you don't include one.
validationandvisibleprops are passed straight through to the renderer.optionsaccept["a","b"]or[{ "label": "...", "value": "..." }].
{
"type": "form",
"props": { "title": "Pick a deploy target" },
"children": [
{
"type": "radio",
"props": {
"param": "target",
"label": "Environment",
"options": ["staging", "prod"],
"validation": { "checks": [{ "fn": "required", "message": "Choose one" }] }
}
},
{ "type": "text-area", "props": { "param": "notes", "label": "Notes (optional)" } }
]
}→ response: { "target": "prod", "notes": "…" }
Component catalog
Fourteen self-contained components. Run relay schema <component> (or the
relay_schema MCP tool) for the exact, always-in-sync props of any one.
| Type | Purpose |
|---|---|
| text | Markdown / plain text, heading variants |
| form / stack | Containers; vertical layout, optional title |
| text-input | Single-line string |
| text-area | Multi-line string |
| number-input | Numeric, with min / max |
| select | Dropdown (single value) |
| radio | Single choice |
| checkbox-group | Multi choice → array |
| slider | Bounded numeric (min / max / step) |
| button | Fires submit or cancel |
| table | Tabular display (columns, rows) |
| bar-chart / line-chart | Charts (data, x, y, title) |
Result contract & exit codes (CLI)
relay render writes only the result JSON to stdout (URLs, logs, and prompts
go to stderr) and maps the outcome to an exit code so the agent can branch:
| Exit | Meaning | stdout |
|---|---|---|
| 0 | Submitted | the data model, e.g. {"target":"prod"} |
| 2 | Cancelled (human clicked Cancel) | {"__cancelled": true} |
| 3 | Timed out (no response within --timeout) | {"__timeout": true} |
| 4 | Closed (browser tab closed without submitting) | {"__closed": true} |
Relay detects a closed tab (a pagehide beacon to the server) and ends promptly
with exit 4 instead of waiting out --timeout. Caveat: a page reload also
fires pagehide, so reloading the tab ends the interaction as closed — distinguishing
reload from close is intentionally not implemented.
CLI command reference
relay render <config.json | ->
Read a config from a file or - (stdin), serve it on 127.0.0.1, block until the
human responds, print the response JSON, exit.
--title "…"— header / browser-tab title.--timeout <sec>— default600. A single clock governs both how long the command blocks and the server's maximum lifetime (no process lingers past it).--no-open— print the URL only; don't auto-open the browser.
relay schema [<component>]
Print JSON Schema to stdout — all components, or one named component. This is the harness-agnostic, on-demand schema accessor.
relay skill install | list | uninstall
Teach the AI harnesses on your machine how to use Relay (see Harness integrations below).
--harness claude,agents,cursor,gemini,mcp— non-interactive selection.--global(default) /--project— install scope.--yes— skip the interactive checklist.
Using Relay from a chat assistant (MCP Apps)
In chat, Relay runs as a local MCP server and renders inline using the official
MCP Apps standard:
the tool declares a UI resource, the host fetches it (resources/read) and renders
it in a sandboxed iframe inside the conversation, and the iframe talks to the
host over a postMessage channel. This works zero-infrastructure over local
stdio — no HTTP server, tunnel, or remote connector required.
1. Install the MCP server into your host
relay skill install --harness mcp # writes ./.mcp.json (project) ...
relay skill install --harness mcp --global # ... or ~/.mcp.json (global)This adds a local stdio entry pointing at the server:
{ "mcpServers": { "relay": { "command": "relay-mcp", "args": ["--stdio"] } } }Supported in MCP Apps hosts — Claude Desktop, Claude Code, and others. Restart (or "Reload MCP Configuration" in) the host so it picks up the server.
2. The agent calls the tools
The MCP server exposes two tools:
relay_render{ config, title? }→ renders the config inline in the chat as an MCP App. This is the only tool the agent calls to ask a question or show output. The human's response is delivered back to the agent as a follow-up message when they submit.relay_schema{ component? }→ the same schema accessor as the CLI'srelay schema, for authoring configs on demand.
How the chat round-trip works
agent ── relay_render(config) ──▶ host fetches the ui:// resource, renders the
iframe inline, and pushes the config to it
human fills it in & submits
iframe ──(sendMessage)──▶ the response (e.g. {"target":"prod"}) arrives as a
message; the agent continuesBecause the UI HTML is delivered via resources/read (not in the tool result), the
host's ~1 MB tool-result limit doesn't apply. The response mirrors the CLI: a
submit yields the data model; a cancel yields { "__cancelled": true }.
Harness integrations (relay skill install)
relay skill install materializes one canonical doc set into each harness's
native format, so your agents know when and how to reach for Relay. Adding a new
harness is one adapter; nothing else changes.
| Harness (--harness id) | Detected by | What's written |
|---|---|---|
| claude (Claude Code) | ~/.claude/ or claude on PATH | Skill dir: SKILL.md + schemas/ |
| agents (generic) | always (fallback) | AGENTS.md managed block |
| cursor | .cursor/ or cursor on PATH | ./.cursor/rules/relay.mdc |
| gemini | ~/.gemini/ or gemini on PATH | GEMINI.md managed block |
| mcp (Claude Desktop/Code) | ~/.claude/, claude on PATH, or ./.mcp.json | .mcp.json server entry (idempotent merge) |
Markdown/JSON targets are written as idempotent managed blocks/merges —
installing twice is a no-op, and uninstall removes only Relay's content (and drops
a .mcp.json it solely created).
relay skill list shows what's detected; relay skill uninstall removes it.
How it works (architecture)
The browser host depends on an injected RelayTransport (loadConfig /
submit) — the single seam that differs between environments. One bundle ships
everywhere and picks its transport at runtime:
- HTTP transport (CLI): a zero-dependency Node server on
127.0.0.1serves the config and collectsPOST /result. - MCP Apps transport (chat): the host fetches the bundle via
resources/readand renders it in a sandboxed iframe; inside it, the bundle uses the@modelcontextprotocol/ext-appsAppclient to read the config (ontoolinput) and return the answer (sendMessage).
Everything else — the component registry, the config compiler, validation,
rendering — is shared, transport-agnostic code. The component schema is the single
source of truth feeding the renderer, relay schema, the relay_schema tool, and
the generated skill docs.
Security: the CLI server binds to 127.0.0.1 only; the config is data, never
code (no eval, no dynamic component loading); markdown is rendered safely; and in
chat the UI runs in the host's sandboxed iframe, isolated from the conversation page.
Design and implementation notes live in docs/superpowers/.
Contributing
npm install
npm run build # bundle the host → dist/ + schemas + the self-contained resource
npm test # vitest
npm run typecheck # tsc --noEmitThe component registry under src/registry/ is the one place to add a component:
a React renderer + a Zod schema with .describe() on every prop. After registry
changes, run npm run build (regenerates schemas) then npm test.
For the full local-development workflow — running the CLI and the MCP server from your checkout without publishing, wiring Relay into Claude Desktop / Claude Code, the build internals, and testing conventions — see CONTRIBUTING.md.
