Skip to content

feat(mcp): cursor pagination shape for list tools (§6a #3) + AutoPager.page()#258

Merged
jiashuoz merged 3 commits into
mainfrom
feat/mcp-pagination-shape
Jun 21, 2026
Merged

feat(mcp): cursor pagination shape for list tools (§6a #3) + AutoPager.page()#258
jiashuoz merged 3 commits into
mainfrom
feat/mcp-pagination-shape

Conversation

@jiashuoz

Copy link
Copy Markdown
Member

Summary

§6a #3 — one pagination shape for the MCP list tools. The cursor-paginated lists now take cursor + limit and return { <items>, next_cursor } (one page; pass next_cursor back for the next), replacing the prior mix where token was accepted-but-ignored and page_size was a total cap that .toArray() walked internally — so an agent could never page past the cap, and token was a lie.

Slices

  1. SDK AutoPager.page(cursor?) — the missing single-page primitive ({items, next_cursor}); normalizes null/empty cursor → undefined; empty-string cursor = first page. Iteration/toArray/forEach unchanged.
  2. MCP list toolslist_messages, list_conversations, list_events: schemas drop token/page_size for a shared paginationInput (cursor+limit); wrapper methods return Page<T> via .page(cursor); handlers return { items, next_cursor } (next_cursor omitted on the last page).

Unchanged by design

  • Small fixed lists (list_agents/list_domains/list_webhooks) stay non-paginated (decision 7).
  • list_webhook_deliveries stays single-page limit (the API has no cursor).
  • list_pending_messages stays a cross-agent aggregate.

Verification

  • SDK 95 (incl. page() cursor walk + empty-cursor) and MCP 132 tests green.
  • Behavior tests: next_cursor surfaced / omitted-on-last-page; cursor+limit forwarded.
  • Over-the-wire e2e: list_messages cursor round-trip (page 1 → follow next_cursor → page 2 = last) across the real Streamable-HTTP/JSON-RPC transport.
  • Build clean.

Docs

Follow-up

🤖 Generated with Claude Code

jiashuoz and others added 3 commits June 20, 2026 22:08
The missing primitive for caller-driven cursor pagination: pass the prior page's
next_cursor (omit for the first page) → get { items, next_cursor }. Normalizes a
null/empty next_cursor to undefined (= last page) and treats an empty-string
cursor as the first page. Existing iteration / toArray / forEach unchanged.

This is what the MCP list tools need to expose the §6a #3 `cursor`+`limit` in,
`next_cursor` out shape (next slice), and is useful to SDK users who want manual
paging. SDK 95 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cursor-paginated list tools now expose ONE shape — `cursor` + `limit` in,
`{ <items>, next_cursor }` out (one page; pass next_cursor back for the next).
Replaces the prior mix where `token` was accepted-but-ignored and `page_size`
was a total cap that `.toArray()` walked internally — which meant an agent could
never page past the cap, and `token` was a lie.

- list_messages, list_conversations, list_events: schemas drop token/page_size →
  shared `paginationInput` (cursor+limit, in util.ts); wrapper methods return
  Page<T> via the new AutoPager.page(cursor); handlers return { items, next_cursor }
  (next_cursor omitted on the last page).
- Unchanged by design: small fixed lists (list_agents/list_domains/list_webhooks)
  stay non-paginated (decision 7); list_webhook_deliveries is single-page `limit`
  (the API has no cursor); list_pending_messages stays a cross-agent aggregate.

Tests: next_cursor surfaced / omitted-on-last-page; cursor+limit forwarded;
stubs updated to Page shape; over-the-wire cursor round-trip (page1→cursor→page2)
across the real Streamable-HTTP transport. MCP 132 + SDK 95 green.

Docs: §6a banner #3 → done; mcp/README list_messages row (cursor+limit; also
fixed the pre-existing status→read_status drift in that line).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Independent review: PASS. Adversarial review: SAFE (proved distinct pages,
next_cursor correctly signals done, and list_pending_messages still aggregates
all pages — no one-page regression). Findings applied, each with a test:

- [LOW, adversarial] AutoPager.page() used `?? undefined`, which leaks an
  empty-string next_cursor as a truthy "more pages" — contradicting its docstring
  and the iterator's `!next` termination. Normalize ""/null/undefined → undefined.
  Test: page() with next_cursor:"" → undefined.
- [should-fix, independent] only list_messages exercised the "more pages" output
  branch. Added next_cursor-surfacing tests for list_conversations + list_events.
- [note] caller-driven loop: strengthened the cursor description — stop when
  next_cursor is absent.
- [FYI, pre-existing, in-theme] corrected the stale ListConversationsInput comment
  in conversations.go that claimed single-page / next_cursor-always-null; the
  handler does real keyset continuation (hasMore → EncodeCursor). Comment-only;
  no spec change (TestSpecGoldenNoDrift green).

SDK 96 + MCP 134 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jiashuoz jiashuoz merged commit 42af1a1 into main Jun 21, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant