Skip to content

Server list handlers ignore cursor and never set nextCursor, and cannot be replaced #1128

Closed as duplicate

Description

@ujibang

Version: 2.0.1 (latest at the time of writing)

What happens

All four server-side list handlers in McpAsyncServer build their result the same way — read the whole registry, map, done:

  • toolsListRequestHandler() — tools.stream().map(...).toList() → ListToolsResult.builder(list).build()
  • promptsListRequestHandler() — prompts.values().stream()... → ListPromptsResult.builder(list).build()
  • resourcesListRequestHandler() — resources.values().stream()... → ListResourcesResult.builder(list).build()
  • resourceTemplateListRequestHandler() — same shape

None of them reads the cursor from the request, and none sets nextCursor on the result — although every one of those result records already carries a nextCursor component. The data model is there; the handlers never use it.

Why this is worth reporting

To be clear about what is not being claimed: returning every entry with no nextCursor is a complete, valid response. The spec says these operations support pagination, not that a server must paginate, and a client that receives no cursor correctly stops after one page. This is a missing capability, not a protocol violation.

What does seem worth fixing:

  1. A server author cannot implement it either. McpServer.AsyncSpecification / SyncSpecification expose tools, resources, resourceTemplates, prompts, completions, capabilities, instructions, requestTimeout, jsonMapper, jsonSchemaValidator, rootsChangeHandler(s) — and no way to supply or replace a request handler. A server with a large catalog has no option inside the SDK.

  2. The client half is implemented. McpAsyncClient exposes listResources(String cursor), listTools(String cursor) and their siblings, so the SDK can send a cursor and follow a nextCursor — one that no SDK-based server will ever produce.

  3. A cursor sent by a client is silently ignored rather than rejected: the request succeeds and returns page one again. Harmless in practice — without a nextCursor no well-behaved client sends one — but it is an argument accepted and dropped.

That it is identical across all four handlers suggests a design state rather than an oversight in one place, which is why this is filed against the set rather than against resources/list.

Where the earlier request went

#746 asked for cursor-based pagination (alongside a tools repository) and was closed as a duplicate of #578. #578 is about pluggable repository APIs for dynamic, per-client content — a different concern, and its description does not cover pagination. The pagination half appears to have been lost in the deduplication.

Concrete case

RESTHeart exposes MCP resources derived from MongoDB collections: one entry per collection an operator opts in, plus per-collection entries for a single document and for the document count. A deployment that opts in broadly produces a resources/list response that grows linearly with the database, returned in one message with no way to page it.

We are not blocked — the catalog size is under the operator's control — and we are not asking for a workaround. We would rather not re-slice the SDK's response in our own transport, which is why we are reporting it instead.

What would help

Either the handlers reading cursor and setting nextCursor with a server-configurable page size, or a hook that lets a server provide its own list handler. Either one closes it; the second also covers per-client filtering, which is what #578 is about.

Activity

  1. ujibang commented on Sep 9, 2026

    @ujibang
    Author

    A follow-up, for the record, because we have since done the thing this issue says we would rather not do.

    We needed resources/list and resources/templates/list to be filtered per caller: a client authorized to reach our MCP endpoint could enumerate every MCP-enabled resource on the server — URIs, action names including the writing ones, parameter names, and each resource's human-written description, which is prose whose whole purpose is to explain what the data is. Reads were already authorized individually; the listing was not.

    With no way to supply a list handler, and #578 (per-caller repositories) targeted at 3.0, we ended up filtering the SDK's result in our own transport after all. So the sentence above — "we would rather not re-slice the SDK's response in our own transport" — no longer describes what we do, and we would rather say so than leave a contradiction sitting in an open issue.

    What changed our mind is that pagination and confidentiality are not the same trade. Returning a whole catalog in one message is a performance question and we can live with it. Returning a catalog that discloses what the caller cannot read is not something we can leave until 3.0.

    Two things we would flag for anyone weighing the same workaround:

    • It couples the filter to the SDK's result shape. A filter that silently stops matching is worse than no filter, because it still looks like it works — ours logs a warning and leaves the response untouched if the payload is not the shape it expects, rather than passing everything through quietly.
    • It only became possible once we stopped answering those methods as an event stream. They produce exactly one message, so we now return a plain JSON response and let our own framework's response pipeline see it. That is also what the Streamable HTTP spec suggests for a response with nothing to stream, so it was worth doing on its own.

    None of this changes the request. A hook for supplying a list handler — or #578's repositories — would let us delete the workaround, and the second one would cover the pagination half of this issue at the same time.

  2. Kehrlann commented on Sep 10, 2026

    @Kehrlann
    Contributor

    Duplicate of #967 re: pagination with fixed page size

    Repositories, as mentioned in #578 , will be implemented for the new version of the spec

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions