PrefectHQ/fastmcp · error · MCPError

INVALID_PARAMS

INVALID_PARAMS

Error message

str(e) (from paginate_sequence ValueError, wrapped as INVALID_PARAMS)

What it means

Raised as MCPError with JSON-RPC code INVALID_PARAMS by _apply_pagination when paginate_sequence() raises ValueError while slicing the items list. This means the client-supplied pagination cursor was malformed (not a valid base64/encoded offset for the sequence), so the list_* operation cannot resolve the page. The original ValueError text is preserved as the message.

Source

Thrown at fastmcp_slim/fastmcp/server/mixins/mcp_operations.py:66

PaginateT = TypeVar("PaginateT")


def _apply_pagination(
    items: Sequence[PaginateT],
    cursor: str | None,
    page_size: int | None,
) -> tuple[list[PaginateT], str | None]:
    """Apply pagination to items, raising MCPError for invalid cursors.

    If page_size is None, returns all items without pagination.
    """
    if page_size is None:
        return list(items), None
    try:
        return paginate_sequence(items, cursor, page_size)
    except ValueError as e:
        raise MCPError(code=INVALID_PARAMS, message=str(e)) from e


def _normalize_call_tool_result(
    result: Any,
) -> mcp_types.CallToolResult:
    """Normalize a tool's ``to_mcp_result()`` output into a ``CallToolResult``.

    ``ToolResult.to_mcp_result()`` returns one of three shapes for backward
    compatibility: a ``CallToolResult`` (error/meta case), a bare
    ``list[ContentBlock]`` (unstructured), or a ``(content, structured)`` tuple.
    The SDK v2 runner requires a ``BaseModel`` result, so wrap the shorthand
    forms here (the SDK's old ``call_tool`` decorator used to do this).
    """
    if isinstance(result, mcp_types.CallToolResult):
        return result
    if isinstance(result, tuple):
        content, structured = result
        return mcp_types.CallToolResult(content=content, structured_content=structured)

View on GitHub (pinned to 1f02114297)

Solutions

  1. Reuse the nextCursor value returned by the previous list_* response verbatim — never fabricate or edit cursors.
  2. Keep page_size constant across the pages of one listing; changing it invalidates cursors.
  3. If a cursor is stale or unknown, restart pagination from the first page (cursor=None) with the same page_size.

Example fix

// before: hand-made cursor
page = await client.list_tools(cursor="2")

// after: echo the server's cursor
first = await client.list_tools(page_size=10)
if first.next_cursor:
    page = await client.list_tools(cursor=first.next_cursor, page_size=10)
Defensive patterns

Strategy: fallback

Validate before calling

def is_valid_cursor(cursor: str | None) -> bool:
    if cursor is None:
        return True
    import base64
    try:
        base64.b64decode(cursor, validate=True)
        return True
    except Exception:
        return False

if not is_valid_cursor(cursor):
    cursor = None  # restart pagination from the first page

Try / catch

try:
    page = await client.list_tools(cursor=cursor, page_size=page_size)
except Exception as e:
    if "INVALID_PARAMS" in str(e) or "cursor" in str(e).lower():
        cursor = None  # discard stale/invalid cursor, restart from page 1
        page = await client.list_tools(page_size=page_size)
    else:
        raise

Prevention

When it happens

Trigger: Calling list_tools / list_resources / list_resource_templates / list_prompts with a cursor argument that is not a valid cursor previously returned by the server — e.g. a hand-crafted string, a cursor from a different list with different page_size, or a truncated/garbled cursor.

Common situations: Manually constructing cursors instead of echoing the server's nextCursor; persisting cursors across server restarts or page_size changes; page_size differing between the call that issued the cursor and the call reusing it.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/d154c0f0b98abfbe. Report an issue: GitHub.