koala73/worldmonitor · error · SafeWebMcpError

Dashboard tab result exceeded the safe output limit.

Error message

Dashboard tab result exceeded the safe output limit.

What it means

boundDashboardTabMutation sanitizes a tab mutation result (create/rename/delete/select), coercing counts, capping lockReason to 32 chars, and then asserts the serialized result fits MAX_OUTPUT_CHARS. If it still exceeds the budget it throws this SafeWebMcpError. Mutation results are tiny by design, so hitting this indicates the result structurally ballooned beyond what the bounder can trim.

Solutions

  1. Retry the mutation once; this is not expected in normal operation and may be transient state.
  2. Check whether you are on a dashboard version that added fields to DashboardTabMutationResult; upgrade or downgrade to a matched pair of tool/app versions.
  3. Shorten the tab name/id you are mutating to rule out oversize user input.
  4. Report a bug with the tool name and args — the bounder should always produce a sub-budget mutation result.

Example fix

// before
await tools.call('rename_dashboard_tab', { id, name: 'x'.repeat(5000) });
// after
await tools.call('rename_dashboard_tab', { id, name: name.slice(0, 64) });
Defensive patterns

Strategy: try-catch

Validate before calling

function validTabMutationArgs(args) {
  return typeof args.id === 'string' && args.id.length <= 128
    && (args.name === undefined || (typeof args.name === 'string' && args.name.length <= 64));
}

Try / catch

try {
  return await tools.call('rename_dashboard_tab', { id, name });
} catch (e) {
  if (e.message.includes('tab result exceeded')) {
    // unexpected: mutation results are tiny; log and retry once, then report
    console.error('oversized tab mutation result', { id, name: name.length });
    return tools.call('rename_dashboard_tab', { id, name: name.slice(0, 32) });
  }
  throw e;
}

Prevention

When it happens

Trigger: A create_dashboard_tab / rename_dashboard_tab / delete_dashboard_tab / select_dashboard_tab call whose bounded result object still serializes above MAX_OUTPUT_CHARS — practically only when the result carries oversized fields the bounder does not truncate.

Common situations: A dashboard version returning extra unbounded fields in the mutation result; extremely long tab ids or lockReason variants; a regression where the bounder's field whitelist stopped covering a new large field.

Related errors


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

Appendix: source

Thrown at src/services/webmcp.ts:1560

    ...(typeof result.effectiveCollapsed === 'boolean'
      ? { effectiveCollapsed: result.effectiveCollapsed }
      : {}),
    ...(typeof result.requestedFullscreen === 'boolean'
      ? { requestedFullscreen: result.requestedFullscreen }
      : {}),
    ...(typeof result.effectiveFullscreen === 'boolean'
      ? { effectiveFullscreen: result.effectiveFullscreen }
      : {}),
    ...(ok ? { changed: result.changed === true } : { changed: false }),
    ...(result.unchanged === true ? { unchanged: true } : {}),
    ...(typeof result.persisted === 'boolean' ? { persisted: result.persisted } : {}),
    ...(!ok && reason ? { reason } : {}),
    message: boundedText(result.message, 160) || (ok ? 'Panel layout updated.' : 'Panel layout change denied.'),
  };
}

function boundDashboardTabList(
  snapshot: DashboardTabListSnapshot,
  cursor?: string,
): DashboardTabActionResult {
  const sourceTabs = Array.isArray(snapshot.tabs) ? snapshot.tabs : [];
  const tabs = sourceTabs.map((tab) => ({
    id: boundedText(tab?.id, 64),
    name: boundedText(tab?.name, DASHBOARD_TAB_NAME_MAX_LENGTH),
    active: tab?.active === true,
    canDelete: tab?.canDelete === true,
  })).filter((tab) => tab.id);
  const tabCount = Math.max(
    Math.max(0, Math.floor(boundedNumber(snapshot.tabCount)) || tabs.length),
    tabs.length,
  );
  const activeTabId = boundedText(snapshot.activeTabId, 64);
  const canCreate = snapshot.canCreate === true;
  const cap = snapshot.cap === null || typeof snapshot.cap === 'number' ? snapshot.cap : null;
  const createBlockReason = snapshot.createBlockReason
    ? boundedText(snapshot.createBlockReason, 32) as DashboardTabListSnapshot['createBlockReason']

View on GitHub (pinned to e586b8b4b8)