{"record":{"id":"806da0c9c6e53deb","repo":"koala73/worldmonitor","slug":"list-dashboard-panels-accepts-only-variant-category-enabled","errorCode":null,"errorMessage":"list_dashboard_panels accepts only variant, category, enabled, available, cursor, and limit.","messagePattern":"list_dashboard_panels accepts only variant, category, enabled, available, cursor, and limit\\.","errorType":"exception","errorClass":"SafeWebMcpError","httpStatus":null,"severity":"error","filePath":"src/services/webmcp.ts","lineNumber":2032,"sourceCode":"            description: 'Catalog cursor from the previous page nextCursor.',\n            minLength: 1,\n            maxLength: DASHBOARD_PANEL_ID_MAX_CHARS,\n            pattern: DASHBOARD_PANEL_ID_PATTERN,\n          },\n          limit: {\n            type: 'integer',\n            description: 'Maximum panels in this page, from 1 to 8.',\n            minimum: 1,\n            maximum: DASHBOARD_PANEL_CATALOG_MAX_LIMIT,\n            default: DASHBOARD_PANEL_CATALOG_DEFAULT_LIMIT,\n          },\n        },\n        additionalProperties: false,\n      },\n      annotations: { readOnlyHint: true },\n      execute: withBindings(WEBMCP_SPA_TOOL.listDashboardPanels, async (args, extra) => {\n        if (!hasOnlyOwnKeys(args, ['variant', 'category', 'enabled', 'available', 'cursor', 'limit'])) {\n          throw new SafeWebMcpError(\n            'list_dashboard_panels accepts only variant, category, enabled, available, cursor, and limit.',\n            'validation',\n          );\n        }\n        const query: DashboardPanelCatalogQuery = {};\n        if (args.variant !== undefined) query.variant = args.variant as string;\n        if (args.category !== undefined) query.category = args.category as string;\n        if (args.enabled !== undefined) query.enabled = args.enabled as boolean;\n        if (args.available !== undefined) query.available = args.available as boolean;\n        if (args.cursor !== undefined) query.cursor = args.cursor as string;\n        if (args.limit !== undefined) query.limit = args.limit as number;\n        return boundDashboardPanelCatalog(await app.listDashboardPanels(query, extra));\n      }, trackEvent, {\n        successMetadata: (_args, value) => {\n          const result = value as DashboardPanelCatalogPage;\n          return {\n            resultCount: result.panels.length,\n            hasMore: result.hasMore === true,","sourceCodeStart":2014,"sourceCodeEnd":2050,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/src/services/webmcp.ts#L2014-L2050","documentation":"The WebMCP tool list_dashboard_panels is registered with a strict input schema (additionalProperties: false). Before executing the tool, the handler checks the exact own-key set of the arguments object via hasOnlyOwnKeys and throws SafeWebMcpError when any unrecognized key is present, keeping the tool surface minimal and predictable for MCP clients.","triggerScenarios":"Calling list_dashboard_panels with any property outside variant, category, enabled, available, cursor, limit — e.g. misspelled names like variants, category, startCursor, page, or extra keys such as search or id.","commonSituations":"Developers guessing parameter names instead of reading the tool's JSON schema; clients auto-generating arguments from stale tool metadata; copying query params from other catalog tools (e.g. pagination keys named differently); SDK version drift where an older schema allowed more fields.","solutions":["Remove all arguments except variant, category, enabled, available, cursor, and limit from the tool call.","Check the tool's inputSchema (properties block above) for exact names and types of the six allowed keys.","Refresh the MCP client's tool listing so the schema it uses matches the server's current registration."],"exampleFix":"// before\nconst res = await mcp.callTool('list_dashboard_panels', { category: 'military', search: 'radar' });\n// after\nconst res = await mcp.callTool('list_dashboard_panels', { category: 'military', limit: 20 });","handlingStrategy":"validation","validationCode":"const ALLOWED = ['variant','category','enabled','available','cursor','limit'];\nconst extra = Object.keys(args ?? {}).filter(k => !ALLOWED.includes(k));\nif (extra.length) throw new Error(`Unknown list_dashboard_panels args: ${extra.join(', ')}`);","typeGuard":"const isPanelQuery = (a: unknown): a is Record<string, string|boolean|number> =>\n  a !== null && typeof a === 'object' &&\n  Object.keys(a).every(k => ['variant','category','enabled','available','cursor','limit'].includes(k));","tryCatchPattern":"try {\n  const res = await mcp.callTool('list_dashboard_panels', query);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('accepts only')) {\n    console.error('Bad arguments, allowed keys: variant, category, enabled, available, cursor, limit', e.message);\n  } else throw e;\n}","preventionTips":["Build arguments from the tool's inputSchema properties instead of by hand.","Keep a shared constant of allowed keys in client code.","Re-list tools after server upgrades to pick up schema changes.","Type the args object in TypeScript with a strict keyof union."],"tags":["mcp","validation","strict-schema"],"backgroundTag":"invalid-query-parameter","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}