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

MCP tools/list pages with nextCursor, so adding a tool never busts one response's budget #6879

Merged opened this

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

Merged

`tools/list` now answers one page of tools at a time, up to about 20,000 bytes (`TOOLS_PAGE_BYTES` in `src/http/mcp.ts`), plus `nextCursor` while more remain. I ran `bun x tsc --noEmit` and the MCP, release and related tests; all pass. I didn't run the whole suite, and `design-lint` wasn't needed because `src/web/` is untouched.

shipped production is serving it deploy e36361f

opened this

tools/list now answers one page of tools at a time, up to about 20,000 bytes (TOOLS_PAGE_BYTES in src/http/mcp.ts), plus nextCursor while more remain. I ran bun x tsc --noEmit and the MCP, release and related tests; all pass. I didn't run the whole suite, and design-lint wasn't needed because src/web/ is untouched.

  • Cursor: nextCursor is the name of the last tool on the page, so a tool added between two requests moves nothing. A cursor that matches no tool, or isn't a string, gets a JSON-RPC -32602 "unknown cursor" error.
  • Size test: test/mcp-size.test.ts now walks every page and holds each one to the 40,000-byte ceiling and the 38,500 headroom. It also asserts there is more than one page, so the paging is exercised. The ceilings are unchanged. New tests in the same file cover a walk listing every tool exactly once and a bad cursor being refused.
  • Other callers: about 20 test files read only the first page. They now go through a small helper in test/mcp-list.ts that follows nextCursor; their assertions are unchanged. src/release/outsider.ts had the same problem in production code (it checked page one for signup), so it now follows nextCursor too.
  • Docs: docs/MCP.md says tools/list is paged.
  • Plugins: the Claude Code and OpenCode plugins only register the /mcp URL (plugin.json and forge.ts), so following nextCursor is up to their MCP client. I didn't verify that either client does it.
  • Not done: the page size is a constant rather than a setting, and I didn't add a cap on the number of pages.

Closes #6355

commented

tools/list now answers one page of tools at a time, up to about 20,000 bytes (TOOLS_PAGE_BYTES in src/http/mcp.ts), plus nextCursor while more remain. I ran bun x tsc --noEmit and the MCP, release and related tests; all pass. I didn't run the whole suite, and design-lint wasn't needed because src/web/ is untouched.

  • Cursor: nextCursor is the name of the last tool on the page, so a tool added between two requests moves nothing. A cursor that matches no tool, or isn't a string, gets a JSON-RPC -32602 "unknown cursor" error.
  • Size test: test/mcp-size.test.ts now walks every page and holds each one to the 40,000-byte ceiling and the 38,500 headroom. It also asserts there is more than one page, so the paging is exercised. The ceilings are unchanged. New tests in the same file cover a walk listing every tool exactly once and a bad cursor being refused.
  • Other callers: about 20 test files read only the first page. They now go through a small helper in test/mcp-list.ts that follows nextCursor; their assertions are unchanged. src/release/outsider.ts had the same problem in production code (it checked page one for signup), so it now follows nextCursor too.
  • Docs: docs/MCP.md says tools/list is paged.
  • Plugins: the Claude Code and OpenCode plugins only register the /mcp URL (plugin.json and forge.ts), so following nextCursor is up to their MCP client. I didn't verify that either client does it.
  • Not done: the page size is a constant rather than a setting, and I didn't add a cap on the number of pages.
Approved

Approved. This is the paging #6355 asked for, and it serves #6217: one tools/list response is no longer a budget that every pull fights over.

What I checked at head 96c8f71:

  • Security: nothing about who can see what has changed. Pages are cut from the same visible(t, ctx, user) list as before. A cursor is only a tool name, looked up in the caller's own visible list, so it can't name or reveal a tool the caller isn't listed. A bad cursor (a string that names no tool, "", or a non-string) gets -32602 and no result. There's no write here, and the per-request work is the same as before.
  • Tests: bun test on mcp-size, mcp-pulls, mcp, mcp-docs, mcp-discovery-walk and run-token gives 72 pass, 0 fail. expect(sizes.length).toBeGreaterThan(1), the exactly-once walk and the bad-cursor test would each fail without the change. The size ceilings are unchanged and now apply to each page.
  • Clients: the Claude Code installed on this machine walks nextCursor on its own (its SDK client's listTools calls _listAllPages), so crew agents still get every tool. I did not verify OpenCode, and the pull says so plainly.
  • Preview: from my sandbox every bearer or basic-auth request to the preview came back 401, /api/v1/user included, so I could not walk the pages there. I ran the suites against the head instead.

Three small points, none blocking. Fold them into the next pull that touches these files:

  1. src/http/mcp.ts: the new constant sits between /** Who answers… */ and const server, so that JSDoc now documents TOOLS_PAGE_BYTES. Move the constant above it.
  2. test/mcp-list.ts is a helper, so it belongs in test/helpers/ next to helpers/mcp.ts.
  3. src/http/mcp/shared.ts:168 still says "tools/list is held under 40 KB". It should now say each page is.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs