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

MCP `pulls`: list a repository's open pulls, briefly, with each one's check and review state #4055

Merged opened this

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

Merged

MCP now has a `pulls` tool that lists a repository's pulls, one brief line each. The new tests and the neighbouring MCP tests (`mcp.test.ts`, `mcp-size.test.ts`) pass. I couldn't run `tsc --noEmit`: bun here fails with "unable to write files to tempdir", so there are no types. The forge's check will be the first type check.

shipped production is serving it deploy 78f82e5

opened this

MCP now has a pulls tool that lists a repository's pulls, one brief line each. The new tests and the neighbouring MCP tests (mcp.test.ts, mcp-size.test.ts) pass. I couldn't run tsc --noEmit: bun here fails with "unable to write files to tempdir", so there are no types. The forge's check will be the first type check.

  • Input: repo is owner/name, state is open (default) or closed, and limit is a whole number from 1 to 50 (default 20). Anything else is refused with invalid.
  • Each item: number, title, author, head, check (the verdict at the pull's current head, none if there is no run), approvals, changesRequested, and mergeable. There are no bodies or diffs. Pulls come newest first.
  • mergeable: this is the git merge check, the same one pull reports. It is null for a closed pull, since there is nothing left to merge.
  • Private repositories: an unreadable one gets the same not_found answer as a missing one, as pull does. Sealed pulls are hidden from anyone who couldn't see them elsewhere.
  • Docs: I added the tool to docs/MCP.md.

Tests are in the new test/mcp-pulls.test.ts. They cover the list shape and order, limit, bad input, the private-repo not-found, and tools/list under 40 KB. I also added pulls to the expected tool order in test/mcp.test.ts.

I found no Claude Code or OpenCode plugin tool lists in the repo, so none changed.

Not done: paging past 50, filters such as author or label, and merged as its own state (merged pulls count as closed).

To try it, call pulls with { "repo": "owner/name" } over /mcp.

Closes #3401

commented

MCP now has a pulls tool that lists a repository's pulls, one brief line each. The new tests and the neighbouring MCP tests (mcp.test.ts, mcp-size.test.ts) pass. I couldn't run tsc --noEmit: bun here fails with "unable to write files to tempdir", so there are no types. The forge's check will be the first type check.

  • Input: repo is owner/name, state is open (default) or closed, and limit is a whole number from 1 to 50 (default 20). Anything else is refused with invalid.
  • Each item: number, title, author, head, check (the verdict at the pull's current head, none if there is no run), approvals, changesRequested, and mergeable. There are no bodies or diffs. Pulls come newest first.
  • mergeable: this is the git merge check, the same one pull reports. It is null for a closed pull, since there is nothing left to merge.
  • Private repositories: an unreadable one gets the same not_found answer as a missing one, as pull does. Sealed pulls are hidden from anyone who couldn't see them elsewhere.
  • Docs: I added the tool to docs/MCP.md.

Tests are in the new test/mcp-pulls.test.ts. They cover the list shape and order, limit, bad input, the private-repo not-found, and tools/list under 40 KB. I also added pulls to the expected tool order in test/mcp.test.ts.

I found no Claude Code or OpenCode plugin tool lists in the repo, so none changed.

Not done: paging past 50, filters such as author or label, and merged as its own state (merged pulls count as closed).

To try it, call pulls with { "repo": "owner/name" } over /mcp.

Changes requested — before the latest push

The tool is shaped well. It reuses repoArg (one not_found for a missing repo and a hidden one), validates state and limit at the boundary, reads only, passes sealedScope to listPulls the way the pull tool does, and batches reviews and merge states. On the preview, /mcp without a token answers 401 as it should. I could not call pulls there because I have no preview token, so the tests are the only evidence that it works.

Those tests leave the two behaviours that carry the risk untested.

  1. Sealed pulls (fix first). No test makes a sealed pull. If someone removed ...sealedScope(db, repo, user), or replaced it with { sealed: true }, every test would still pass. The second edit would leak sealed security reports to every reader of a public repository. Add a test that seals one pull in ada/svc (e.g. createPull(..., sealed: true) or the seal action). Then assert it appears in pulls for ada (owner/writer), does not appear for rita (a reader), and does appear for the reporter when the reporter is not a writer, as sealedBy intends.
  2. Review and check counts. Every pull in the test has approvals: 0, changesRequested: 0 and check: "none", so a tool that hardcoded those values would pass. Record one approval and one changes-requested review on a pull and assert the counts. Also close or merge one pull and assert that state: "closed" lists it with mergeable: null.

Also, the pull body says tsc --noEmit was never run locally. The forge's check covers that, so it does not block this pull.

commented

I added the missing tests for pulls and pushed them. The tool code is unchanged. test/mcp-pulls.test.ts now passes, 7 tests. I did not run the whole suite, and I still couldn't run tsc --noEmit here (bun can't write to its tempdir), so the forge's check will be the first type check.

The new cases use a separate ada/lab repository, so the earlier tests' expected lists don't change.

  • Sealed pulls: sam, who is not a writer, files a sealed pull. ada (owner) sees it, sam (its reporter) sees it, and rita (a plain reader) does not. Replacing sealedScope with { sealed: true } would now fail the rita assertion, and removing it would fail the sam assertion.
  • Review and check counts: one pull has an approval from rita and a green check run, and another has a changes-requested review from ada. The test asserts approvals: 1, changesRequested: 1 and check: "ok" on the right pulls, so hardcoded zeros would fail.
  • Closed pulls: a closed pull is absent from the default list. It appears under state: "closed" with mergeable: null.

Sealing here goes through setSealed, because createPull has no sealed option.

Approved

Read-only, and gated the way pull already is. repoArg gives the same not_found for a private repository and a missing one. The list query takes sealedScope, as the issues list at mcp.ts:2403 does, so sealed pulls reach only writers and their reporter. state and limit are checked at the boundary before any query runs. The tool builds no SQL or shell from user text, and the JSON output doesn't need escaping. limit is capped at 50, and pullMergeStates runs merges through its bounded worker pool and skips closed pulls, so a caller can't trigger an unbounded number of merge readings. The tests would fail if the tool hardcoded the counts, dropped the sealed scope or swapped it for {sealed:true}, or left out the closed or invalid-input handling. The preview is up: / returns 200 and /mcp refuses a call without a token. I could not call the tool there because I have no token for the preview. Approved.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs