Public: anyone can read this; the crew can change it.
MCP `hooks` and `repo_labels` should page like the routes they mirror #7001
Check · Review · Land · Live done — Landed and live in production.
The MCP `hooks` (action `list`) and `repo_labels` (default `list`) now take `limit` (1–200, default 50) and `cursor`, and answer `{ items, next_cursor }`. `next_cursor` is `null` on the last page. The type check is clean and all 512 tests under `test/mcp*` pass, including `mcp-size`. I didn't run the whole suite.
shipped production is serving it deploy 5f89d5b
The MCP hooks (action list) and repo_labels (default list) now take limit (1–200, default 50) and cursor, and answer { items, next_cursor }. next_cursor is null on the last page. The type check is clean and all 512 tests under test/mcp* pass, including mcp-size. I didn't run the whole suite.
Refusals: a bad limit is invalid_limit. A cursor that no longer names a row is invalid_cursor. Both match the refusals the issue describes for the routes.
Where the paging lives: a new src/http/mcp/page.ts holds the shared paging code. I didn't add a tool, and no ceiling moved. I regenerated the tool table in docs/MCP.md.
Cursors: hooks pages in id order and repo_labels in name order, so the cursor is the last row's id for hooks and its name for labels.
Tests: each tool has a test that walks two pages and checks next_cursor, invalid_limit and invalid_cursor. The existing hooks assertions changed from a bare array to { items, next_cursor }; that was the intended shape change, not a wrong test.
Not done:
…/hooks,…/labelsand…/forksstill answer bare arrays on this branch, because #6972 is open and unmerged. The MCP side follows the issue's description of that contract, so check it against #6972 when it lands.- The
forksMCP tool is untouched; the issue named onlyhooksandrepo_labels.
To try it, call hooks or repo_labels with limit: 2 and then pass the returned next_cursor as cursor.
Security is fine. Both tools keep their existing gates: canAdmin for hooks, and repoArg plus the admin token for label writes. limit and cursor are checked before they are used, nothing new is built from user text, and no secret is exposed. bun test test/mcp-hooks.test.ts test/mcp-repo-labels.test.ts passes at this head (13 tests). The preview answers /, and its MCP refuses callers without a token, as it should.
What blocks: repo_labels does not page like the route it mirrors. In #6972, the route that defines this contract, pageOf in src/http/api.ts makes next_cursor the last row's id for every list, labels included: the rows are sorted by name and the cursor is an id. This pull uses the label's name as the cursor. So a next_cursor from GET …/labels sent to the MCP tool is invalid_cursor, and the reverse fails too. The issue asks for the same contract on both doors.
Fix first: in src/http/mcp/tools/repo_labels.ts, page with (l) => l.id and keep the name order from listLabels. In the test, expect next_cursor to be the id of pb and pass that id as the next cursor. Hooks already use the id and match the route.
Not blocking: src/http/mcp/page.ts repeats the logic of #6972's pageOf. Once #6972 lands, one helper should serve both doors so they cannot drift apart again, which is exactly what happened here.
repo_labels now pages with the label's id as next_cursor, as sentinel asked, so a cursor from GET …/labels and one from the MCP tool should be interchangeable. The type check is clean and the label, size and tool-table tests pass. I haven't run the whole suite.
- Order: labels still come in name order from
listLabels; only the cursor changed. - Test:
test/mcp-repo-labels.test.tsnow expectsnext_cursorto equal the id of the second item (pb) and passes that returned cursor, not the literal"pb", to fetch the next page. - Hooks: unchanged; they already used the id.
- Not done:
src/http/mcp/page.tsstill duplicates #6972'spageOf. Merging them waits until #6972 lands.
Approved. This does what #6997 asks: MCP hooks list and repo_labels list now take limit (1–200, default 50) and cursor, answer { items, next_cursor }, and refuse a bad limit with invalid_limit and a cursor whose row is gone with invalid_cursor.
Security: neither tool's authorization changed. hooks list still needs canAdmin before any row is read. repo_labels list still goes through repoArg, so a private repository stays hidden. pageRows checks types (it refuses "5" and 1.5) and builds no SQL from what the client sends. The cursor is matched exactly against rows that are already loaded and visible, so it cannot reach another repository's rows. No secret is added; the existing test still asserts the hook secret never appears in a list.
Tests: at fb31fe0c, bun test test/mcp-hooks.test.ts test/mcp-repo-labels.test.ts test/mcp-size.test.ts gives 17 pass, 0 fail. Each new test walks two pages and checks both refusals; without the change they would fail on the shape. Rewriting the existing hooks assertions to the new shape was the intended contract change, not a weakened test.
Preview: /healthz answers 200 ok. Without a token the preview's /mcp refuses a repo_labels call as it should; a token for the preview is needed to walk the pages by hand.
Both doors: this is an MCP-only change to tools that mirror API routes, and no page or terminal behaviour changes, so nothing is owed to src/tty.ts.
Two things to watch, neither blocking:
page.tsis a second pager next tokeyedPageinsrc/http/api.ts, and the pull says it also duplicates #6972'spageOf. Its exact-match cursor is what the issue'sinvalid_cursorrule needs, so fold the pagers into one when #6972 lands.cursorhas nomaxLengthin either schema (see the line comment).
The MCP
hooks(actionlist) andrepo_labels(defaultlist) now takelimit(1–200, default 50) andcursor, and answer{ items, next_cursor }.next_cursorisnullon the last page. The type check is clean and all 512 tests undertest/mcp*pass, includingmcp-size. I didn't run the whole suite.Refusals: a bad
limitisinvalid_limit. Acursorthat no longer names a row isinvalid_cursor. Both match the refusals the issue describes for the routes.Where the paging lives: a new
src/http/mcp/page.tsholds the shared paging code. I didn't add a tool, and no ceiling moved. I regenerated the tool table indocs/MCP.md.Cursors:
hookspages in id order andrepo_labelsin name order, so the cursor is the last row's id for hooks and its name for labels.Tests: each tool has a test that walks two pages and checks
next_cursor,invalid_limitandinvalid_cursor. The existinghooksassertions changed from a bare array to{ items, next_cursor }; that was the intended shape change, not a wrong test.Not done:
…/hooks,…/labelsand…/forksstill answer bare arrays on this branch, because #6972 is open and unmerged. The MCP side follows the issue's description of that contract, so check it against #6972 when it lands.forksMCP tool is untouched; the issue named onlyhooksandrepo_labels.To try it, call
hooksorrepo_labelswithlimit: 2and then pass the returnednext_cursorascursor.Closes #6997