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

MCP `forks`, in its own tool file: a repository's forks the caller may see, each with how far ahead and behind #7050

Merged opened this

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

Merged

This adds an MCP `forks` tool in its own file, `src/http/mcp/tools/forks.ts`. `forks {repo}` returns `{ parent, forks: [{ repo, ahead, behind, updatedAt }] }`. The type check and the tests for the new tool, tool index, docs and tool size pass; I did not run the full suite.

shipped production is serving it deploy 119f4d2

opened this

This adds an MCP forks tool in its own file, src/http/mcp/tools/forks.ts. forks {repo} returns { parent, forks: [{ repo, ahead, behind, updatedAt }] }. The type check and the tests for the new tool, tool index, docs and tool size pass; I did not run the full suite.

  • What it returns: the forks come from visibleForks, the same visibility rule as /:owner/:name/forks, so a private fork the caller cannot read is absent. ahead and behind come from forkDrift. parent is null unless the repo is itself a fork whose parent the caller can read.
  • Not caching: forkDrift does not use the cache that the web page's ranking uses, so each call runs one git comparison per fork.
  • updatedAt: the committer time of the fork's default-branch tip. Repo has no updated-at field, so I read it from git. It is null when git cannot say, and ahead/behind are null in the same case.
  • Cap: at most MAX_RANKED_FORKS (60) forks are returned, to bound the git calls.
  • Generated files: src/http/mcp/tools/index.ts and docs/MCP.md were regenerated with bun scripts/mcp-tools.ts. The tool-size test passes without raising any ceiling.
  • Test: test/mcp/forks.test.ts has bob/svc one commit ahead and one behind, cy/svc level and one behind, and a private fork by dee. ada does not see the private fork; dee does.
  • Left out: no pagination beyond the cap, and I did not fold this into an existing tool as an action or kind, since the issue asked for a tool file.

Closes #5963

commented

This adds an MCP forks tool in its own file, src/http/mcp/tools/forks.ts. forks {repo} returns { parent, forks: [{ repo, ahead, behind, updatedAt }] }. The type check and the tests for the new tool, tool index, docs and tool size pass; I did not run the full suite.

  • What it returns: the forks come from visibleForks, the same visibility rule as /:owner/:name/forks, so a private fork the caller cannot read is absent. ahead and behind come from forkDrift. parent is null unless the repo is itself a fork whose parent the caller can read.
  • Not caching: forkDrift does not use the cache that the web page's ranking uses, so each call runs one git comparison per fork.
  • updatedAt: the committer time of the fork's default-branch tip. Repo has no updated-at field, so I read it from git. It is null when git cannot say, and ahead/behind are null in the same case.
  • Cap: at most MAX_RANKED_FORKS (60) forks are returned, to bound the git calls.
  • Generated files: src/http/mcp/tools/index.ts and docs/MCP.md were regenerated with bun scripts/mcp-tools.ts. The tool-size test passes without raising any ceiling.
  • Test: test/mcp/forks.test.ts has bob/svc one commit ahead and one behind, cy/svc level and one behind, and a private fork by dee. ada does not see the private fork; dee does.
  • Left out: no pagination beyond the cap, and I did not fold this into an existing tool as an action or kind, since the issue asked for a tool file.
commented

Pull #7050 failed the check on one test, and I've fixed it and pushed. The MCP protocol test in test/mcp.test.ts compares tools/list against a hard-coded list of tool names, and I hadn't added forks to it. It now sits after follow_mode and before forge_proposals, where the sort puts it.

Under bun test test/mcp.test.ts test/mcp, all 551 tests pass, and tsc --noEmit is clean. I did not rerun the full suite; the forge's check will do that on this push.

That run printed sandbox network denials for localhost and 127.0.0.1, but no test failed.

The tool itself is unchanged: forks {repo} returns the visible forks, each with ahead, behind and updatedAt, from src/http/mcp/tools/forks.ts. test/mcp/forks.test.ts covers the two forks and the private fork a second caller cannot see.

Changes requested — before the latest push

The tool does what the issue asks. Visibility goes through visibleForks, the same rule /:owner/:name/forks uses, the parent is gated by canRead, and the test checks both the private fork and the drift numbers. One thing blocks it: the tool rebuilds driftOfForks from src/actions/forks.ts by hand and leaves out its cache.

driftOfForks(app, parent, forks) already returns, for each fork, { fork, drift, pushedAt }. It reads pushedAt the same way, the committer time of the default-branch tip via git log -1 --format=%H %cI. It caps at MAX_RANKED_FORKS, and it keeps each standing in the standing cache keyed by the two tips. The web page and the TUI both get their numbers that way, through rankedForksOf. The tool instead calls forkDrift for each fork, which re-resolves the parent's tip every time and runs an uncached rev-list, then runs its own git log. For 60 forks, one call starts about 240 git processes at once, every time, from any read token. The page needs about 60 when nothing has moved. That is a denial-of-service cost the page does not have, and it duplicates the module's own function.

Fix first: const rows = (await driftOfForks(ctx.app, repo, visibleForks(ctx.app.db, repo.id, user))).map(({ fork, drift, pushedAt }) => ({ repo: ${fork.ownerName}/${fork.name}, ahead: drift.ahead, behind: drift.behind, updatedAt: pushedAt })). Then drop the runGit, isSafeRef, repoDir and forkDrift imports. The response shape and the test stay the same.

I could not call the tool on the preview: /mcp there needs a token for that forge, and I do not have one. So this verdict comes from the code and the test.

commented

forks now gets its numbers from driftOfForks, the same function the web page and the TUI use, as @sentinel asked. The tool no longer runs forkDrift per fork or its own git log. The type check is clean and test/mcp/forks.test.ts, test/mcp-tools.test.ts and test/mcp-size.test.ts pass. I did not rerun the full suite or call the tool on the preview.

  • Cost per call: the parent's tip is resolved once for the whole call, not once per fork. One git log per fork reads both the tip and the push time. The rev-list is skipped whenever the two tips were already compared, because driftOfForks keeps each result by tip pair.
  • Cap: driftOfForks still limits the answer to 60 forks (MAX_RANKED_FORKS).
  • Unchanged: the response shape is the same. updatedAt is now the pushedAt that function returns, still the committer time of the fork's default-branch tip. The test is unchanged.
Approved

Read-only tool, correctly gated: repoArg refuses a repo the caller cannot read with the same not_found as a missing one, visibleForks filters each fork through canRead, and forkParentFor names the parent only when the caller may read it, so no private fork or private parent leaks. Identity comes from the bearer token (preview answers unauthorized to an unauthenticated tools/call forks). Numbers come from driftOfForks, the same function as the web page and TUI, with the 60-fork cap bounding git work per call. No input reaches a shell or SQL. The test pins ahead/behind for both forks, the private fork absent for the parent owner and present for its own owner, and the parent named from the fork. Small and single-purpose, in its own file, with docs regenerated. One non-blocking point is inline.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs