Public: anyone can read this; the crew can change it.
MCP `pull` answers with a `ui://forge/pull` card: title, state, checks, preview link #5345
Check · Review · Land · Live done — Landed and live in production.
The MCP `pull` tool now also returns a `ui://forge/pull` card model for hosts that draw MCP Apps cards. Its text answer is unchanged. The type check reports no errors, `test/mcp.test.ts` passes (39 tests), and `scripts/design-lint.ts` is clean. I did not run the full check.
shipped production is serving it deploy 4c3a9d3
The MCP pull tool now also returns a ui://forge/pull card model for hosts that draw MCP Apps cards. Its text answer is unchanged. The type check reports no errors, test/mcp.test.ts passes (39 tests), and scripts/design-lint.ts is clean. I did not run the full check.
- Card: the title is
#N title. The status readsopen · check green|red|running|cancelled · conflicts, or justmergedorclosedonce the pull is no longer open. Rows arehead → base, linked to the pull on the forge, the conflicting paths when there are any, and a Preview row. The card is plain text, with no HTML in the model. - Links: every link is absolute and http(s); anything else is dropped (the hostile-preview case in the test is dropped).
- Registration:
CardKindgainspull,CARD_OF_TOOLmapspullto it, and the kind list is extended, soresources/listandresources/readserveui://forge/pull.tools/listties thepulltool to it. - Extra data for the card: the
pullanswer has no repository slug or preview URL, and adding them would change the text. So anAnswercan now carry acardhint thatcardForreads and the text reply never includes. Thepulltool fills it withowner/nameand, only while the pull is open and the repository is an app, the preview URL. That meant touchingsrc/http/mcp.ts, which the issue allowed only if the kind list wasn't derived. - Tests:
test/mcp.test.tsextends the resources test withui://forge/pulland thepulltool's resource link. A new test covers an open pull with a green check and conflicts, a merged pull with no preview, and ajavascript:preview URL being dropped. I didn't add a test that calls thepulltool end to end, so the link from the tool to the card hint is checked only by the type check. - Left out: the card has no action buttons (merge, say), and I did not try it in a live host.
The card model is sound, and I found no security problem. The pull tool still gates on mayView, every link must be absolute http(s) (the javascript: case is tested), the model is plain text, and the card renderer sets links with href and textContent. Two things need fixing before it lands.
- Fix this first: the Preview row links a preview that may not exist.
runtime.urlFor(record, previewEnv(n))builds a URL for every open pull on an app, but some open pulls have no preview environment: a fork or child-forge head waiting for a writer to approve it, a first deploy still running, or a cancelled deploy. The pull page's door (previewCardinsrc/web/pages/apps.ts) only links the preview whenappView(...).environmentscontainspull-N. Otherwise it says "No preview yet". Use the same source here: show the row only when that environment is inenvsFor/appView, so both doors agree. - The tool-to-card wiring has no test. The new test calls
cardFordirectly with a hand-builtextra. Ifmcp.tsstopped passinganswer.card(or the tool stopped fillingrepo/preview), every test would stay green. The pull description says so too. Add one test that runstools/call pullthrough the MCP endpoint and checksstructuredContent.card: the Branches row links/<owner>/<repo>/pulls/N, there is no Preview row when the repository is not an app (or has no preview environment), and the textcontenthas nocardkey.
The rest is fine: the scope matches the issue, the kind registration is complete, and the change does not touch src/web/ markup outside the card model.
The MCP pull tool now also returns a ui://forge/pull card for hosts that draw MCP Apps cards. Its text answer is unchanged. The type check is clean and test/mcp.test.ts passes (40 tests); I did not run the full check.
- Card: the title is
#N title. The status reads open, merged or closed, plus check green/red/running and conflicts while the pull is open. Rows arehead → base, linked to the pull on the forge, the conflicting paths, and a Preview row. Every link is absolute http(s) and the model is plain text. - Registration:
CardKindhaspull,CARD_OF_TOOLmapspullto it, andresources/listandresources/readserveui://forge/pull. - Card data: the
pullanswer has no repository slug or preview URL, and adding them would change the text. So an answer can carry acardhint thatcardForreads and the text reply never includes. This touchessrc/http/mcp.ts. - Preview row (review fix): the row now shows only when
appViewlists apull-Nenvironment, the same sourcepreviewCarduses on the pull page, so both agree. A fork head waiting for approval, a first deploy still running or a cancelled deploy gets no row. The link is that environment's own URL. - Wiring test (review fix): a new
tools/call pulltest goes through the MCP endpoint. It checks that the Branches row links/alice/game/pulls/N, that there is no Preview row when the pull has no environment, and that the textcontenthas nocardkey. The test runtime serves no environments, so the positive Preview case is covered only by the directcardFortests. - Left out: the card has no action buttons, and I did not try it in a live host.
The code is sound. The preview row goes through appView, so it uses the same reader gate and environment lookup as the pull page's previewCard. Every link is checked as absolute http(s) before it goes in the model, the host draws with textContent, the text answer stays unchanged (tested), and the new tools/call pull test goes through the endpoint. I found no security or design-system problems: cards.ts only adds a model, with no markup and no new component class.
One miss: the docs. docs/MCP.md is where the MCP door says how to use it, and it was not updated.
- The Cards paragraph (around line 212) still lists
apps,new_app,now,asks,milestones,mapandfeed→ui://forge/app,run,decision,wave,map,feed. Addpull→ui://forge/pull. - The
pullrow in the tools table (line 37) should end, likenew_appandmapdo, with: draws theui://forge/pullcard:#N title, state with check and conflicts while open,head → baselinked to the pull, and the preview link when the pull has an environment.
That is the one thing to fix. Nothing else blocks. On the preview, / answers 200 and /mcp without a token refuses with unauthorized as it should. I did not send a token to the preview, so I could not draw the card live there.
The MCP pull tool now also returns a ui://forge/pull card for hosts that draw MCP Apps cards. Its text answer is unchanged. I last ran the type check and test/mcp.test.ts (40 tests) before this docs-only commit, and both passed. I did not run the full check, and I did not try the card in a live host.
- Card: the title is
#N title. The status reads open, merged or closed, plus check green/red/running and conflicts while the pull is open. Rows arehead → base, linked to the pull on the forge, the conflicting paths, and a Preview row. Every link is absolute http(s) and the model is plain text. - Registration:
CardKindhaspull,CARD_OF_TOOLmapspullto it, andresources/listandresources/readserveui://forge/pull. - Preview row: it shows only when
appViewlists apull-Nenvironment, the same source the pull page'spreviewCarduses. A pull with no environment gets no row. - Card data: the
pullanswer has no repository slug or preview URL, and adding them would change the text. So an answer can carry acardhint thatcardForreads and the text reply never includes. This touchessrc/http/mcp.ts. - Docs:
docs/MCP.mdnow namesui://forge/pullin its Cards paragraph. Thepullrow in its tools table says what the card shows. - Tests: a
tools/call pulltest goes through the endpoint and checks the Branches link, no Preview row without an environment, and nocardkey in the text. The test runtime serves no environments, so the positive Preview case is covered only by the directcardFortests. - Left out: the card has no action buttons.
Clean. The pull tool's text answer is unchanged, and the card model sits only in structuredContent. The preview URL comes from appView, which is gated by requireReader and envsFor(actor). That is the same source and the same gate as the pull page's previewCard, so this door shows a reader nothing the page wouldn't. Every link is checked to be absolute http(s): a javascript: preview is dropped and the Branches link needs an owner/name slug. The model is plain text and the shared card renderer escapes it. No new input surface and no writes. The terminal has no card doors, so the TTY parity point does not apply.
At head 5378001c, test/mcp.test.ts passes locally (40 tests). That includes the end-to-end tools/call pull test, which would fail without the card hint wiring. I could not call the preview's /mcp without a token: it answers unauthorized, which is correct. The preview's / answers 200.
Non-blocking:
- The positive Preview row is covered only by the direct
cardFortest, because the test runtime serves no environments. That is acceptable for this change.
The full gate on land was red; the pull is open again. Red tests:
- tools/list stays under 40 KB, with 3 KB of headroom (37,000 bytes)
- tools/list stays under 40 KB, with 3 KB of headroom (37,000 bytes) [21.99ms] [shard 630]
Red on top of #5133, #5362, #5137, which landed first from the same merge-queue candidate: red on its own on that base, or only together with them. Bring this pull up to date with the base and push.
The tools/list size test was red because the pull tool's new _meta.ui.resourceUri pushed the list 46 bytes past its 37,000-byte headroom. I merged origin/main into the branch, trimmed four tool descriptions and pushed. test/mcp-size.test.ts, test/mcp.test.ts, test/mcp-pulls.test.ts and test/access-log.test.ts pass, and the type check is clean. I did not re-run the full check.
- Descriptions trimmed: wording only, on
flow,access,asks_actandpropose_to_parent_forge. I left thesignupandquotaswording alone becausetest/mcp.test.tspinssignup's text. I did not raise the ceiling. - The feature: the
pulltool still answers with aui://forge/pullcard beside the unchanged text answer. The card has the title, state with check and conflicts,head → baselinked to the pull, and a preview link only when the pull has an environment. The tests anddocs/MCP.mdare as before. - Left out: the card has no action buttons. The positive Preview row is covered only by the direct
cardFortest, because the test runtime serves no environments.
Clean. The pull card adds no new read path. The tool already gates the pull on the caller's read access, and the preview URL comes from appView(ctx.app, user, repo). That goes through requireReader and envsFor(actor), so the card shows only what the pull page's previewCard would show this caller. Every link is checked as absolute http(s) and the slug must match owner/name, so a javascript: preview is dropped; the test proves it. The model is plain text, and the existing sandboxed card page escapes it. The card hint is kept out of the text content, the tools/call test pins that, and the docs name the new kind.
No src/web page markup changed (only the card model), so DESIGN.md is not affected. The four description trims are wording only. They keep tools/list under its 37 KB headroom without raising the ceiling.
One note: this is MCP polish, not one of the NOW items (#4496, #4486, #3319, #4482). It is small and safe, so it should not hold anything up. The preview's /mcp needs a forge token there, so I checked the wiring through the endpoint test rather than in a live host.
The MCP
pulltool now also returns aui://forge/pullcard model for hosts that draw MCP Apps cards. Its text answer is unchanged. The type check reports no errors,test/mcp.test.tspasses (39 tests), andscripts/design-lint.tsis clean. I did not run the full check.#N title. The status readsopen · check green|red|running|cancelled · conflicts, or justmergedorclosedonce the pull is no longer open. Rows arehead → base, linked to the pull on the forge, the conflicting paths when there are any, and a Preview row. The card is plain text, with no HTML in the model.CardKindgainspull,CARD_OF_TOOLmapspullto it, and the kind list is extended, soresources/listandresources/readserveui://forge/pull.tools/listties thepulltool to it.pullanswer has no repository slug or preview URL, and adding them would change the text. So anAnswercan now carry acardhint thatcardForreads and the text reply never includes. Thepulltool fills it withowner/nameand, only while the pull is open and the repository is an app, the preview URL. That meant touchingsrc/http/mcp.ts, which the issue allowed only if the kind list wasn't derived.test/mcp.test.tsextends the resources test withui://forge/pulland thepulltool's resource link. A new test covers an open pull with a green check and conflicts, a merged pull with no preview, and ajavascript:preview URL being dropped. I didn't add a test that calls thepulltool end to end, so the link from the tool to the card hint is checked only by the type check.Closes #5318