Public: anyone can read this; the crew can change it.
MCP `releases` without `shipped` also returns the `ui://forge/release` card resource, and docs/MCP.md names it in the page's words #11368
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/11361-mcp-releases-without-shipped-also-return → main 2 commits fork 2 threads · 2 unresolved
2 files
changed +33
−1
docs/MCP.md +1 −1
| @@ -522,7 +522,7 @@ answers | ||
| 522 | 522 | The Apps card (`apps`) links each `owner/name` row to the app's page on the forge and adds a |
| 523 | 523 | linked production row when the app has an http(s) URL; the `new_app` card links its |
| 524 | 524 | Production row the same way. Any other address stays plain text, never a link. |
| 525 | The Releases card (`releases`, `ui://forge/release`) is one row per kept release, the last 10, newest first (the status line reads `last 10 of N` when there are more), its version (else its short sha), how long ago it landed, the pull that landed it (as the releases page names it, linked to that pull) and one word, `proven` (the keeper verified it), `unproven` (preflight failed, the keeper never cut it or is still verifying it) or `rolled back` (the keeper rolled it back). The keeper's row is looked up by sha, however old. The rows are the `items` of the tool's text, which carry the same `proof` and `pull`; `releases` with `shipped` draws the Shipped card (`ui://forge/releases`). | |
| 525 | The Releases card (`releases`, `ui://forge/release`) is one row per kept release, the last 10, newest first (the status line reads `last 10 of N` when there are more), its version (else its short sha), how long ago it landed, the pull that landed it (as the releases page names it, linked to that pull; a release no pull landed has the sha or version and no link, as on the page) and one word, `proven` (the keeper verified it), `unproven` (preflight failed, the keeper never cut it or is still verifying it) or `rolled back` (the keeper rolled it back). The keeper's row is looked up by sha, however old. The rows are the `items` of the tool's text, which carry the same `proof` and `pull`; `releases` with `shipped` draws the Shipped card (`ui://forge/releases`). | |
| 526 | 526 | The Forks card (`fork_tree`, `ui://forge/forks`) is one row per repository you may read, nested by a `depth` field the card pads (not leading spaces, which HTML collapses); the value says how it follows: `follows on its own`, `takes updates`, `offers updates` (the card's own wording for the `offers` mode) or `off`. |
| 527 | 527 | `resources/list` and `resources/read` serve one small |
| 528 | 528 | sandboxed page, which draws the `structuredContent.card` model that the |
test/mcp-releases-card.test.ts +32
| @@ -82,6 +82,38 @@ describe("MCP releases card", () => { | ||
| 82 | 82 | expect(card.status).toBe("4 kept · 1 proven · 2 unproven · 1 rolled back"); |
| 83 | 83 | }); |
| 84 | 84 | |
| 85 | test("releases with {} and with {shipped:true} each carry their card: 10 rows at most, newest first, each pull row linked", async () => { | |
| 86 | const plain = await rpc("tools/call", { name: "releases", arguments: {} }); | |
| 87 | const items = JSON.parse(plain.content[0].text).items as any[]; | |
| 88 | const card = plain.structuredContent.card; | |
| 89 | expect(card.kind).toBe("release"); | |
| 90 | expect(card.rows.length).toBeLessThanOrEqual(10); | |
| 91 | const landed = items.map((i) => i.landed as string); | |
| 92 | expect(landed).toEqual([...landed].sort().reverse()); | |
| 93 | expect(items.some((i) => !i.pull)).toBe(true); | |
| 94 | items.forEach((i, n) => { | |
| 95 | expect(card.rows[n].label).toBe(i.version ?? i.sha); | |
| 96 | // a release with a pull links to it; one with none is unlinked, on purpose | |
| 97 | if (i.pull) expect(card.rows[n].link).toBe(`${servers.url}/${i.pull.repo}/pulls/${i.pull.number}`); | |
| 98 | else expect(card.rows[n].link).toBeUndefined(); | |
| 99 | }); | |
| 100 | expect(items.filter((i) => i.pull).length).toBeGreaterThan(0); | |
| 101 | const shipped = await rpc("tools/call", { name: "releases", arguments: { shipped: true } }); | |
| 102 | const json = JSON.parse(shipped.content[0].text); | |
outdated
Shipped card: add an order assertion (newest first / live first) alongside the link check, as box 1 requires, and check rows match the tool items. |
||
| 103 | const sc = shipped.structuredContent.card; | |
| 104 | expect(sc.kind).toBe("releases"); | |
| 105 | expect(sc.rows.length).toBeLessThanOrEqual(10); | |
| 106 | const ats = json.live.map((l: any) => l.at as string); | |
| 107 | expect(ats).toEqual([...ats].sort().reverse()); | |
| 108 | const expected = [...json.live.map((l: any) => [l, false]), ...json.onItsWay.map((l: any) => [l, true])].slice(0, 10); | |
| 109 | expect(sc.rows.length).toBe(expected.length); | |
| 110 | expected.forEach(([l, way]: any, n: number) => { | |
| 111 | expect(sc.rows[n].label).toBe(`#${l.number} ${l.title}`); | |
| 112 | expect(sc.rows[n].link).toBe(`${servers.url}/${l.repo}/pulls/${l.number}`); | |
| 113 | expect(sc.rows[n].value === "on its way").toBe(way); | |
| 114 | }); | |
| 115 | }); | |
| 116 | ||
| 85 | 117 | test("the card's pull is the one the releases page names, not a count", async () => { |
| 86 | 118 | const answer = releasesAnswer(app, undefined); |
| 87 | 119 | expect(answer.ok).toBe(false); |
Only rows with i.pull are checked; the fixture has a null-pull row. Assert every plain-card row has a link, or pin the documented behaviour for unlinked rows.