mcp: harden paginate against infinite loops and param mutation

DO
3mo ago
+238-9

The paginate() generic helper used by Tools(), Resources(), ResourceTemplates(), and Prompts() iterators has no safety guards against:

  1. Unbounded pagination — a buggy server returning a non-empty NextCursor on every page causes an infinite loop and unbounded memory/CPU consumption.
  2. Cursor cycles — a server returning a previously-seen cursor causes the client to loop forever over the same pages.
  3. Caller param mutation — paginate() writes the next cursor back into the caller's params struct, which is surprising behavior and can cause subtle bugs if the caller reuses the params.

The TypeScript SDK already protects against these cases in its _listAllPages helper with a listMaxPages=64 limit and a seen cursor set. This change brings the Go SDK to parity.

ref: https://github.com/modelcontextprotocol/typescript-sdk/pull/2336
ref: https://github.com/modelcontextprotocol/python-sdk/pull/3156

Changes:

  • Add ClientOptions.ListMaxPages (default 64 when zero; negative means unlimited), passed through to paginate() by all four iterator methods.
  • Track seen cursors in a map; return an error when a cycle is detected.
  • Use reflect.New to shallow-copy the caller's params struct so the original is never mutated.
  • Add tests for the default limit, explicit limit, unlimited mode, cursor cycle detection, and params non-mutation.

Behavioral Change Note

This PR introduces a default page limit of 64, which is a behavioral change for existing users. Previously, pagination was unbounded. This is a security hardening fix — without a limit, a misbehaving or buggy server can cause the client to enter an infinite loop, consuming unbounded CPU and memory (CVE-worthy denial-of-service vector).

The default of 64 aligns with:

  • TypeScript SDK: DEFAULT_LIST_MAX_PAGES = 64 (ref)
  • Python SDK: same limit (ref)

Users who need more pages can explicitly set ListMaxPages to a higher value or -1 for unlimited.