koala73/worldmonitor · error · DashboardPanelCatalogError

invalid_cursor

invalid_cursor

Error message

cursor is not a valid catalog cursor.

What it means

Pagination cursors in listDashboardPanelCatalog() are the panel id of the last item on the previous page. A cursor must be a string of at most DASHBOARD_PANEL_ID_MAX_CHARS chars, match the panel-id pattern (PANEL_ID_RE), and exist in CANONICAL_PANEL_ID_SET (canonical panel ids, excluding cw-/mcp- internal ids). Anything else throws DashboardPanelCatalogError with reason 'invalid_cursor'.

Source

Thrown at src/services/webmcp-panel-catalog.ts:193

      'enabled must be a boolean.',
    );
  }
  if (query.available !== undefined && typeof query.available !== 'boolean') {
    throw new DashboardPanelCatalogError(
      'malformed_arguments',
      'available must be a boolean.',
    );
  }

  const cursor = query.cursor;
  if (cursor !== undefined) {
    if (
      typeof cursor !== 'string'
      || cursor.length > DASHBOARD_PANEL_ID_MAX_CHARS
      || !PANEL_ID_RE.test(cursor)
      || !CANONICAL_PANEL_ID_SET.has(cursor)
    ) {
      throw new DashboardPanelCatalogError(
        'invalid_cursor',
        'cursor is not a valid catalog cursor.',
      );
    }
  }

  const limit = query.limit ?? DASHBOARD_PANEL_CATALOG_DEFAULT_LIMIT;
  if (
    !Number.isInteger(limit)
    || limit < 1
    || limit > DASHBOARD_PANEL_CATALOG_MAX_LIMIT
  ) {
    throw new DashboardPanelCatalogError(
      'invalid_limit',
      `limit must be an integer from 1 to ${DASHBOARD_PANEL_CATALOG_MAX_LIMIT}.`,
    );
  }

View on GitHub (pinned to 9361220cc0)

Solutions

  1. Only use the exact nextCursor value returned by the previous listDashboardPanelCatalog response; never construct cursors manually.
  2. If a stored cursor fails, discard it and restart pagination from the first page (omit cursor).
  3. Validate the candidate cursor against getCanonicalDashboardPanelIds() (getCanonicalDashboardPanelIds().includes(cursor)) before calling.
  4. Catch DashboardPanelCatalogError with reason 'invalid_cursor' and transparently retry without the cursor.

Example fix

// before
listDashboardPanelCatalog(live, { cursor: savedState.cursor ?? 'start' });
// after
const cursor = savedState.cursor && getCanonicalDashboardPanelIds().includes(savedState.cursor)
  ? savedState.cursor
  : undefined;
listDashboardPanelCatalog(live, cursor ? { cursor } : {});
Defensive patterns

Strategy: try-catch

Validate before calling

import { getCanonicalDashboardPanelIds } from '@/services/webmcp-panel-catalog';
const validCursor = typeof cursor === 'string' && cursor.length <= 96
  && getCanonicalDashboardPanelIds().includes(cursor)
  ? cursor
  : undefined;

Type guard

function isValidCatalogCursor(value: unknown): value is string {
  return typeof value === 'string'
    && value.length <= 96
    && getCanonicalDashboardPanelIds().includes(value);
}

Try / catch

try {
  return listDashboardPanelCatalog(live, { cursor, ...query });
} catch (err) {
  if (err instanceof DashboardPanelCatalogError && err.reason === 'invalid_cursor') {
    return listDashboardPanelCatalog(live, query); // restart from page 1
  }
  throw err;
}

Prevention

When it happens

Trigger: Passing cursor values that are opaque tokens ('page2', base64), an id filtered out of the canonical set (a 'cw-' or 'mcp-' panel id), an id longer than 96 chars, a malformed/empty string, or a panel id that existed when the cursor was issued but was later removed from ALL_PANELS.

Common situations: Inventing cursors instead of using nextCursor from the previous page; persisting cursors across releases where the panel registry changed; passing an internal (cw-/mcp-) panel id obtained from another subsystem.

Related errors


AI-assisted analysis of koala73/worldmonitor@9361220cc0 (2026-09-01). Data as JSON: /api/errors/c1944a6fb95013d6. Report an issue: GitHub.