{"record":{"id":"42aaa582abeb2f1a","repo":"Hmbown/CodeWhale","slug":"repeated-pagination-cursor-aborting","errorCode":null,"errorMessage":"{} repeated pagination cursor; aborting","messagePattern":"(.+?) repeated pagination cursor; aborting","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/mcp.rs","lineNumber":1339,"sourceCode":"                self.method,\n                MAX_MCP_CATALOG_ITEMS\n            );\n        }\n        if self.bytes > MAX_MCP_CATALOG_BYTES {\n            anyhow::bail!(\n                \"{} exceeded the {}-byte aggregate catalogue limit\",\n                self.method,\n                MAX_MCP_CATALOG_BYTES\n            );\n        }\n        let cursor = result\n            .get(\"nextCursor\")\n            .and_then(|value| value.as_str())\n            .map(str::to_owned);\n        if let Some(cursor) = cursor.as_ref()\n            && !self.seen_cursors.insert(cursor.clone())\n        {\n            anyhow::bail!(\"{} repeated pagination cursor; aborting\", self.method);\n        }\n        Ok(cursor)\n    }\n}\n\nfn is_legacy_sse_transport(config: &McpServerConfig) -> bool {\n    config\n        .transport\n        .as_deref()\n        .map(|transport| transport.trim().eq_ignore_ascii_case(\"sse\"))\n        .unwrap_or(false)\n}\n\npub fn validate_mcp_transport(transport: Option<&str>) -> Result<()> {\n    let Some(transport) = transport else {\n        return Ok(());\n    };\n    if transport.trim().eq_ignore_ascii_case(\"sse\") {","sourceCodeStart":1321,"sourceCodeEnd":1357,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L1321-L1357","documentation":"After each catalogue page, observe_page records the returned nextCursor in a HashSet; if a cursor that was already seen appears again, pagination would loop forever and the method aborts (crates/tui/src/mcp.rs:1332-1340). This is an infinite-loop guard against servers whose cursor generation regresses or echoes the cursor that was just sent.","triggerScenarios":"A server that ignores the cursor parameter and always returns the same first-page cursor; server state resetting mid-pagination (restart, cache eviction); a load balancer alternating between instances with independent cursor state.","commonSituations":"Stateless server implementations that do not really implement cursors; rolling restarts during discovery; sticky-session-less LBs in front of stateful MCP servers.","solutions":["Fix the server: cursors must advance and never repeat; opaque stateless tokens (e.g. encoding the offset) are the robust pattern.","If the catalogue is small, disable server-side pagination and return everything on one page with no nextCursor.","Ensure load-balanced instances share cursor state or use sticky sessions.","Retry after a server restart has settled - transient resets clear themselves."],"exampleFix":"# before (server bug): every page returns nextCursor=\"page-1\"\n#   tools/list repeated pagination cursor; aborting\n# after (server): cursor derived from a monotonic offset, never repeated\n#   cursor = base64(f\"{offset}:{catalogue_version}\")","handlingStrategy":"validation","validationCode":"#!/usr/bin/env bash\n# Walk the catalogue once and fail if any cursor repeats.\nurl=\"https://mcp.example.com/mcp\"; cursor=\"\"; seen=\"\"\nwhile :; do\n  body=$(curl -s \"$url\" -H 'content-type: application/json' \\\n    -d \"{\\\"jsonrpc\\\":\\\"2.0\\\",\\\"id\\\":1,\\\"method\\\":\\\"tools/list\\\"${cursor:+,\\\"params\\\":{\\\"cursor\\\":\\\"$cursor\\\"}}}\")\n  cursor=$(printf '%s' \"$body\" | jq -r '.result.nextCursor // empty')\n  [ -z \"$cursor\" ] && break\n  case \" $seen \" in *\" $cursor \"*) echo \"cursor repeated: $cursor\"; exit 1;; esac\n  seen=\"$seen $cursor\"\ndone; echo \"cursors unique\"","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Implement cursors as opaque stateless tokens derived from an offset.","Never echo the incoming cursor back as nextCursor on the same page.","Share cursor state across load-balanced instances or use sticky sessions."],"tags":["mcp","pagination","infinite-loop","cursor"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}