{"record":{"id":"054a67319a819829","repo":"Hmbown/CodeWhale","slug":"mcp-returned-a-non-string-nextcursor","errorCode":null,"errorMessage":"MCP {} returned a non-string nextCursor","messagePattern":"MCP (.+?) returned a non-string nextCursor","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/mcp/src/stdio_client.rs","lineNumber":393,"sourceCode":"        if self.items > self.max_items {\n            bail!(\n                \"MCP {} exceeded the {}-item catalog limit\",\n                self.method,\n                self.max_items\n            );\n        }\n        if self.bytes > self.max_bytes {\n            bail!(\n                \"MCP {} exceeded the {}-byte aggregate catalog limit\",\n                self.method,\n                self.max_bytes\n            );\n        }\n\n        let next_cursor = match page.get(\"nextCursor\") {\n            None => None,\n            Some(Value::String(cursor)) => Some(cursor.clone()),\n            Some(_) => bail!(\"MCP {} returned a non-string nextCursor\", self.method),\n        };\n        if let Some(cursor) = next_cursor.as_ref()\n            && !self.seen_cursors.insert(cursor.clone())\n        {\n            bail!(\"MCP {} repeated a pagination cursor\", self.method);\n        }\n        if next_cursor.is_some() && self.pages >= self.max_pages {\n            bail!(\n                \"MCP {} exceeded the {}-page catalog limit\",\n                self.method,\n                self.max_pages\n            );\n        }\n        Ok(next_cursor)\n    }\n}\n\n/// What the server said it supports in its `initialize` response.","sourceCodeStart":375,"sourceCodeEnd":411,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/mcp/src/stdio_client.rs#L375-L411","documentation":"Per the MCP pagination spec, nextCursor must be a string. observe_page strictly narrows the JSON field: absent is fine, a string is accepted, anything else (number, object, null) is a protocol violation and fails the listing rather than guessing.","triggerScenarios":"A resources/prompts/tools list response contains \"nextCursor\" with a non-string JSON value (e.g. null, a number, or an object).","commonSituations":"Server bug emitting null nextCursor to signal end-of-list instead of omitting the key; custom server implementations typing the cursor wrong; schema drift after upgrade.","solutions":["Fix the server to omit nextCursor entirely when there are no more pages (never emit null)","Emit nextCursor as a JSON string when more pages exist","Update/patch a proxy or fixture that rewrites the cursor value"],"exampleFix":"// before\n{\"resources\":[...],\"nextCursor\":null}\n// after\n{\"resources\":[...]}","handlingStrategy":"validation","validationCode":"fn next_cursor_is_valid(page: &serde_json::Value) -> bool {\n    match page.get(\"nextCursor\") {\n        None | Some(serde_json::Value::String(_)) => true,\n        _ => false,\n    }\n}","typeGuard":"fn is_valid_cursor(v: Option<&serde_json::Value>) -> bool {\n    matches!(v, None | Some(serde_json::Value::String(_)))\n}","tryCatchPattern":"match client.list_resources_with_metadata().await {\n    Ok(entries) => use(entries),\n    Err(e) if e.to_string().contains(\"non-string nextCursor\") => {\n        log::warn(\"server violated MCP cursor type; aborting listing\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Servers must omit nextCursor, never emit null, at end of listing","Validate pagination responses against the MCP schema in server tests","Check proxies/intermediaries do not rewrite cursor values"],"tags":["mcp","pagination","protocol"],"backgroundTag":"unexpected-response-shape","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T21:17:16.096Z"}