@khanglvm/relay 0.10.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/docs/AGENT.md +72 -1
- package/package.json +6 -2
- package/skills/relay/SKILL.md +12 -2
- package/skills/relay/examples/palette-and-color.json +42 -0
- package/src/cli.js +94 -0
- package/src/mcp-ui/board.js +877 -0
- package/src/mcp-ui/index.html +38 -0
- package/src/mcp.js +454 -0
- package/src/server.js +3 -0
- package/src/spec.js +65 -3
- package/src/ui/annotate.js +8 -1
- package/src/ui/app.js +93 -1
- package/src/ui/blocks.css +40 -0
- package/src/ui/blocks.js +86 -5
- package/src/ui/style.css +28 -2
package/README.md
CHANGED
|
@@ -38,6 +38,34 @@ Keep relay current with **`rly upgrade`** — it installs the latest CLI and
|
|
|
38
38
|
refreshes the skill (via `npx skills`, falling back to the bundled copy) in one
|
|
39
39
|
step, leaving any boards you have open untouched.
|
|
40
40
|
|
|
41
|
+
## Inside the Claude & Codex apps (MCP App)
|
|
42
|
+
|
|
43
|
+
relay also runs as an **MCP App** ([SEP-1865](https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp))
|
|
44
|
+
— so the same board renders **inline, right in the conversation**, on Claude
|
|
45
|
+
desktop **and mobile** and in Codex, no browser tab. `rly mcp` is a
|
|
46
|
+
zero-dependency stdio MCP server; register it once and the agent gets two tools,
|
|
47
|
+
`relay_ask` (collect decisions/feedback with real form controls) and
|
|
48
|
+
`relay_show` (present a plan, diagram, diff, table, or prototype):
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
# Claude Code
|
|
52
|
+
claude mcp add relay -- rly mcp
|
|
53
|
+
|
|
54
|
+
# Claude Desktop / Codex — write the host config for you
|
|
55
|
+
rly mcp install --target claude # claude_desktop_config.json
|
|
56
|
+
rly mcp install --target codex # ~/.codex/config.toml
|
|
57
|
+
|
|
58
|
+
# or print the snippet for any MCP host (incl. the raw JSON/TOML)
|
|
59
|
+
rly mcp config
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
When the agent calls a relay tool, the host renders relay's `ui://relay/board`
|
|
63
|
+
resource in a sandboxed iframe, hands it the board spec, and the user's answers
|
|
64
|
+
flow back to the agent over the bridge (`ui/update-model-context`) — markdown,
|
|
65
|
+
code, diffs, tables, charts, mermaid/graphviz diagrams, images, and forms, all
|
|
66
|
+
in-chat. The classic browser board (`rly ask` / `rly show`) is unchanged; pick
|
|
67
|
+
whichever surface fits.
|
|
68
|
+
|
|
41
69
|
## What it improves
|
|
42
70
|
|
|
43
71
|
| Without relay | With relay |
|
|
@@ -61,6 +89,7 @@ Node ≥ 18; Chart.js / Mermaid / Graphviz are vendored and lazy-loaded offline.
|
|
|
61
89
|
| `rly help` | every command at a glance |
|
|
62
90
|
| `rly install --target <agent>` | write relay's rules into an agent's instruction file — `claude` `codex` `cursor` `copilot` `kiro` `windsurf` `cline` `gemini` `opencode` `droid` `agents`; `--all`, `--scope`, `--print`, `--list` |
|
|
63
91
|
| `rly upgrade` | update the CLI **and** refresh the skill in one step (safe around open boards; `--dry-run`, `--cli-only`, `--skill-only`) |
|
|
92
|
+
| `rly mcp` | run relay as an MCP App server so boards render **inline** in the chat — **stdio** for local desktop hosts (Claude Desktop, Codex), or `rly mcp --http` (Streamable HTTP) for web/mobile/remote; `rly mcp config` / `rly mcp install --target claude\|codex` to register it |
|
|
64
93
|
| `rly agent` | the full agent guide — spec format, all block types, annotations, patterns ([docs/AGENT.md](docs/AGENT.md)) |
|
|
65
94
|
| `rly schema` | board spec JSON Schema |
|
|
66
95
|
| [skills/relay/SKILL.md](skills/relay/SKILL.md) | the bundled skill |
|
|
@@ -73,6 +102,49 @@ npm test # zero-dep smoke tests (spawns real servers, fake-submits)
|
|
|
73
102
|
|
|
74
103
|
## Changelog
|
|
75
104
|
|
|
105
|
+
### 0.11.0 — render inline inside the Claude & Codex apps (MCP App)
|
|
106
|
+
- **`rly mcp` — relay as an MCP App** ([SEP-1865](https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp),
|
|
107
|
+
extension `io.modelcontextprotocol/ui`). A zero-dependency stdio MCP server
|
|
108
|
+
that declares a `ui://relay/board` resource (`text/html;profile=mcp-app`) and
|
|
109
|
+
two tools, **`relay_ask`** and **`relay_show`**, linked to it via
|
|
110
|
+
`_meta.ui.resourceUri` (plus `openai/outputTemplate` for ChatGPT/Codex). The
|
|
111
|
+
host renders the board **inline in the conversation** — Claude desktop **and
|
|
112
|
+
mobile**, Codex — instead of opening a browser tab.
|
|
113
|
+
- **Same board, postMessage transport.** The inline board reuses relay's block
|
|
114
|
+
renderer (markdown, code, diff, table, chart, mermaid, graphviz, image, html)
|
|
115
|
+
over the MCP Apps JSON-RPC bridge: the spec arrives as the tool result, the
|
|
116
|
+
user's answers go back via `ui/update-model-context`, the iframe auto-sizes
|
|
117
|
+
via `ui/notifications/size-changed`, and vendored Chart.js / Mermaid / Viz.js
|
|
118
|
+
load on demand through the host's `resources/read` (no `/vendor` route, no
|
|
119
|
+
server in the sandbox).
|
|
120
|
+
- **One-command setup** — `rly mcp install --target claude|codex` writes the
|
|
121
|
+
host config; `rly mcp config` prints the snippet for any MCP host. The classic
|
|
122
|
+
browser board is untouched.
|
|
123
|
+
- **Streamable HTTP transport for web/mobile/remote** — `rly mcp --http
|
|
124
|
+
[--port N --host H --token SECRET --allow-origin ORIGIN]` serves the same tools
|
|
125
|
+
over MCP's Streamable HTTP transport (single `/mcp` endpoint, JSON responses,
|
|
126
|
+
CORS, Origin validation, bearer auth, `Mcp-Session-Id`, `$PORT`-aware). relay is
|
|
127
|
+
**stateless**, so one instance serves everyone — deploy it once with the repo
|
|
128
|
+
**`Dockerfile`** on any free MCP host (mcpdeploy.dev, mcphosting.io,
|
|
129
|
+
Render/Railway/Fly, Glama) or publish to **Smithery**, then add the URL as a
|
|
130
|
+
custom connector. Only the relay CLI is required — no tunnel/tailscale. stdio
|
|
131
|
+
stays the zero-setup path for local desktop.
|
|
132
|
+
- **Native look** — the inline board **color-blends** onto the host's SEP-1865
|
|
133
|
+
style variables (surfaces, text, borders, primary button, fonts), pins
|
|
134
|
+
`color-scheme` so `light-dark()` tokens resolve, and uses the host's **own
|
|
135
|
+
full-screen** control (centering content to a readable column in fullscreen).
|
|
136
|
+
After submit it collapses to a one-line confirmation so the iframe shrinks.
|
|
137
|
+
- **Progressive rendering** — when the host streams the tool call
|
|
138
|
+
(`ui/notifications/tool-input-partial`), the board renders valid blocks as they
|
|
139
|
+
arrive (a "Composing…" preview) instead of waiting for the whole spec.
|
|
140
|
+
- **`palette` block** — color palettes as swatch cards (hover reveals hex, click
|
|
141
|
+
copies); mark one `featured` for a spotlight. **`color` question type** — native
|
|
142
|
+
picker + hex field + optional `presets`, returns a hex string. Both work on the
|
|
143
|
+
browser board and inline.
|
|
144
|
+
- **Element annotations inline** — comment on chart points, diagram nodes, table
|
|
145
|
+
cells, images and text selections in the MCP board too, returned in
|
|
146
|
+
`annotations` exactly like the CLI board.
|
|
147
|
+
|
|
76
148
|
### 0.10.0 — open files, richer code, diffs & video
|
|
77
149
|
- **Clickable local file-links.** Write a path in any markdown (`~/clip.mp4`,
|
|
78
150
|
`./src/app.ts`, `/abs/report.pdf`, a `file://` URL, a backtick-wrapped path,
|
package/docs/AGENT.md
CHANGED
|
@@ -62,6 +62,63 @@ Answers **autosave in real time** as the user fills the board — a page reload
|
|
|
62
62
|
restores them, and a draft survives timeouts/cancellation (included in those
|
|
63
63
|
results), so partial input is never lost.
|
|
64
64
|
|
|
65
|
+
## Inline mode — relay as an MCP App (Claude & Codex apps)
|
|
66
|
+
|
|
67
|
+
Everything above is the **CLI** surface (you run `rly` in a terminal and read
|
|
68
|
+
JSON from stdout). relay is **also** an MCP App (SEP-1865): when a host registers
|
|
69
|
+
`rly mcp` (see `rly mcp config` / `rly mcp install --target claude|codex`), you
|
|
70
|
+
get two tools that render the board **inline in the conversation** instead of a
|
|
71
|
+
browser tab. `rly mcp` speaks the MCP **stdio** transport, so it pairs with a
|
|
72
|
+
**local desktop** host — Claude Desktop and Codex today.
|
|
73
|
+
|
|
74
|
+
- **`relay_ask`** — collect decisions/feedback with real form controls.
|
|
75
|
+
- **`relay_show`** — present a plan, diagram, diff, table, or prototype.
|
|
76
|
+
|
|
77
|
+
**Web / mobile / remote hosts** can't reach a stdio subprocess, so for those run
|
|
78
|
+
the **Streamable HTTP** transport: `rly mcp --http [--port N]` serves the same
|
|
79
|
+
tools at `/mcp`. relay needs **only its own CLI** for this — no tunnel tool, no
|
|
80
|
+
extra infra. Because relay is **stateless** (answers go iframe→host→model, never
|
|
81
|
+
back to the server), **one deployed instance serves everyone**, so deploy it once
|
|
82
|
+
on any address the app can reach and register that URL as a custom connector:
|
|
83
|
+
|
|
84
|
+
- **Free MCP hosts** — the repo's `Dockerfile` runs relay with zero config on
|
|
85
|
+
**mcpdeploy.dev**, **mcphosting.io**, **Render / Railway / Fly**, or **Glama**;
|
|
86
|
+
the platform's `$PORT` is honored automatically. Set `RLY_MCP_TOKEN` for a
|
|
87
|
+
bearer-protected endpoint.
|
|
88
|
+
- **Smithery** (largest MCP marketplace) — publish the deployed URL with
|
|
89
|
+
`smithery mcp publish <url> -n @you/relay` for discovery + an OAuth gateway, or
|
|
90
|
+
distribute relay as a stdio bundle clients run locally.
|
|
91
|
+
- **Tunnel (optional, dev only)** — `cloudflared`/`tailscale` is *only* for
|
|
92
|
+
exposing a NAT'd laptop; it's never required and not a relay dependency.
|
|
93
|
+
|
|
94
|
+
A hosted instance can't see your local files (so `codeFile`/local-image blocks
|
|
95
|
+
won't resolve there — pass URLs or inline content); run it on your own machine if
|
|
96
|
+
you need local-file access.
|
|
97
|
+
|
|
98
|
+
Progressive rendering: if the host streams the tool call as you write it, the
|
|
99
|
+
board renders valid blocks/questions incrementally (a "Composing…" preview) and
|
|
100
|
+
finalizes when your call completes — so the user sees it build, not a blank wait.
|
|
101
|
+
|
|
102
|
+
Both take **the exact same board spec** documented below (the tool `inputSchema`
|
|
103
|
+
*is* this spec). Call the tool with your spec; the host shows the board, the user
|
|
104
|
+
fills it in, and their answers come back to you (answers, per-question notes,
|
|
105
|
+
comment) — read them just as you would the CLI's result JSON. There is **no
|
|
106
|
+
`--detach`/`rly wait` dance, no stdout parsing, and no timeout** in this mode; the
|
|
107
|
+
board stays live until the user submits and the host delivers the result.
|
|
108
|
+
|
|
109
|
+
**Near-full parity with the browser board.** Local `codeFile` / `htmlFile` /
|
|
110
|
+
`diffFile` and local **image** files work inline too — the server inlines them
|
|
111
|
+
(images as data URIs) while normalizing your spec, so the sandboxed board needs
|
|
112
|
+
no file access. **Element-level annotations work inline** — the user can comment
|
|
113
|
+
on chart points, diagram nodes, table cells, images and text selections, returned
|
|
114
|
+
in `annotations` just like the CLI. The board also adopts the host's **theme,
|
|
115
|
+
fonts and colors** and uses the host's **full-screen** control. The only
|
|
116
|
+
inline-mode gaps vs. the browser board: **local video files** (use a
|
|
117
|
+
YouTube/Vimeo/`https` URL instead — those play) and commenting on elements
|
|
118
|
+
*inside* a custom-HTML mockup (every other annotation target works). Everything
|
|
119
|
+
else — questions plus markdown/code/diff/table/chart/mermaid/graphviz/plantuml/
|
|
120
|
+
image/palette/html — renders identically.
|
|
121
|
+
|
|
65
122
|
## Creating boards
|
|
66
123
|
|
|
67
124
|
From a JSON spec file (`--file spec.json`), stdin (`--file -`), or quick
|
|
@@ -131,9 +188,11 @@ rly show --html-file prototype.html --title "Dashboard concept" --height 600
|
|
|
131
188
|
| `text` | `"string"` |
|
|
132
189
|
| `textarea` | `"string"` |
|
|
133
190
|
| `scale` | number (`min`…`max`, default 1–5) |
|
|
191
|
+
| `color` | hex string (e.g. `"#c2674b"`) — native picker + hex field; optional `presets:["#…"]` swatches |
|
|
134
192
|
|
|
135
193
|
Aliases accepted: radio/choice/select→single, checkbox→multi,
|
|
136
|
-
boolean/bool/yn→yesno, input→text, longtext→textarea, rating/likert→scale
|
|
194
|
+
boolean/bool/yn→yesno, input→text, longtext→textarea, rating/likert→scale,
|
|
195
|
+
colour/swatch→color.
|
|
137
196
|
|
|
138
197
|
### Result JSON (stdout)
|
|
139
198
|
|
|
@@ -313,6 +372,17 @@ Rules of thumb:
|
|
|
313
372
|
// for a huge / high-resolution image pass an http(s) URL (streamed, no size cap).
|
|
314
373
|
{ "type": "image", "src": "screenshots/variant-a.png", "alt": "Variant A", "height": 220 }
|
|
315
374
|
{ "type": "image", "src": "https://example.com/mock.png" }
|
|
375
|
+
|
|
376
|
+
// palette — color palettes as swatch cards (hover reveals hex, click copies).
|
|
377
|
+
// Mark one {"featured": true} to render it larger as a spotlight; the rest tile
|
|
378
|
+
// into a responsive grid. Pairs with a "color" question to let the user pick.
|
|
379
|
+
{ "type": "palette", "title": "Trending palettes", "palettes": [
|
|
380
|
+
{ "name": "Mocha Mousse", "sub": "Pantone 2025 · warm", "tag": "Pantone", "tagTone": "warm",
|
|
381
|
+
"featured": true, "colors": ["#C4956A","#A67B52","#8B6240","#D4AB89","#EDD9C4"] },
|
|
382
|
+
{ "name": "Digital Lavender", "mood": "soft tech", "tag": "Cool", "tagTone": "cool",
|
|
383
|
+
"colors": ["#C9BAF5","#A08EE8","#7B66CC","#5849A8"] } ] }
|
|
384
|
+
{ "type": "palette", "name": "Brand", "colors": ["#c2674b", "#1c1b19", "#fcfbf9"] } // single-palette shorthand
|
|
385
|
+
// tagTone (optional pill color): warm | cool | neutral | nature | bold | digital
|
|
316
386
|
```
|
|
317
387
|
|
|
318
388
|
### Local file links — clickable, open in the default app
|
|
@@ -339,6 +409,7 @@ real path over telling the user to paste it into a terminal.
|
|
|
339
409
|
| `diff` | proposed code changes / before-after — a unified diff rendered as a colored git-style comparison (no git needed) |
|
|
340
410
|
| `video` | demos, screen recordings, walkthroughs — YouTube/Vimeo embeds, a media URL, or a local video file (streamed) |
|
|
341
411
|
| `image` | screenshots, mockup exports, photos — local files embed and work offline |
|
|
412
|
+
| `palette` | color palettes / themes — swatch cards with hover-hex + click-to-copy; pair with a `color` question to let the user pick |
|
|
342
413
|
| `html` | anything else — pixel-perfect mockups, custom widgets, embeds |
|
|
343
414
|
|
|
344
415
|
### Height rules
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "Question boards with rich blocks (markdown, charts, mermaid, tables, code, diffs, video, sandboxed HTML), clickable local file-links, and element-level annotations for AI coding agents (Claude Code, Codex, …): ask users structured questions, present interactive visuals, collect inline comments, read answers as JSON — in a local browser board OR rendered INLINE inside the Claude & Codex apps as an MCP App (SEP-1865).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
7
7
|
"claude-code",
|
|
8
8
|
"codex",
|
|
9
|
+
"mcp",
|
|
10
|
+
"mcp-app",
|
|
11
|
+
"mcp-ui",
|
|
12
|
+
"model-context-protocol",
|
|
9
13
|
"cli",
|
|
10
14
|
"interactive-questions",
|
|
11
15
|
"human-in-the-loop",
|
package/skills/relay/SKILL.md
CHANGED
|
@@ -13,6 +13,13 @@ time; the tab auto-closes after submit.
|
|
|
13
13
|
If `rly` is not installed: `npm i -g @khanglvm/relay` or invoke via
|
|
14
14
|
`npx -y @khanglvm/relay <command …>`.
|
|
15
15
|
|
|
16
|
+
**Inside the Claude or Codex app?** relay is also an MCP App: if the
|
|
17
|
+
`relay_ask` / `relay_show` tools are available, call them with the same board
|
|
18
|
+
spec (below) and the board renders **inline in the chat** — no terminal, no
|
|
19
|
+
browser tab; the user's answers come straight back to you. Set it up once with
|
|
20
|
+
`rly mcp install --target claude|codex` (or `rly mcp config`). Everything below
|
|
21
|
+
describes the spec both surfaces share.
|
|
22
|
+
|
|
16
23
|
**Full reference: run `rly agent` (complete guide) and `rly schema` (spec JSON
|
|
17
24
|
Schema).** The essentials are below.
|
|
18
25
|
|
|
@@ -97,8 +104,9 @@ answers and any annotations written so far.
|
|
|
97
104
|
}
|
|
98
105
|
```
|
|
99
106
|
|
|
100
|
-
Types: `single`, `multi`, `yesno`, `text`, `textarea`, `scale
|
|
101
|
-
|
|
107
|
+
Types: `single`, `multi`, `yesno`, `text`, `textarea`, `scale`, `color`
|
|
108
|
+
(native picker + hex; optional `"presets":["#…"]`, returns a hex string). Users
|
|
109
|
+
may submit with unanswered questions (returned in `skipped`) unless
|
|
102
110
|
`"allowPartial": false` or per-question `"required": true`.
|
|
103
111
|
|
|
104
112
|
Set `"note": true` on a question to add a small optional free-text field under
|
|
@@ -148,6 +156,8 @@ single/multi question.
|
|
|
148
156
|
// screen + zoom (⌘/Ctrl+wheel or −/+, up to 8× native) + drag-to-pan viewer,
|
|
149
157
|
// so detail is always reachable regardless of height. Local images embed up
|
|
150
158
|
// to 8 MB; for a huge/high-detail image pass an http(s) URL (streamed, no cap).
|
|
159
|
+
{ "type": "palette", "palettes": [{ "name":"Brand", "colors":["#3B8EA5","#6DBAD1","#1E6278"], "featured": true }] }
|
|
160
|
+
// ^ color palettes as swatch cards: hover=hex, click=copy. Shorthand {"type":"palette","colors":[…]}; pair with a `color` question.
|
|
151
161
|
```
|
|
152
162
|
|
|
153
163
|
### Visual options — show each choice, don't describe it
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Brand color direction",
|
|
3
|
+
"intro": "Pick a direction from the curated palettes, then choose the exact accent.",
|
|
4
|
+
"blocks": [
|
|
5
|
+
{
|
|
6
|
+
"type": "palette",
|
|
7
|
+
"title": "Candidate palettes — hover a swatch for its hex, click to copy",
|
|
8
|
+
"palettes": [
|
|
9
|
+
{ "name": "Coastal Calm", "sub": "ocean, serene", "tag": "Cool", "tagTone": "cool", "featured": true,
|
|
10
|
+
"colors": ["#3B8EA5", "#6DBAD1", "#1E6278", "#A8DDE9", "#D6EEF5"] },
|
|
11
|
+
{ "name": "Peach Fuzz", "mood": "warm, optimistic", "tag": "Warm", "tagTone": "warm",
|
|
12
|
+
"colors": ["#F2A07B", "#E8795A", "#C4543C", "#FBCFB8"] },
|
|
13
|
+
{ "name": "Muted Sage", "mood": "earthy, wellness", "tag": "Nature", "tagTone": "nature",
|
|
14
|
+
"colors": ["#8DB87E", "#5D9252", "#3A6B34", "#BDD9B3"] }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{ "type": "palette", "name": "Current brand", "colors": ["#c2674b", "#1c1b19", "#fcfbf9"] }
|
|
18
|
+
],
|
|
19
|
+
"questions": [
|
|
20
|
+
{
|
|
21
|
+
"id": "direction",
|
|
22
|
+
"type": "single",
|
|
23
|
+
"label": "Which palette direction?",
|
|
24
|
+
"required": true,
|
|
25
|
+
"options": [
|
|
26
|
+
{ "value": "coastal", "label": "Coastal Calm" },
|
|
27
|
+
{ "value": "peach", "label": "Peach Fuzz" },
|
|
28
|
+
{ "value": "sage", "label": "Muted Sage" },
|
|
29
|
+
{ "value": "keep", "label": "Keep current brand" }
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "accent",
|
|
34
|
+
"type": "color",
|
|
35
|
+
"label": "Exact accent color",
|
|
36
|
+
"description": "Use the picker or paste a hex; presets are the featured palette.",
|
|
37
|
+
"presets": ["#3B8EA5", "#E8795A", "#5D9252", "#c2674b"],
|
|
38
|
+
"default": "#3B8EA5"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"submitLabel": "Lock the palette"
|
|
42
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
HOME,
|
|
19
19
|
} from './store.js';
|
|
20
20
|
import { runBoard } from './server.js';
|
|
21
|
+
import { runMcp, runMcpHttp, mcpConfig } from './mcp.js';
|
|
21
22
|
import { openUrl } from './open.js';
|
|
22
23
|
|
|
23
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -31,6 +32,7 @@ const VALUED_FLAGS = new Set([
|
|
|
31
32
|
'file', 'html', 'html-file', 'title', 'intro', 'timeout', 'port',
|
|
32
33
|
'submit-label', 'height', 'limit', 'target', 'id', 'replies',
|
|
33
34
|
'on-result', 'notify-cmd', 'idle-grace', 'scope',
|
|
35
|
+
'host', 'token', 'allow-origin',
|
|
34
36
|
]);
|
|
35
37
|
|
|
36
38
|
function camel(key) {
|
|
@@ -1181,6 +1183,91 @@ async function cmdUpgrade(args) {
|
|
|
1181
1183
|
return 0;
|
|
1182
1184
|
}
|
|
1183
1185
|
|
|
1186
|
+
// `rly mcp` — run relay as an MCP App server, or print/write the host config.
|
|
1187
|
+
// rly mcp serve over stdio (what a local desktop host launches)
|
|
1188
|
+
// rly mcp --http [--port N] serve over Streamable HTTP (web/mobile/remote hosts)
|
|
1189
|
+
// rly mcp config [--print] show setup for Claude / Codex / generic hosts
|
|
1190
|
+
// rly mcp install --target claude|codex write it into that host's config
|
|
1191
|
+
async function cmdMcp(rest) {
|
|
1192
|
+
const sub = rest[0];
|
|
1193
|
+
if (sub === 'config' || sub === 'setup' || sub === 'install' || sub === 'add') {
|
|
1194
|
+
return cmdMcpConfig(parseArgs(rest.slice(1)), sub);
|
|
1195
|
+
}
|
|
1196
|
+
const args = parseArgs(rest);
|
|
1197
|
+
if (args.http) {
|
|
1198
|
+
// Port precedence: --port flag, then PaaS-injected $PORT / $RLY_MCP_PORT,
|
|
1199
|
+
// then the default. PaaS hosts (Render, Railway, Fly, mcpdeploy, …) set $PORT.
|
|
1200
|
+
const envPort = process.env.PORT || process.env.RLY_MCP_PORT;
|
|
1201
|
+
return runMcpHttp({
|
|
1202
|
+
port: args.port ? Number(args.port) : (envPort ? Number(envPort) : undefined),
|
|
1203
|
+
host: typeof args.host === 'string' ? args.host : (process.env.RLY_MCP_HOST || '127.0.0.1'),
|
|
1204
|
+
token: typeof args.token === 'string' ? args.token : (process.env.RLY_MCP_TOKEN || ''),
|
|
1205
|
+
allowOrigin: typeof args.allowOrigin === 'string' ? args.allowOrigin : (process.env.RLY_MCP_ALLOW_ORIGIN || ''),
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
// Default: be the stdio server. main() never resolves this, so the CLI's
|
|
1209
|
+
// post-run exit timer never arms — the server lives until stdin closes.
|
|
1210
|
+
return runMcp();
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Resolve Claude Desktop's config path for this platform.
|
|
1214
|
+
function claudeDesktopConfigPath() {
|
|
1215
|
+
const home = os.homedir();
|
|
1216
|
+
if (process.platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
1217
|
+
if (process.platform === 'win32') return path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
|
|
1218
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'Claude', 'claude_desktop_config.json');
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function cmdMcpConfig(args, sub) {
|
|
1222
|
+
const cfg = mcpConfig({ command: 'rly' });
|
|
1223
|
+
const target = String(args.target || '').toLowerCase();
|
|
1224
|
+
const wantInstall = (sub === 'install' || sub === 'add') && args.print !== true;
|
|
1225
|
+
|
|
1226
|
+
// --print, or `config`/`setup` with no writable target: show every option.
|
|
1227
|
+
if (!wantInstall || !target) {
|
|
1228
|
+
const claudePath = claudeDesktopConfigPath();
|
|
1229
|
+
const codexPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
1230
|
+
printJson({
|
|
1231
|
+
note: 'Register relay as an MCP App server, then call relay_ask / relay_show from inside the host.',
|
|
1232
|
+
claudeCode: 'claude mcp add relay -- rly mcp',
|
|
1233
|
+
claudeDesktop: { file: claudePath, add: cfg.json },
|
|
1234
|
+
codex: { file: codexPath, add: cfg.toml },
|
|
1235
|
+
generic: { add: cfg.json },
|
|
1236
|
+
webMobile: cfg.http,
|
|
1237
|
+
install: 'rly mcp install --target claude | rly mcp install --target codex',
|
|
1238
|
+
});
|
|
1239
|
+
return 0;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (target === 'claude' || target === 'claude-desktop') {
|
|
1243
|
+
const file = claudeDesktopConfigPath();
|
|
1244
|
+
let existing = {};
|
|
1245
|
+
try { existing = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { existing = {}; }
|
|
1246
|
+
if (existing === null || typeof existing !== 'object' || Array.isArray(existing)) existing = {};
|
|
1247
|
+
existing.mcpServers = existing.mcpServers && typeof existing.mcpServers === 'object' ? existing.mcpServers : {};
|
|
1248
|
+
existing.mcpServers.relay = { command: cfg.command, args: cfg.args };
|
|
1249
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1250
|
+
fs.writeFileSync(file, JSON.stringify(existing, null, 2) + '\n');
|
|
1251
|
+
printJson({ installed: 'claude-desktop', file, server: 'relay', note: 'restart Claude Desktop to load it' });
|
|
1252
|
+
return 0;
|
|
1253
|
+
}
|
|
1254
|
+
if (target === 'codex') {
|
|
1255
|
+
const file = path.join(os.homedir(), '.codex', 'config.toml');
|
|
1256
|
+
let body = '';
|
|
1257
|
+
try { body = fs.readFileSync(file, 'utf8'); } catch { body = ''; }
|
|
1258
|
+
if (/\[mcp_servers\.relay\]/.test(body)) {
|
|
1259
|
+
printJson({ installed: 'codex', file, server: 'relay', note: 'already present — left as-is' });
|
|
1260
|
+
return 0;
|
|
1261
|
+
}
|
|
1262
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1263
|
+
const sep = body && !body.endsWith('\n') ? '\n\n' : body ? '\n' : '';
|
|
1264
|
+
fs.writeFileSync(file, body + sep + cfg.toml);
|
|
1265
|
+
printJson({ installed: 'codex', file, server: 'relay', note: 'restart Codex to load it' });
|
|
1266
|
+
return 0;
|
|
1267
|
+
}
|
|
1268
|
+
throw new CliError(`unknown --target "${target}". Use claude or codex (or \`rly mcp config\` to print all).`, 4);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1184
1271
|
async function cmdServeInternal(args) {
|
|
1185
1272
|
const id = args.id;
|
|
1186
1273
|
if (!id) throw new CliError('__serve: missing --id');
|
|
@@ -1236,6 +1323,11 @@ USAGE
|
|
|
1236
1323
|
--scope global|project · --print (copy/paste) · --all · --list (no flags)
|
|
1237
1324
|
rly upgrade install the latest CLI globally + refresh the skill in one step
|
|
1238
1325
|
--stop/--force handle running boards · --dry-run · --cli-only/--skill-only
|
|
1326
|
+
rly mcp run relay as an MCP App server (stdio) — boards render INLINE in
|
|
1327
|
+
a local desktop host (Claude Desktop, Codex) instead of a browser tab
|
|
1328
|
+
rly mcp --http [--port N] serve over Streamable HTTP for web/mobile/remote hosts
|
|
1329
|
+
[--host H --token SECRET --allow-origin ORIGIN]
|
|
1330
|
+
rly mcp config | install --target print host setup, or write it (claude | codex)
|
|
1239
1331
|
|
|
1240
1332
|
COMMON FLAGS
|
|
1241
1333
|
--title <s> --intro <s> --html-file <f> --height <px> --submit-label <s>
|
|
@@ -1310,6 +1402,8 @@ export async function main(argv) {
|
|
|
1310
1402
|
case 'schema':
|
|
1311
1403
|
console.log(JSON.stringify(SPEC_SCHEMA, null, 2));
|
|
1312
1404
|
return 0;
|
|
1405
|
+
case 'mcp':
|
|
1406
|
+
return await cmdMcp(rest);
|
|
1313
1407
|
case '__serve':
|
|
1314
1408
|
return await cmdServeInternal(parseArgs(rest));
|
|
1315
1409
|
default:
|