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

  1. Fix the server to omit nextCursor entirely when there are no more pages (never emit null)
  2. Emit nextCursor as a JSON string when more pages exist
  3. 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

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


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)