Hmbown/CodeWhale · error
MCP returned a non-string nextCursor
Error message
MCP {} returned a non-string nextCursor What it means
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.
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
Example fix
// before
{"resources":[...],"nextCursor":null}
// after
{"resources":[...]} Defensive patterns
Strategy: validation
Validate before calling
fn next_cursor_is_valid(page: &serde_json::Value) -> bool {
match page.get("nextCursor") {
None | Some(serde_json::Value::String(_)) => true,
_ => false,
}
} Type guard
fn is_valid_cursor(v: Option<&serde_json::Value>) -> bool {
matches!(v, None | Some(serde_json::Value::String(_)))
} Try / catch
match client.list_resources_with_metadata().await {
Ok(entries) => use(entries),
Err(e) if e.to_string().contains("non-string nextCursor") => {
log::warn("server violated MCP cursor type; aborting listing");
}
Err(e) => return Err(e),
} Prevention
- 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
When it happens
Trigger: A resources/prompts/tools list response contains "nextCursor" with a non-string JSON value (e.g. null, a number, or an object).
Common situations: 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.
Related errors
- exceeded the -page catalogue limit
- JSON-RPC line exceeded the
- MCP exceeded its overall deadline
- MCP exceeded the -item catalog limit
- MCP exceeded the -page catalog limit
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/054a67319a819829.
Report an issue: GitHub.
Appendix: source
Thrown at crates/mcp/src/stdio_client.rs:393
if self.items > self.max_items {
bail!(
"MCP {} exceeded the {}-item catalog limit",
self.method,
self.max_items
);
}
if self.bytes > self.max_bytes {
bail!(
"MCP {} exceeded the {}-byte aggregate catalog limit",
self.method,
self.max_bytes
);
}
let next_cursor = match page.get("nextCursor") {
None => None,
Some(Value::String(cursor)) => Some(cursor.clone()),
Some(_) => bail!("MCP {} returned a non-string nextCursor", self.method),
};
if let Some(cursor) = next_cursor.as_ref()
&& !self.seen_cursors.insert(cursor.clone())
{
bail!("MCP {} repeated a pagination cursor", self.method);
}
if next_cursor.is_some() && self.pages >= self.max_pages {
bail!(
"MCP {} exceeded the {}-page catalog limit",
self.method,
self.max_pages
);
}
Ok(next_cursor)
}
}
/// What the server said it supports in its `initialize` response.View on GitHub (pinned to 73e0f67d83)