{"record":{"id":"948310ee7cc274d4","repo":"koala73/worldmonitor","slug":"dashboard-panel-catalog-exceeded-the-safe-output-l","errorCode":null,"errorMessage":"Dashboard panel catalog exceeded the safe output limit.","messagePattern":"Dashboard panel catalog exceeded the safe output limit\\.","errorType":"error_code","errorClass":"SafeWebMcpError","httpStatus":null,"severity":"error","filePath":"src/services/webmcp.ts","lineNumber":1008,"sourceCode":"    },\n  };\n\n  const collections = [enabled, mounted, enabledLayers];\n  while (JSON.stringify(result).length > maxChars) {\n    const candidate = collections\n      .filter((collection) => collection.length > 0)\n      .sort((left, right) => (\n        right.reduce((sum, item) => sum + item.length, 0)\n        - left.reduce((sum, item) => sum + item.length, 0)\n      ))[0];\n    if (!candidate) break;\n    candidate.pop();\n  }\n\n  result.map.layersTruncated = enabledLayers.length < result.map.enabledLayerCount;\n  result.panels.mountedTruncated = mounted.length < result.panels.mountedCount;\n  result.panels.enabledTruncated = enabled.length < result.panels.enabledCount;\n  return result;\n}\n\nfunction boundUnavailableReason(value: unknown): DashboardPanelUnavailableReason | undefined {\n  if (value === 'panel_not_entitled' || value === 'panel_disabled' || value === 'panel_not_live') {\n    return value;\n  }\n  return undefined;\n}\n\nfunction boundDashboardPanelCatalog(result: DashboardPanelCatalogPage): DashboardPanelCatalogPage {\n  const panels = (Array.isArray(result.panels) ? result.panels : []).map((panel) => {\n    const available = panel?.available === true;\n    const unavailableReason = available ? undefined : boundUnavailableReason(panel?.unavailableReason);\n    const bounded: DashboardPanelCatalogItem = {\n      id: boundedText(panel?.id, DASHBOARD_PANEL_ID_MAX_CHARS),\n      label: boundedText(panel?.label, DASHBOARD_PANEL_LABEL_MAX_CHARS),\n      category: boundedText(panel?.category, DASHBOARD_PANEL_CATEGORY_MAX_CHARS),\n      variants: Array.isArray(panel?.variants)","sourceCodeStart":990,"sourceCodeEnd":1026,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/src/services/webmcp.ts#L990-L1026","documentation":"boundDashboardPanelsResult caps the panel catalog response (pagination, popping panels, setting hasMore/nextCursor) and then verifies the final JSON fits MAX_OUTPUT_CHARS. If even the bounded payload exceeds the safe output limit, it throws this SafeWebMcpError rather than emitting an oversized MCP response. It protects the WebMCP channel from unbounded catalog data.","triggerScenarios":"Calling list_dashboard_panels when the serialized (even fully paginated) panel catalog still exceeds MAX_OUTPUT_CHARS — e.g. an enormous number of panels with very long descriptions/names such that a single page cannot fit under the budget.","commonSituations":"Environments with many custom/registered panels with verbose metadata; a limit parameter that yields a page too large for the remaining budget; catalog entries with unusually long localized strings.","solutions":["Request smaller pages by passing a lower `limit` to list_dashboard_panels and paginate via the returned nextCursor.","Narrow the query with `variant`/`category`/`enabled`/`available` filters so fewer, smaller entries are returned.","Shorten panel catalog metadata (titles/descriptions) at registration time if you control the panels.","If legitimately unavoidable, this is a bug-report condition: the bounder should always be able to produce a page under budget."],"exampleFix":"// before\nconst all = await tools.call('list_dashboard_panels', {});\n// after\nlet cursor;\nconst panels = [];\ndo {\n  const page = await tools.call('list_dashboard_panels', { limit: 10, cursor });\n  panels.push(...page.panels);\n  cursor = page.nextCursor;\n} while (cursor);","handlingStrategy":"validation","validationCode":"const ALLOWED = ['variant', 'category', 'enabled', 'available', 'cursor', 'limit'];\nfunction validPanelQuery(args) {\n  return Object.keys(args).every((k) => ALLOWED.includes(k))\n    && (args.limit === undefined || (Number.isInteger(args.limit) && args.limit > 0 && args.limit <= 50));\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await tools.call('list_dashboard_panels', { limit: 20, cursor });\n} catch (e) {\n  if (e.message.includes('exceeded the safe output limit')) {\n    return tools.call('list_dashboard_panels', { limit: 5, cursor, category: narrowCategory });\n  }\n  throw e;\n}","preventionTips":["Always paginate with a modest limit and follow nextCursor.","Filter by variant/category/enabled/available to shrink result payloads.","Keep panel registration metadata (titles/descriptions) short.","Cap your own limit parameter well below the channel budget."],"tags":["webmcp","output-budget","pagination","catalog"],"backgroundTag":"response-size-limit-exceeded","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-01T10:32:37.851Z","contentChangedAt":"2026-09-01T10:32:37.851Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}