readyroom 0.1.4 → 0.1.6
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 +160 -6
- package/dist/server.mjs +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ These words are used exactly, in code, comments, tool descriptions, and docs. No
|
|
|
57
57
|
- **ticket** — one pending request: `{ id, stream, question, options, context, urgency, status, created_at, expires_at }`.
|
|
58
58
|
- **stream** — which agent asked. Defaults to `$READYROOM_STREAM`, else `basename(cwd)`.
|
|
59
59
|
- **drain** — the human answering tickets from the page.
|
|
60
|
-
- **
|
|
60
|
+
- **readyroom page** — the single HTML page served at `GET /`.
|
|
61
61
|
|
|
62
62
|
The tool names (`ask_human`, `wait_for_answer`, `list_pending`, `cancel_ticket`) are the
|
|
63
63
|
agent-facing contract and do not change with the product name. The product name is a separate
|
|
@@ -222,7 +222,7 @@ handles `SIGTERM`.
|
|
|
222
222
|
|
|
223
223
|
### Cloudflare Worker
|
|
224
224
|
|
|
225
|
-
Always-on, reachable from anywhere, no box to patch. The Worker serves the
|
|
225
|
+
Always-on, reachable from anywhere, no box to patch. The Worker serves the readyroom page and
|
|
226
226
|
the Streamable HTTP MCP endpoint from the same origin, backed by one SQLite Durable Object
|
|
227
227
|
instance.
|
|
228
228
|
|
|
@@ -240,6 +240,160 @@ the stream is required and comes from the path segment or an explicit `stream` t
|
|
|
240
240
|
If neither is present, the tool call fails with an error naming both ways to supply it. No
|
|
241
241
|
default is invented.
|
|
242
242
|
|
|
243
|
+
## MCP Configuration
|
|
244
|
+
|
|
245
|
+
An MCP client reaches readyroom over one of two transports. How you run the server decides
|
|
246
|
+
which one is available.
|
|
247
|
+
|
|
248
|
+
### Entry points
|
|
249
|
+
|
|
250
|
+
| Command | Target | Transports | State |
|
|
251
|
+
|---|---|---|---|
|
|
252
|
+
| `npx readyroom` | Node, on the host | stdio + Streamable HTTP | `./.state` on disk |
|
|
253
|
+
| `npm start` (`node dist/server.mjs`) | Node, built bundle | stdio + Streamable HTTP | `./.state` on disk |
|
|
254
|
+
| `npm run dev` (`tsx watch --import ./scripts/html-loader-register.mjs src/index.ts`) | Node, development | stdio + Streamable HTTP | `./.state` on disk |
|
|
255
|
+
| `docker run … ghcr.io/therebelrobot/readyroom:latest` | Docker | stdio + Streamable HTTP | volume mount |
|
|
256
|
+
| `npm run dev:worker` (`wrangler dev`) | Cloudflare Worker, local | Streamable HTTP only | Durable Object |
|
|
257
|
+
| `npm run deploy:worker` (`wrangler deploy`) | Cloudflare Worker, edge | Streamable HTTP only | Durable Object |
|
|
258
|
+
|
|
259
|
+
On the Node targets a process resolves into a **daemon** or a **client** (see
|
|
260
|
+
[the singleton](#three-ways-aboard)): the first process becomes the daemon and serves both
|
|
261
|
+
transports on the port; later processes join as clients and serve stdio only, proxying their
|
|
262
|
+
tool calls to the daemon. The role is decided at start — there is nothing to configure.
|
|
263
|
+
|
|
264
|
+
### Transports
|
|
265
|
+
|
|
266
|
+
- **stdio** — Node targets only. The MCP client spawns the process and speaks the protocol over
|
|
267
|
+
its file descriptors. The server writes nothing to stdout but MCP frames; all logs go to
|
|
268
|
+
stderr. Select it by configuring the client to run a `command`.
|
|
269
|
+
- **Streamable HTTP** — every target. On Node the daemon mounts it at `POST /mcp` (methods
|
|
270
|
+
`GET`, `POST`, `DELETE`) on the same port. On the Worker it is the only transport, mounted at
|
|
271
|
+
`/mcp` (stream supplied as the `/mcp/{stream}` path segment). Select it by pointing the client at the URL.
|
|
272
|
+
|
|
273
|
+
The two are not exclusive on Node: one daemon serves stdio and HTTP at once, so one agent can
|
|
274
|
+
run over stdio and another over HTTP against the same queue.
|
|
275
|
+
|
|
276
|
+
#### The `/mcp` endpoint is Origin-guarded
|
|
277
|
+
|
|
278
|
+
Both HTTP mounts reject a request whose `Origin` header is missing or not on the allowlist,
|
|
279
|
+
with `403`. On Node the allowlist is built from the server's own origins — `http://127.0.0.1:<port>`
|
|
280
|
+
and `http://localhost:<port>` — plus any `READYROOM_ALLOWED_ORIGINS` entries. On the Worker it
|
|
281
|
+
is the Worker's own origin plus the same extras. See `src/security.ts`.
|
|
282
|
+
|
|
283
|
+
> [!NOTE]
|
|
284
|
+
> An HTTP MCP client must send an `Origin` header that matches the allowlist. A client that
|
|
285
|
+
> sends no `Origin` at all is rejected. If your client sends a different origin (or none), add
|
|
286
|
+
> it to `READYROOM_ALLOWED_ORIGINS`.
|
|
287
|
+
|
|
288
|
+
#### Streams over HTTP
|
|
289
|
+
|
|
290
|
+
On Node the stream defaults to `READYROOM_STREAM`, else `basename(cwd)`, and can be overridden
|
|
291
|
+
per call with the `stream` tool argument. On the Worker there is no `cwd`: the stream comes from
|
|
292
|
+
the `POST /mcp/{stream}` path segment, or an explicit `stream` argument. If neither is present,
|
|
293
|
+
the call fails with an error naming both ways to supply it.
|
|
294
|
+
|
|
295
|
+
### Configuration options
|
|
296
|
+
|
|
297
|
+
All six are read from the process environment on the Node targets, and, on the Worker, only
|
|
298
|
+
`READYROOM_TOKEN` and `READYROOM_ALLOWED_ORIGINS` are read from `[vars]` / `secrets` in
|
|
299
|
+
`wrangler.toml`. `src/config.ts` is the single source of truth.
|
|
300
|
+
|
|
301
|
+
| Variable | Default | Required? | Meaning |
|
|
302
|
+
|---|---|---|---|
|
|
303
|
+
| `READYROOM_STREAM` | `basename(cwd)` | optional | Default stream for tickets. Overridable per call via the `stream` tool argument. Not applicable on the Worker. |
|
|
304
|
+
| `READYROOM_TOKEN` | *(unset)* | **mandatory on Docker and the Worker**, optional on Node | Shared secret for `/api/*` and the Node `/internal/tool-call` proxy. |
|
|
305
|
+
| `READYROOM_BIND` | `127.0.0.1` | optional | Bind address. A non-loopback value refuses to start unless `READYROOM_TOKEN` is set. Not applicable on the Worker. |
|
|
306
|
+
| `READYROOM_STATE_DIR` | `./.state` | optional | Directory holding `tickets.json`, `audit.jsonl`, and `daemon.json`. Not applicable on the Worker. |
|
|
307
|
+
| `READYROOM_PORT` | `8971` | optional | TCP port. Must not collide with 3000-3899, which a worktree helper allocates. Not applicable on the Worker. |
|
|
308
|
+
| `READYROOM_ALLOWED_ORIGINS` | *(unset)* | optional | Comma-separated extra origins allowed on the `/mcp` Origin guard, appended to the server's own origin. |
|
|
309
|
+
|
|
310
|
+
The per-target Worker mapping is in [Environment variables](#environment-variables).
|
|
311
|
+
|
|
312
|
+
### MCP client configuration
|
|
313
|
+
|
|
314
|
+
#### stdio (Claude Desktop and other MCP clients)
|
|
315
|
+
|
|
316
|
+
```json
|
|
317
|
+
{
|
|
318
|
+
"mcpServers": {
|
|
319
|
+
"readyroom": {
|
|
320
|
+
"command": "npx",
|
|
321
|
+
"args": ["-y", "readyroom"],
|
|
322
|
+
"env": {
|
|
323
|
+
"READYROOM_STREAM": "my-agent",
|
|
324
|
+
"READYROOM_STATE_DIR": "/home/you/.readyroom"
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Every client that spawns readyroom becomes the daemon or a client automatically; you do not set
|
|
332
|
+
the role. Give each agent its own stream with `READYROOM_STREAM` (or the `stream` argument to
|
|
333
|
+
`ask_human`). If any process in a shared daemon group sets `READYROOM_TOKEN`, every process in
|
|
334
|
+
that group must set the same value — the client forwards it to the daemon.
|
|
335
|
+
|
|
336
|
+
For a built or installed copy, replace `command` with the absolute path to `dist/server.mjs`
|
|
337
|
+
(or the `readyroom` binary on `PATH`).
|
|
338
|
+
|
|
339
|
+
#### Streamable HTTP (Node daemon)
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
{
|
|
343
|
+
"mcpServers": {
|
|
344
|
+
"readyroom": {
|
|
345
|
+
"url": "http://127.0.0.1:8971/mcp",
|
|
346
|
+
"headers": {
|
|
347
|
+
"Origin": "http://127.0.0.1:8971"
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### Streamable HTTP (Cloudflare Worker)
|
|
355
|
+
|
|
356
|
+
The stream is the path segment, so each agent gets its own URL.
|
|
357
|
+
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"mcpServers": {
|
|
361
|
+
"readyroom": {
|
|
362
|
+
"url": "https://readyroom.example.com/mcp/my-agent",
|
|
363
|
+
"headers": {
|
|
364
|
+
"Origin": "https://readyroom.example.com"
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Auth and secrets
|
|
372
|
+
|
|
373
|
+
`src/security.ts` holds both guards.
|
|
374
|
+
|
|
375
|
+
- **`READYROOM_TOKEN`** protects `/api/*` (the page's API) and, on Node, the `/internal/tool-call`
|
|
376
|
+
proxy between a client process and its daemon. The comparison is constant-time via the shared
|
|
377
|
+
`src/core/constantTimeEqual.ts`, so Node and the Worker get the identical guarantee.
|
|
378
|
+
- **The `/mcp` MCP endpoint is guarded by the `Origin` header, not by the token.** A missing or
|
|
379
|
+
mismatched `Origin` is rejected with `403`; the MCP specification requires this of HTTP
|
|
380
|
+
transports. `READYROOM_ALLOWED_ORIGINS` extends that allowlist.
|
|
381
|
+
- On Docker and the Worker the token is **mandatory**. On Node it is optional for a loopback
|
|
382
|
+
bind and mandatory for any non-loopback bind — the refusal rule in `src/config.ts` enforces it.
|
|
383
|
+
|
|
384
|
+
> [!NOTE]
|
|
385
|
+
> `src/config.ts` and the [Environment variables](#environment-variables) table describe
|
|
386
|
+
> `READYROOM_TOKEN` as the secret for "the MCP endpoint". In the shipped code the token is
|
|
387
|
+
> enforced on `/api/*` (and the Node `/internal/tool-call` proxy) only; the `/mcp` endpoint is
|
|
388
|
+
> authenticated by its `Origin` guard instead. This section documents the code's actual behavior.
|
|
389
|
+
|
|
390
|
+
##### A note on secrets
|
|
391
|
+
|
|
392
|
+
Treat `READYROOM_TOKEN` as a bearer secret. On the Worker, set it with
|
|
393
|
+
`wrangler secret put READYROOM_TOKEN` and never commit it. On Docker and npx, supply it through
|
|
394
|
+
an `--env-file` or your process manager, not a shell history. Never expose the server to a
|
|
395
|
+
public network directly — see [Never expose this to a public network directly](#never-expose-this-to-a-public-network-directly).
|
|
396
|
+
|
|
243
397
|
## Environment variables
|
|
244
398
|
|
|
245
399
|
Five variables, plus one optional extra.
|
|
@@ -372,7 +526,7 @@ The Worker has no state dir, no bind address, and no daemon role. Three conseque
|
|
|
372
526
|
1. **The token is mandatory.** There is no loopback and no safe fallback, so a token-less
|
|
373
527
|
request to any `/api/*` route is rejected. (On the Node target, a missing token is an allow —
|
|
374
528
|
the bind refusal rule guarantees a token-less server is only reachable on loopback.)
|
|
375
|
-
2. **`/api/events` returns `501`** with a short plain-text body. The
|
|
529
|
+
2. **`/api/events` returns `501`** with a short plain-text body. The readyroom page's polling
|
|
376
530
|
fallback carries the Worker target; no Durable-Object-held stream fleet is built for v1.
|
|
377
531
|
3. **`wait_for_answer` clamps its timeout** to a short window (5 seconds) and returns
|
|
378
532
|
`status: "pending"` promptly rather than erroring. This is the **one place the three targets
|
|
@@ -380,7 +534,7 @@ The Worker has no state dir, no bind address, and no daemon role. Three conseque
|
|
|
380
534
|
protocol — call again later — is already the correct handling, so no agent needs to know
|
|
381
535
|
which target it is talking to.
|
|
382
536
|
|
|
383
|
-
## The
|
|
537
|
+
## The readyroom page
|
|
384
538
|
|
|
385
539
|
The page is a **status board**, not a chat log. It shows what is waiting and clears when you
|
|
386
540
|
have answered — which is why there is no markdown parser and no message history.
|
|
@@ -398,7 +552,7 @@ network otherwise cut. It is inlined at build time into each target's bundle.
|
|
|
398
552
|
option buttons, a free-text field, and an Answer button.
|
|
399
553
|
- **Keyboard-first.** `j`/`k` move between tickets, `1`-`5` pick an option, `Enter` submits, `u`
|
|
400
554
|
undoes the last answer. Draining a queue with a mouse is the slow path.
|
|
401
|
-
- **Tab title carries the count.** `(3)
|
|
555
|
+
- **Tab title carries the count.** `(3) readyroom`.
|
|
402
556
|
- **No colour-only signalling.** Urgency and status are carried by text and borders as well as
|
|
403
557
|
hue, so the page is legible on an e-ink display and in grayscale print.
|
|
404
558
|
- **No animation on the critical path.** Respects `prefers-reduced-motion`. No hover-only
|
|
@@ -416,7 +570,7 @@ network otherwise cut. It is inlined at build time into each target's bundle.
|
|
|
416
570
|
|
|
417
571
|
```
|
|
418
572
|
GET /healthz -> 200, body "ok"
|
|
419
|
-
GET / -> the
|
|
573
|
+
GET / -> the readyroom page (index.html)
|
|
420
574
|
GET /api/tickets -> { tickets: Ticket[] } pending + last 5 min of answered
|
|
421
575
|
GET /api/events -> text/event-stream of ticket changes (501 on the Worker)
|
|
422
576
|
POST /api/answer -> { id, answer, answered_by? } -> 200 | 404 | 409 already answered
|
package/dist/server.mjs
CHANGED
|
@@ -7506,7 +7506,7 @@ var public_default = `<!DOCTYPE html>
|
|
|
7506
7506
|
<head>
|
|
7507
7507
|
<meta charset="utf-8">
|
|
7508
7508
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7509
|
-
<title>
|
|
7509
|
+
<title>readyroom</title>
|
|
7510
7510
|
<style>
|
|
7511
7511
|
/* No external fonts, no external origins, no images fetched over the network.
|
|
7512
7512
|
Everything below is self-contained so the page renders with the network cut. */
|
|
@@ -7660,7 +7660,7 @@ var public_default = `<!DOCTYPE html>
|
|
|
7660
7660
|
</head>
|
|
7661
7661
|
<body>
|
|
7662
7662
|
<header>
|
|
7663
|
-
<h1>
|
|
7663
|
+
<h1>readyroom</h1>
|
|
7664
7664
|
<p class="sub">Drain the queue. <kbd>j</kbd>/<kbd>k</kbd> move · <kbd>1</kbd>-<kbd>5</kbd> pick · <kbd>Enter</kbd> answer · <kbd>u</kbd> undo</p>
|
|
7665
7665
|
<div id="status" class="banner" role="status" aria-live="polite">
|
|
7666
7666
|
<span class="tag">Status:</span> <span id="status-text">starting</span>
|
|
@@ -8155,7 +8155,7 @@ var public_default = `<!DOCTYPE html>
|
|
|
8155
8155
|
var recent = recentTickets();
|
|
8156
8156
|
|
|
8157
8157
|
/* requirement 5: tab title carries the count */
|
|
8158
|
-
document.title = pending.length > 0 ? "(" + pending.length + ")
|
|
8158
|
+
document.title = pending.length > 0 ? "(" + pending.length + ") readyroom" : "readyroom";
|
|
8159
8159
|
|
|
8160
8160
|
/* status line */
|
|
8161
8161
|
var statusText = "idle";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "readyroom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "An MCP server that lets coding agents request a human decision without hijacking whichever window is focused. Agents enqueue a ticket; the human drains the queue from one page when they choose to.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|