Public: anyone can read this; the crew can change it.
docs/MCP.md names every ui:// card the server draws, and a test fails when a card is drawn but not described #11180
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/11174-docs-mcp-md-names-every-ui-card-the-serv → main 1 commit fork
2 files
changed +80
−3
docs/MCP.md +25 −3
| @@ -482,9 +482,31 @@ answers | ||
| 482 | 482 | |
| 483 | 483 | ## Cards, the contract and the plugins (#1116) |
| 484 | 484 | |
| 485 | - **Cards.** `apps`, `new_app`, `now`, `asks`, `milestones`, `releases`, `waves` (one wave), `map`, `feed`, `pull`, `pulls`, `runs` and `asked` name a `ui://` resource in | |
| 486 | `_meta.ui.resourceUri` (MCP Apps, SEP-1865): `ui://forge/app`, `run`, | |
| 487 | `asks`, `release`, `wave`, `map`, `feed`, `pull`, `pulls`, `runs`, `asked`. The Runs card (`runs`) is one row per run, newest first: glyph, kind, the issue or pull and its title, then the state as a word with how long it ran (or has been running), linked to the run page; failed runs lead the status line. The Plan card (`map`) shows one row per pillar with | |
| 485 | - **Cards.** Each tool below names a `ui://` resource in `_meta.ui.resourceUri` (MCP Apps, SEP-1865). `test/mcp-card-table.test.ts` keeps this table equal to the server's resource list. | |
| 486 | ||
| 487 | | Resource | Tool | Shows | | |
| 488 | | --- | --- | --- | | |
| 489 | | `ui://forge/app` | `apps`, `new_app` | Your apps, or the app just made: live or not, what is working, a Tell box. | | |
| 490 | | `ui://forge/run` | — | Legacy running-now list; `now` draws `crew` instead. | | |
| 491 | | `ui://forge/asks` | `asks`, `asks_act` | One row per waiting decision with its button; after a press, the receipt. | | |
| 492 | | `ui://forge/wave` | `waves` | One wave: headline, highlights with their why, pull numbers. | | |
| 493 | | `ui://forge/map` | `map` | The plan: per pillar ready, working and waiting counts, the first ready issues. | | |
| 494 | | `ui://forge/feed` | `feed` | The latest timeline items, how many are new. | | |
| 495 | | `ui://forge/pull` | `pull` | One pull: state, check, conflicts, head → base, preview link. | | |
| 496 | | `ui://forge/pulls` | `pulls` | A repository's pulls, one row each, with check state. | | |
| 497 | | `ui://forge/runs` | `runs` | Recent runs, newest first, linked to their logs. | | |
| 498 | | `ui://forge/asked` | `asked` | What people asked the crew for and where each ask stands. | | |
| 499 | | `ui://forge/explore` | `explore` | Apps and forges ranked by real metrics. | | |
| 500 | | `ui://forge/issue` | `issue` | One issue: title, state, assignee, labels, what it waits for. | | |
| 501 | | `ui://forge/flow` | `flow` | The pipeline: per stage counts, oldest waiting, the bottleneck. | | |
| 502 | | `ui://forge/scorecard` | `scorecard` | The crew's scorecard per agent (forge admin). | | |
| 503 | | `ui://forge/crew` | `now` | What the crew is doing right now, with Stop buttons. | | |
| 504 | | `ui://forge/board` | `boards` | One board's panels (person admins). | | |
| 505 | | `ui://forge/forks` | `fork_tree` | The fork tree around a repository, each row with how it follows. | | |
| 506 | | `ui://forge/milestones` | `milestones` | The milestones and how far each is. | | |
| 507 | | `ui://forge/release` | `releases` | Kept releases, newest first, each proven, unproven or rolled back. | | |
| 508 | ||
| 509 | The Runs card (`runs`) is one row per run, newest first: glyph, kind, the issue or pull and its title, then the state as a word with how long it ran (or has been running), linked to the run page; failed runs lead the status line. The Plan card (`map`) shows one row per pillar with | |
| 488 | 510 | its ready, working and waiting counts, and the first five ready issues linked |
| 489 | 511 | to their pages; it has no actions. The asks card (`ui://forge/asks`, drawn by `asks`) is one row per waiting decision: |
| 490 | 512 | what, which repository, how long it has waited, and its one button on the row itself. |
test/mcp-card-table.test.ts added +55
| @@ -0,0 +1,55 @@ | ||
| 1 | /** | |
| 2 | * docs/MCP.md has one table row per `ui://forge/<name>` the server lists, and | |
| 3 | * no row for a card the server no longer draws. The cards come from the live | |
| 4 | * `resources/list`, never from a grep of the source. | |
| 5 | */ | |
| 6 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 7 | import { mkdtemp, readFile, rm } from "node:fs/promises"; | |
| 8 | import { tmpdir } from "node:os"; | |
| 9 | import { join } from "node:path"; | |
| 10 | import { createApp, type App } from "../src/app.ts"; | |
| 11 | import { createToken } from "../src/core/tokens.ts"; | |
| 12 | import { createUser } from "../src/core/users.ts"; | |
| 13 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 14 | import { quietLogs } from "./helpers/globals.ts"; | |
| 15 | ||
| 16 | quietLogs(); | |
| 17 | ||
| 18 | let dataDir: string; | |
| 19 | let app: App; | |
| 20 | let servers: Servers; | |
| 21 | let token = ""; | |
| 22 | ||
| 23 | beforeAll(async () => { | |
| 24 | dataDir = await mkdtemp(join(tmpdir(), "forge-mcp-cardtable-")); | |
| 25 | app = createApp({ dataDir, port: 0 }); | |
| 26 | servers = await serve(app); | |
| 27 | const a = await createUser(app.db, { origin: "seed", trusted: true, | |
| 28 | username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true }); | |
| 29 | if (!a.ok) throw new Error("users"); | |
| 30 | token = createToken(app.db, a.value.id, "admin", "admin").token; | |
| 31 | }); | |
| 32 | ||
| 33 | afterAll(async () => { | |
| 34 | servers.stop(); | |
| 35 | app.close(); | |
| 36 | await rm(dataDir, { recursive: true, force: true }); | |
| 37 | }); | |
| 38 | ||
| 39 | test("docs/MCP.md has a table row for exactly the cards the server lists", async () => { | |
| 40 | const res = await fetch(`${servers.url}/mcp`, { | |
| 41 | method: "POST", | |
| 42 | headers: { "content-type": "application/json", accept: "application/json, text/event-stream", authorization: `Bearer ${token}` }, | |
| 43 | body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "resources/list", params: {} }), | |
| 44 | }); | |
| 45 | const body = (await res.json()) as any; | |
| 46 | const drawn = new Set<string>( | |
| 47 | body.result.resources.map((r: any) => String(r.uri)).filter((u: string) => /^ui:\/\/forge\/[a-z-]+$/.test(u)), | |
| 48 | ); | |
| 49 | expect(drawn.size).toBeGreaterThan(0); | |
| 50 | const doc = await readFile(join(import.meta.dir, "..", "docs", "MCP.md"), "utf8"); | |
| 51 | const described = new Set<string>(); | |
| 52 | for (const m of doc.matchAll(/^\| `(ui:\/\/forge\/[a-z-]+)` \|/gm)) described.add(m[1]!); | |
| 53 | expect([...drawn].filter((u) => !described.has(u)).sort()).toEqual([]); | |
| 54 | expect([...described].filter((u) => !drawn.has(u)).sort()).toEqual([]); | |
| 55 | }); |