Public: anyone can read this; the crew can change it.

MCP `pull` carries the pull's looks: each picture with its width, line and URL #8438

Merged opened this

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

Merged

torque/forge:torque/8425-mcp-pull-carries-the-pull-s-looks-each-p → main 1 commit fork

  • Merged. torque/8425-mcp-pull-carries-the-pull-s-looks-each-p was merged into main as 488d8ef.

Review

Sign in to review this change.
2 files changed +66 −1
src/http/mcp/tools/pull.ts +22 −1
@@ -11,11 +11,14 @@ import { appView } from "../../../actions/apps.ts";
1111 import { countThread, EMBEDDED_COMMENTS, listComments } from "../../../core/issues.ts";
1212 import { isCrew } from "../../../core/agents.ts";
1313 import { markReaders } from "../../../crew/direct.ts";
14 import { getAppByRepo } from "../../../core/apps.ts";
15 import { looksForEnv, lookUrl } from "../../../core/looks.ts";
16 import { canWrite } from "../../../core/repos.ts";
1417 import { good, bad, type Tool, REPO_ARG, repoArg, isAnswer } from "../shared.ts";
1518
1619 export const tool: Tool = {
1720 name: "pull",
18 doc: "`{ repo, number }`: a pull's title, state, head and base, and for an open pull whether it merges into the base's current head (`mergeable`) and the paths that stop it (`conflicts`), and `next: { who, why }` (open only; who acts next: builder, reviewer, queue or person), `step: { step, why, done }` (check, review, land or live; null once closed unlanded), and `comments` (the newest 50: author, body, at, `untrusted` on an outsider's), `commentsTotal` and `commentsNext` (older: `GET …/issues/:number/comments?cursor=`), and `check: { state, baseOid }`: the latest check's state and the base tip it was merged with (empty when it merged nothing); `mergeable` and `conflicts` are the same fields as `GET …/pulls/:number`, `check` is `state` and `run.baseOid` from `GET …/pulls/:number/checks`; draws the `ui://forge/pull` card: `#N title`, state with check and conflicts while open, `head → base` linked to the pull, and the preview link when the pull has an environment",
21 doc: "`{ repo, number }`: a pull's title, state, head and base, and for an open pull whether it merges into the base's current head (`mergeable`) and the paths that stop it (`conflicts`), and `next: { who, why }` (open only; who acts next: builder, reviewer, queue or person), `step: { step, why, done }` (check, review, land or live; null once closed unlanded), and `comments` (the newest 50: author, body, at, `untrusted` on an outsider's), `commentsTotal` and `commentsNext` (older: `GET …/issues/:number/comments?cursor=`), and `check: { state, baseOid }`: the latest check's state and the base tip it was merged with (empty when it merged nothing); `mergeable` and `conflicts` are the same fields as `GET …/pulls/:number`, `check` is `state` and `run.baseOid` from `GET …/pulls/:number/checks`; `looks` (newest first, for writers: `[{ id, width, says, by, commit, url, at }]`, the pictures taken or attached for the pull's preview; `[]` when there are none), draws the `ui://forge/pull` card: `#N title`, state with check and conflicts while open, `head → base` linked to the pull, and the preview link when the pull has an environment",
1922 description:
2023 "A pull: title, state, head, base; open: `mergeable`, `conflicts`, `checkStale`, `check`.",
2124 inputSchema: {
@@ -50,7 +53,25 @@ export const tool: Tool = {
5053 const rows = listComments(db, pull.id, { newest: true, limit: EMBEDDED_COMMENTS });
5154 const comments = marking ? markReaders(db, repo, rows) : rows;
5255 const total = countThread(db, pull.id);
56 // The pictures are the writers' (as `mayFetchLook` keeps them), so
57 // anyone else sees none.
58 const record = canWrite(db, repo, user) ? getAppByRepo(db, repo.id) : undefined;
59 const looks =
60 record === undefined
61 ? []
62 : looksForEnv(db, record.id, previewEnv(pull.number))
63 .filter((l) => l.error === null)
64 .map((l) => ({
65 id: l.id,
66 width: l.kind,
67 says: l.note ?? "",
68 by: l.attachedBy,
69 commit: l.commitOid,
70 url: lookUrl(`/${repo.ownerName}/${repo.name}`, l),
71 at: l.createdAt,
72 }));
5373 const answer = good({
74 looks,
5475 comments: comments.map((c) => ({
5576 author: c.authorName,
5677 body: c.body,
test/attach-look.test.ts +44
@@ -262,4 +262,48 @@ describe("attaching a look to a pull", () => {
262262 expect(done.value.url).toContain("/alice/garden/looks/");
263263 expect(kept().length).toBe(before + 1);
264264 });
265
266 test("MCP pull carries the looks of its pull, newest first; a pull with none has []", async () => {
267 const call = async (token: string, number: number) => {
268 const res = await fetch(`${base}/mcp`, {
269 method: "POST",
270 headers: {
271 "content-type": "application/json",
272 accept: "application/json, text/event-stream",
273 authorization: `Bearer ${token}`,
274 },
275 body: JSON.stringify({
276 jsonrpc: "2.0",
277 id: 1,
278 method: "tools/call",
279 params: { name: "pull", arguments: { repo: "alice/garden", number } },
280 }),
281 });
282 const body = (await res.json()) as any;
283 return JSON.parse(body.result.content[0].text);
284 };
285 const done = await mcp(tokens["alice"]!, { ...good, says: "seen over MCP", dryRun: false });
286 const seen = await call(tokens["alice"]!, pull.number);
287 expect(seen.looks[0]).toMatchObject({
288 width: "phone",
289 says: "seen over MCP",
290 by: "alice",
291 commit: pull.headOid,
292 url: done.value.url,
293 });
294 expect(seen.looks.length).toBe(kept().length);
295 const ats = seen.looks.map((l: any) => l.at);
296 expect(ats).toEqual([...ats].sort().reverse());
297 // Not a writer: none.
298 expect((await call(tokens["bob"]!, pull.number)).looks).toEqual([]);
299 // A pull with no looks.
300 await commitToBranch(app, alice, repo, {
301 branch: "two", base: "main", message: "two", files: [{ path: "two.md", content: "x\n" }],
302 });
303 const second = await openPull(app, alice, repo, {
304 title: "Change two", body: "", baseBranch: "main", headBranch: "two",
305 });
306 if (!second.ok) throw new Error(String(second.error));
307 expect((await call(tokens["alice"]!, second.value.number)).looks).toEqual([]);
308 });
265309 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs