PrefectHQ/fastmcp · error · ValueError
Invalid cursor: {cursor}
Error message
Invalid cursor: {cursor} What it means
FastMCP's pagination cursor is an opaque base64-encoded, signed JSON token; `decode` raised this ValueError because the supplied cursor could not be decoded or did not have the expected structure (the underlying JSONDecodeError/KeyError/TypeError/binascii.Error/ValueError is chained). It means the cursor is corrupt, truncated, or was not produced by `encode`/`paginate_sequence`.
Source
Thrown at fastmcp_slim/fastmcp/utilities/pagination.py:47
@classmethod
def decode(cls, cursor: str) -> CursorState:
"""Decode cursor from an opaque string.
Raises:
ValueError: If the cursor is invalid or malformed.
"""
try:
data = json.loads(base64.urlsafe_b64decode(cursor.encode()).decode())
return cls(offset=data["o"])
except (
json.JSONDecodeError,
KeyError,
ValueError,
TypeError,
binascii.Error,
) as e:
raise ValueError(f"Invalid cursor: {cursor}") from e
def paginate_sequence(
items: Sequence[T],
cursor: str | None,
page_size: int,
) -> tuple[list[T], str | None]:
"""Paginate a sequence of items.
Args:
items: The full sequence to paginate.
cursor: Optional cursor from a previous request. None for first page.
page_size: Maximum number of items per page.
Returns:
Tuple of (page_items, next_cursor). next_cursor is None if no more pages.
Raises:View on GitHub (pinned to 1f02114297)
Solutions
- Discard the bad cursor and restart pagination from the beginning (`cursor=None`) to obtain a fresh valid token.
- Ensure the cursor is passed opaquely — no URL decoding/encoding, trimming, or re-serialization between server response and next request.
- If cursors were persisted across a FastMCP upgrade, invalidate stored cursors and re-paginate with the current library version.
Example fix
// before const page = await client.listTools(decodeURIComponent(cursor)); // after (pass opaquely, or restart) const page = cursor ? await client.listTools(cursor) : await client.listTools();
Defensive patterns
Strategy: try-catch
Validate before calling
def cursor_looks_valid(cursor: str | None) -> bool:
if cursor is None:
return True
import base64, binascii
try:
base64.urlsafe_b64decode(cursor)
return True
except (binascii.Error, ValueError):
return False Type guard
def is_opaque_cursor(v: object) -> bool:
return isinstance(v, str) and len(v) > 0 and not v.isdigit() Try / catch
try:
result = paginate_sequence(items, cursor, page_size)
except ValueError as e:
if "Invalid cursor" in str(e):
cursor = None # restart pagination from the beginning
result = paginate_sequence(items, cursor, page_size)
else:
raise Prevention
- Treat cursors as fully opaque — never decode, trim, or URL-transform them.
- Don't persist cursors across library upgrades; store the original query instead.
- Restart pagination from cursor=None on any invalid-cursor error instead of retrying the bad token.
When it happens
Trigger: Passing a cursor string that was hand-crafted, URL-decoded/re-encoded incorrectly, truncated by a client, generated by an older/different FastMCP version with an incompatible format, or any non-cursor garbage string in the `cursor` argument of `paginate_sequence` or a paginated MCP `list_*` call.
Common situations: Clients mangling the opaque token (e.g. treating it as a page number), storing cursors in systems that strip padding or change encoding, upgrading FastMCP so old persisted cursors no longer decode, or users typing cursor values by hand.
Related errors
- Horizon returned an invalid organization cursor
- [{self.name}] Reached auto-pagination limit ({max_pages} pag
- [{self.name}] Reached auto-pagination limit ({max_pages} pag
- [{self.name}] Reached auto-pagination limit ({max_pages} pag
- [{self.name}] Reached auto-pagination limit ({max_pages} pag
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/722b768b0315803a.
Report an issue: GitHub.