Pagination and caching
created: 2026-09-30T08:00:00-05:00
last-updated: 2026-10-29T02:40:00-05:00
increment: 1
status: standard
tags: area:compare
description: cursor rules per revision, 2026 cacheable results (ttlMs, cacheScope), list stability, and how mcpx and opencode page and cache
Every list method pages with an opaque cursor in every revision; 2026 adds caching hints (ttlMs, cacheScope) that
six results MUST carry, and tightens what a cursor and a list may be. PG-08..PG-13 were trued up on 2026-10-29
(#1604, Phase 3 of design #1601): mcpx’s server now stamps both cache fields on complete cacheable results for
modern peers, and its client reads, aggregates and honours them – the pool catalogue cache expires at the
earliest declared ttlMs for modern upstreams, and cacheScope: "private" catalogues never reach the disk
cache. What matters most in the rest of this register at 05c78b2 is that its client stops paging on an empty
nextCursor, which 2026 says is a valid cursor. Deterministic tools/list order is TOOL-23 in the tools register.
Status column: mcpx @ 05c78b2, except PG-08..PG-13, which read mcpx @ c58507a (post-#1601, trued up by #1604). Legend and citation conventions: ../README.md.
| ID | Difference | Labels | Where it exists | mcpx @ 05c78b2 | Value | Effort | Risk |
|---|---|---|---|---|---|---|---|
| PG-01 | opaque cursors; “don’t persist across sessions” dropped in 2026 | 2024-11-05 has 2026-07-28 has | 24-11..25-11 ✓ (session-bound) · 26-07 ✓ (no sessions) | ✓ — stateless encoded offsets | + low | S | low |
| PG-02 | "" is a valid cursor and MUST NOT end the list | 2026-07-28 has mcpx missing | 26-07 ✓ only · opencode v1 ✓ (only undefined ends) | ✗ — client stops paging on "" (#200) | + med | S | med |
| PG-03 | invalid cursor SHOULD be -32602 | 2024-11-05 has mcpx missing | all five ✓ (SHOULD) | ✗ — undecodable cursor silently restarts at zero (#202) | + low | S | low |
| PG-04 | servers SHOULD provide stable cursors | 2024-11-05 has | all five ✓ (SHOULD); 26-07 no cross-page guarantee | partial — offsets shift if the list changes | + low | S | low |
| PG-05 | page size is the server’s choice | 2024-11-05 has | all five ✓ | ✓ — mcp.pageSize, default 100 | + low | S | low |
| PG-06 | client page-following caps | opencode v1 has opencode v2 has | opencode v1 1000 pages · v2 64 pages · spec silent | partial — 100 pages, silent truncation, no loop check | + low | S | low |
| PG-07 | mcpx client swallows list errors and skips template pages | 2024-11-05 has mcpx missing | mcpx client | ✗ — prompts/list and templates errors hidden (#203) | + low | S | med |
| PG-08 | six results MUST carry cache hints; changelog names five | 2026-07-28 has specs conflict | 26-07 ✓ only | ✓ — server stamps both fields on complete cacheable results for modern peers (#199); client reads and aggregates them on list results (#1601 Phase 1, PR #1608) | + med | S | med |
| PG-09 | ttlMs: integer ms ≥ 0; absent means 0 | 2026-07-28 has | 26-07 ✓ only | ✓ — emitted by the server (Cache.List / Cache.Read); honoured by the client: a modern pool cache expires at fetch + min declared ttlMs, absent/0/negative is immediately stale (#1602) | + med | S | med |
| PG-10 | cacheScope: "public" or "private" | 2026-07-28 has | 26-07 ✓ only | ✓ — emitted by the server; honoured by the client at the disk boundary: private catalogues never persist, public entries carry deadline + scope (#1603, PR #1705) | + med | S | high |
| PG-11 | per-page caching: own TTL clock, same scope on every page | 2026-07-28 has | 26-07 ✓ only | ✓ by construction — pages are aggregated into one catalogue cache with the minimum TTL across pages and private if any page is private; an expired catalogue is re-fetched whole, never per page (#1601) | + low | S | low |
| PG-12 | MRTR retries and input_required results never cached | 2026-07-28 has | 26-07 ✓ only | ✓ — mcpx caches no MRTR result in either direction; the server stamps hints only on resultType: "complete" results, so input_required carries none | + low | S | low |
| PG-13 | mcpx’s upstream schema cache ignores ttlMs | 2026-07-28 has | 26-07 (client MAY cache per ttlMs) | ✓ (modern era) — the pool cache expires at the declared deadline and a failed refetch of an expired entry serves stale for that call only; legacy upstreams keep cache-until-list_changed by design (#1601); disk persistence is scope-aware (#1603) | + med | S | med |
| PG-14 | lists MUST NOT vary per connection; MAY vary by authorization | 2026-07-28 has | 26-07 ✓ only (tools, prompts, resources) | ✓ — lists are per daemon | + med | S | med |
PG-01 opaque cursors and persistence
- What. Cursors are opaque strings in every revision. 2024-11-05 through 2025-11-25 also tell clients not to persist cursors across sessions; 2026 drops that line (there are no sessions) and replaces it with PG-02’s rule.
- Where. All five; the persistence rule is gone in 2026.
- mcpx @ 05c78b2.
nextCursorisbase64url("o:<offset>")(internal/mcpserver/server.go:1477-1505), encoded so that clients are not tempted to parse it (internal/mcpserver/server.go:1472-1476). It depends on no server state, so it stays valid across connections — what 2026 now permits. - Value to mcpx. + already stateless.
- Effort. S.
- Risk. Low.
- Detail. Being stateless, the cursor is also unsigned and unversioned (PG-04).
- Sources.
2024-11-05/server/utilities/pagination.mdx:17“- The cursor is an opaque string token, representing a position in the result set”;2025-11-25/server/utilities/pagination.mdx:93“- Don’t persist cursors across sessions”;internal/mcpserver/server.go:1477-1505
PG-02 the empty-string cursor
- What. 2026: clients MUST NOT decide anything from a cursor’s value except whether a non-null one was given; an
empty string is a valid cursor and MUST NOT be treated as the end of results. Only an absent or null
nextCursorends the list. - Where. 2026 only. opencode v1 already stops only on
undefined. - mcpx @ 05c78b2. As a server it never emits
""(it addsnextCursoronly when non-empty,internal/mcpserver/server.go:551-555). As a client it stops whenNextCursor == ""(internal/mcpclient/client.go:744,internal/mcpclient/client.go:766,internal/mcpclient/client.go:820), and a Gostringcannot tell""from absent, so a 2026 upstream that uses""as a real cursor is read as one page. - Value to mcpx. + complete catalogues from every conformant 2026 server.
- Effort. S — decode
nextCursoras*string. - Risk. If not fixed: silent truncation of an upstream’s tools, resources or prompts.
- Detail. mcpx’s server treats an incoming
cursor: ""as “start” (internal/mcpserver/server.go:1483) — harmless, since it never issues one. - Sources.
2026-07-28/server/utilities/pagination.mdx:106“non-null value was provided (e.g. an empty string is a valid cursor and”;internal/mcpclient/client.go:744“if res.NextCursor == “;v1:packages/opencode/src/mcp/catalog.ts:29
PG-03 invalid cursors
- What. An invalid cursor SHOULD produce
-32602(Invalid params). The 2026 schema’sInvalidParamsErrordoc names “invalid or expired cursor values” and ships an example. - Where. All five (SHOULD).
- mcpx @ 05c78b2. An undecodable cursor is ignored and the list restarts at offset 0
(
internal/mcpserver/server.go:1483-1486). - Value to mcpx. + clients notice a stale cursor instead of looping or duplicating.
- Effort. S.
- Risk. Low. In 2026 an error is also the signal for a client to discard its cached pages (PG-11); a silent restart never sends it.
- Detail. None beyond the above.
- Sources.
2024-11-05/server/utilities/pagination.mdx:94“Invalid cursors SHOULD result in an error with code -32602 (Invalid params).”;2026-07-28/server/utilities/pagination.mdx:111;schema/2026-07-28/schema.ts:368;internal/mcpserver/server.go:1483-1486
PG-04 stable cursors
- What. Servers SHOULD provide stable cursors. 2026’s caching page adds that there is no cross-page consistency guarantee: if data changes between pages, clients may see duplicates or gaps, and those wanting a snapshot SHOULD re-fetch from the start.
- Where. All five (SHOULD); the relaxation is 2026.
- mcpx @ 05c78b2. Cursors are plain offsets, not tied to a list version, so a list that changes between pages
(an upstream reload, an adapter added) shifts silently (
internal/mcpserver/server.go:1477-1505). - Value to mcpx. + low:
tools/listfits one page today (61 < 100). - Effort. S — version or sign the cursor.
- Risk. Low, and within what 2026 explicitly tolerates.
- Detail. mcpx’s own tool list never changes after it is built (TOOL-24, CAP-20), so only resource and prompt lists can shift.
- Sources.
2024-11-05/server/utilities/pagination.mdx:80“- Provide stable cursors”;2026-07-28/server/utilities/caching.mdx:158“- There is no cross-page consistency guarantee. If the underlying data changes between”
PG-05 page size
- What. The server decides the page size; clients MUST NOT assume a fixed one.
- Where. All five.
- mcpx @ 05c78b2.
mcp.pageSize(internal/cli/serve.go:309), default"100"written inline in the settings registry (internal/settings/registry.go:417), with a second inlinereturn 100fallback (internal/mcpserver/server.go:1469). All four list methods use it. - Value to mcpx. + low.
- Effort. S — move both defaults into
defaults.json. - Risk. Low. The page size is configurable; only its two defaults are inline constants, against mcpx’s “nothing hardcoded” rule.
- Detail. With opencode v2’s 64-page cap (PG-06), 100 per page allows 6,400 entries before v2 stops reading.
- Sources.
2024-11-05/server/utilities/pagination.mdx:18“- Page size is determined by the server, and clients MUST NOT assume a fixed page”;internal/cli/serve.go:309“srv.PageSize = a.Settings().Int(”;internal/settings/registry.go:417;internal/mcpserver/server.go:1469“return 100”
PG-06 client page-following caps
- What. No revision caps how many pages a client follows; clients choose a cap to survive a server that never stops.
- Where. opencode v1 pages itself up to 1,000 pages and errors on a repeated cursor; opencode v2 relies on its SDK, which aggregates up to 64 pages.
- mcpx @ 05c78b2.
ListTools,ListResourcesandListPromptsfollow at most 100 pages (internal/mcpclient/client.go:734,internal/mcpclient/client.go:756,internal/mcpclient/client.go:807), then return what they have without an error, and do not detect a repeated cursor. - Value to mcpx. + low.
- Effort. S — report truncation, detect loops, move the literal into settings.
- Risk. A server that repeats a cursor costs 100 round trips; a very large one is truncated without notice.
- Detail. The cap is an inline
100in three loops. - Sources.
internal/mcpclient/client.go:734“for i := 0; i < 100; i++ {”;v1:packages/opencode/src/mcp/catalog.ts:12“const MAX_LIST_PAGES = 1_000”;v1:packages/opencode/src/mcp/catalog.ts:30; https://unpkg.com/@modelcontextprotocol/client@2.0.0/dist/index.mjs L2904
PG-07 mcpx client swallows list errors
- What. How a list reader treats an error mid-listing.
- Where. mcpx client.
- mcpx @ 05c78b2.
ListPromptsreturns whatever it has on any error, as if the server had no prompts (internal/mcpclient/client.go:813-818).ListResourcesrequestsresources/templates/listonce, without a cursor, and ignores its error (internal/mcpclient/client.go:772);ListResourceTemplatesdoes not page either (internal/mcpclient/client.go:556-562). - Value to mcpx. + low.
- Effort. S — distinguish
-32601from other errors; follow template cursors. - Risk. A transient error hides a server’s prompts until the next refresh, because the empty list is cached (PG-13); templates past the first page never appear.
- Detail. The pool calls these only when the server declared the capability, so “no prompts” is not the likely meaning of an error there.
- Sources.
internal/mcpclient/client.go:813-818“A server without prompts answers method-not-found”;internal/mcpclient/client.go:772;internal/mcpclient/client.go:556-562
PG-08 which results must carry cache hints
- What. In 2026, complete results of
server/discover,tools/list,prompts/list,resources/list,resources/templates/listandresources/readMUST carryttlMsandcacheScope.prompts/get,completion/complete,tools/calland anyinput_requiredresult do not. A cached response is keyed by the method plus the parameters that affect it (uri,cursor). - Where. 2026 only. The changelog lists five methods and omits
server/discover; the caching page and the schema (DiscoverResult extends CacheableResult) include it. - mcpx @ c58507a. Both halves landed. As a server, mcpx stamps
ttlMsandcacheScopeon every complete cacheable result for modern peers (envelope ininternal/mcpserver/server.go, scope table andCachevalues from theprotoMessageslist/read max-age settings; legacy peers have both fields stripped,internal/mcpserver/revisions.go). As a client, the list result types carryttlMs/cacheScope(internal/mcpclient/client.go) and theList*WithHintsvariants aggregate them across pages intoCacheHints(minimum declared TTL; private if any page is private) – #1601 Phase 1, PR #1608. - Value to mcpx. + schema-required; hosts could cache mcpx’s small, fixed tool surface.
- Effort. S — stamp both fields for modern peers alongside
resultType. (Done.) - Risk. Strict 2026 clients reject the results as schema-invalid.
- Detail.
resources/readis cacheable whileprompts/getis not — an asymmetry the schema makes explicit. - Sources.
2026-07-28/server/utilities/caching.mdx:13“Servers MUST include caching hints on results with”;2026-07-28/changelog.mdx:36“RequirettlMsandcacheScopefields on results returned by”;schema/2026-07-28/schema.ts:678“export interface DiscoverResult extends CacheableResult {”;schema/2026-07-28/schema.ts:1779“export interface ListToolsResult extends PaginatedResult, CacheableResult {”;schema/2026-07-28/schema.ts:1229“export interface ReadResourceResult extends CacheableResult {”;schema/2026-07-28/schema.ts:1634“export interface GetPromptResult extends Result {”;2026-07-28/server/utilities/caching.mdx:31
PG-09 ttlMs
- What. An integer number of milliseconds the result may be considered fresh, like
Cache-Control: max-age. Servers MUST send a value>= 0.0means immediately stale; absent means clients assume0; negative is treated as0. TTL is not a polling interval; clients that poll MUST add jitter and backoff. Alist_changednotification invalidates a fresh cached list. - Where. 2026 only.
- mcpx @ c58507a. Emitted by the server (
Cache.Listcovers discover and the list methods,Cache.Readcoversresources/read; a negative configured value is clamped to 0). Honoured by the client for modern upstreams:Pool.refreshSchemassets the catalogue deadline to fetch time + the minimum effective TTL across the lists actually fetched, where a list that declared nottlMs– or 0, or a negative – makes the catalogue stale immediately;Pool.Schemasre-lists once the deadline passes. Legacy upstreams keep a zero deadline: their cache never expires on a clock (#1601 Phase 1, PR #1608). - Value to mcpx. + mcpx’s own tool list is fixed for the daemon’s life, so a long TTL is honest for
tools/list;ttlMs: 0is always legal where freshness is unknown. - Effort. S.
- Risk. As PG-08.
- Detail. A server MAY send
ttlMswithoutlistChanged, relying on TTL alone. - Sources.
schema/2026-07-28/schema.ts:1094“ttlMs: number;”;2026-07-28/server/utilities/caching.mdx:56“- IfttlMsis absent, clients SHOULD assume a default of0(immediately stale)”;2026-07-28/server/utilities/caching.mdx:60“Servers MUST provide attlMsvalue that is>= 0.”;2026-07-28/server/utilities/caching.mdx:81
PG-10 cacheScope
- What.
"public": no user-specific data; any client, gateway or proxy MAY share it across users."private": reuse only within the same authorization context; caches MUST NOT be shared across contexts. Servers MUST NOT rely oncacheScopefor access control. - Where. 2026 only.
- mcpx @ c58507a. Emitted by the server per method (scope table in
internal/mcpserver/server.go). Honoured by the client at the disk boundary (#1601 Phase 2, PR #1705):Registry.SaveCachenever persists a private catalogue, public entries are written with their absolute deadline and scope, andloadCachenever seeds a private entry – a public entry whose deadline already passed seeds expired, so the first read re-lists. In memory, one private page marks the whole aggregated list private. - Value to mcpx. +
"private"is the honest answer for a per-user daemon’s resources;"public"fits mcpx’s own tool list, which is identical for every caller. As a gateway in front of several hosts mcpx must honour"private"from upstreams by keying any cache on the upstream credential. - Effort. S to emit; M to honour upstream scopes in a shared cache.
- Risk. High if gotten wrong:
"public"on a per-user result lets a shared cache leak one user’s resources to another. - Detail. The spec’s own guidance:
"public"for lists identical for all users,"private"for user-dependentresources/readresults and per-user filtered lists. - Sources.
schema/2026-07-28/schema.ts:1109“cacheScope: “;2026-07-28/server/utilities/caching.mdx:101;2026-07-28/server/utilities/caching.mdx:178“- MUST apply appropriate per-primitive access controls, and MUST NOT rely on”
PG-11 caching paginated lists
- What. Each page is cached independently: its own
ttlMsand freshness clock (servers MAY vary TTL per page), all pages of one list MUST share onecacheScope, an expired page is re-fetched by its cursor, and a cursor that becomes invalid means discarding every cached page and starting over. - Where. 2026 only.
- mcpx @ c58507a. Satisfied by construction, in the conservative direction: mcpx’s client aggregates a
list’s pages into a single catalogue cache, so there are no per-page clocks to keep – the aggregate takes
the minimum TTL declared on any page and is private if any page is private, and an expired catalogue is
re-fetched whole (all pages) rather than page by page. This is why the conformance gap
caching-page-refetchwas retired as not applicable by construction (#1604). mcpx’s silent restart on a bad cursor (PG-03) remains a server-side concern for its own caching clients. - Value to mcpx. + low.
- Effort. S.
- Risk. Low.
- Detail. None beyond the above.
- Sources.
2026-07-28/server/utilities/caching.mdx:153“- Each page response carries its ownttlMsvalue. The freshness clock for each page”;2026-07-28/server/utilities/caching.mdx:162;2026-07-28/server/utilities/caching.mdx:166“Servers MUST apply the samecacheScopeto all response pages for a given list”
PG-12 MRTR retries are never cached
- What.
input_requiredresults carry no cache hints, and results of retried requests — those carryinginputResponsesorrequestState— MUST NOT be cached, because they depend on inputs outside the cache key. - Where. 2026 only.
- mcpx @ c58507a. ✓ — mcpx caches no MRTR result in either direction, and the server stamps cache hints
only when the result’s
resultTypeis"complete", so aninput_requiredresult carries none. - Value to mcpx. + a guardrail for any future cache.
- Effort. S.
- Risk. None today.
- Detail. A
resources/readthat went through an MRTR round is cacheable by method but not by this rule. - Sources.
2026-07-28/server/utilities/caching.mdx:23-25;2026-07-28/server/utilities/caching.mdx:36“is, requests carryinginputResponsesorrequestState—MUST NOT be cached,”
PG-13 mcpx’s upstream schema cache ignores ttlMs
- What. A 2026 client MAY treat an upstream list as fresh for
ttlMsand SHOULD re-fetch once stale. - Where. 2026.
- mcpx @ c58507a. Fixed for the modern era by #1601 (Phases 1-2, PRs #1608 and #1705): the client’s list
result types carry the hint fields, the pool cache expires at the earliest declared deadline, and the disk
cache is scope-aware (PG-10). Legacy upstreams are unchanged by design – the TTL rules exist only in
2026-07-28, and legacy’s invalidation channel is its notifications. A
list_changednotification empties the cache in both eras (internal/daemon/hooks.goOnListChanged ->Pool.Invalidate), now covered bydaemon.TestListChangedInvalidatesPoolCache(#1604), which retired the conformance gapcaching-notification-invalidates. Two sibling gaps were retired with reasons in the same pass:caching-may-refetch-early-serve-stale– declined by decision on #1601 (refresh is synchronous on read; stale is served only when a refetch of an expired entry fails, #1603), andcaching-key-method-params– not applicable by construction (the pool cache is one catalogue per configured server, fetched by fixed list methods; it is never keyed by method+params andresources/readresults are not cached by the pool). - Value to mcpx. + fresher catalogues from modern upstreams, which today send mcpx no
list_changedat all because it never opens a listen stream (notifications register). - Effort. S.
- Risk. Stale catalogues until
mcpx refresh. - Detail.
cacheScope: "private"would matter only if mcpx shared one cache across authorization contexts; the cache is per configured server, and each server carries one credential. - Sources.
internal/pool/pool.go:446-455;internal/mcpclient/client.go:131-134;2026-07-28/changelog.mdx:36
PG-14 lists must not vary per connection
- What.
tools/list,prompts/listandresources/listMAY change over time but MUST NOT vary per connection or as a side effect of other requests; they MAY vary by the authorization presented on the request. - Where. 2026 only.
- mcpx @ 05c78b2. mcpx’s tool list is built once per daemon (
internal/cli/serve_ask.go:242) and resource and prompt listings come from per-server schema caches (internal/pool/pool.go:121-128), so no list depends on which connection asks. - Value to mcpx. + keeps mcpx cacheable; rules out “load a namespace’s tools into this session after the agent asks for it”, a pattern mcpx should not adopt over MCP.
- Effort. S (policy).
- Risk. Per-connection lazy tool expansion would break 2026 caching.
- Detail. This is the list half of the 2026 statelessness change; the ordering half is TOOL-23 in the tools register.
- Sources.
2026-07-28/server/tools.mdx:65“but MUST NOT vary”;2026-07-28/server/tools.mdx:67“MAY vary by the authorization presented on the request”;2026-07-28/server/prompts.mdx:60;2026-07-28/server/resources.mdx:77;internal/cli/serve_ask.go:242