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 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
- - **heads-up page** — the single HTML page served at `GET /`.
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 heads-up page and
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 heads-up page's polling
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 heads-up page
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) heads-up`.
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 heads-up page (index.html)
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>heads-up</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>heads-up</h1>
7663
+ <h1>readyroom</h1>
7664
7664
  <p class="sub">Drain the queue. <kbd>j</kbd>/<kbd>k</kbd> move &middot; <kbd>1</kbd>-<kbd>5</kbd> pick &middot; <kbd>Enter</kbd> answer &middot; <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 + ") heads-up" : "heads-up";
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.4",
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": [