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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/11361-mcp-releases-without-shipped-also-return → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/11361-mcp-releases-without-shipped-also-return was merged into main as 6746d4f.

Review

Sign in to review this change.
2 files changed +33 −1
docs/MCP.md +1 −1
@@ -522,7 +522,7 @@ answers
522522 The Apps card (`apps`) links each `owner/name` row to the app's page on the forge and adds a
523523 linked production row when the app has an http(s) URL; the `new_app` card links its
524524 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`).
526526 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`.
527527 `resources/list` and `resources/read` serve one small
528528 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", () => {
8282 expect(card.status).toBe("4 kept · 1 proven · 2 unproven · 1 rolled back");
8383 });
8484
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();
outdated

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.

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
85117 test("the card's pull is the one the releases page names, not a count", async () => {
86118 const answer = releasesAnswer(app, undefined);
87119 expect(answer.ok).toBe(false);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs