From bc1778184215858b80d9c22c5d5f7c095a22d539 Mon Sep 17 00:00:00 2001 From: Alex Fedotyev <61838744+alex-fedotyev@users.noreply.github.com> Date: Wed, 24 Jun 2026 02:27:40 +0000 Subject: [PATCH] feat(api): support number-tile backgroundChart in the MCP dashboard tools Add backgroundChart ({ type: line | area, color? }) to the builder number-tile config in clickstack_save_dashboard and clickstack_patch_dashboard, reusing BackgroundChartSchema from common-utils. The MCP tools already share the external-api conversion, so this is schema + prompt + tests only, no conversion edits. Builder number tiles only: raw SQL number tiles return a single value with no time dimension to bucket, so backgroundChart is not on their schema (a sent value is stripped on save). Mirrors number-tile color in the MCP tools (#2480). Co-Authored-By: Claude Opus 4.8 --- .../mcp-number-tile-background-chart.md | 5 ++ .../dashboards/patchDashboard.test.ts | 54 ++++++++++++ .../dashboards/saveDashboard.test.ts | 85 +++++++++++++++++++ .../api/src/mcp/prompts/dashboards/content.ts | 17 ++++ .../api/src/mcp/tools/dashboards/schemas.ts | 13 +++ 5 files changed, 174 insertions(+) create mode 100644 .changeset/mcp-number-tile-background-chart.md diff --git a/.changeset/mcp-number-tile-background-chart.md b/.changeset/mcp-number-tile-background-chart.md new file mode 100644 index 0000000000..e14f2f71aa --- /dev/null +++ b/.changeset/mcp-number-tile-background-chart.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/api": patch +--- + +Add `backgroundChart` support to number tiles in the MCP dashboard tools (`clickstack_save_dashboard` and `clickstack_patch_dashboard`). Builder number tiles can now carry an optional background trend sparkline (`type` line or area, with an optional palette-token `color`), matching the dashboard editor and the v2 REST API. Raw SQL number tiles do not support it. diff --git a/packages/api/src/mcp/__tests__/dashboards/patchDashboard.test.ts b/packages/api/src/mcp/__tests__/dashboards/patchDashboard.test.ts index fe6cb93c68..f318568c07 100644 --- a/packages/api/src/mcp/__tests__/dashboards/patchDashboard.test.ts +++ b/packages/api/src/mcp/__tests__/dashboards/patchDashboard.test.ts @@ -651,6 +651,60 @@ describe('MCP Dashboard Tools - clickstack_patch_dashboard', () => { ]); }); + it('should round-trip backgroundChart: patch then get_dashboard_tile', async () => { + const sourceId = ctx.traceSource._id.toString(); + const createResult = await callTool( + ctx.client!, + 'clickstack_save_dashboard', + { + name: 'Background Chart Patch Test', + tiles: [ + { + name: 'Original', + config: { + displayType: 'line', + sourceId, + select: [{ aggFn: 'count' }], + }, + }, + ], + }, + ); + const created = JSON.parse(getFirstText(createResult)); + const tileId = created.tiles[0].id; + + await callTool(ctx.client!, 'clickstack_patch_dashboard', { + dashboardId: created.id, + tileId, + tile: { + name: 'Patched', + config: { + displayType: 'number', + sourceId, + select: [{ aggFn: 'avg', valueExpression: 'Duration' }], + backgroundChart: { type: 'line', color: 'chart-blue' }, + }, + }, + }); + + const getResult = await callTool( + ctx.client!, + 'clickstack_get_dashboard_tile', + { + dashboardId: created.id, + tileId, + }, + ); + + expect(getResult.isError).toBeFalsy(); + const tile = JSON.parse(getFirstText(getResult)); + expect(tile.config.displayType).toBe('number'); + expect(tile.config.backgroundChart).toEqual({ + type: 'line', + color: 'chart-blue', + }); + }); + describe('raw SQL macro warnings', () => { // Patching a tile to a macro-less raw SQL config succeeds (non-blocking) // but surfaces an advisory `warnings` array on the response. diff --git a/packages/api/src/mcp/__tests__/dashboards/saveDashboard.test.ts b/packages/api/src/mcp/__tests__/dashboards/saveDashboard.test.ts index 0b51e7e2cf..8929ef1889 100644 --- a/packages/api/src/mcp/__tests__/dashboards/saveDashboard.test.ts +++ b/packages/api/src/mcp/__tests__/dashboards/saveDashboard.test.ts @@ -833,6 +833,91 @@ describe('MCP Dashboard Tools - clickstack_save_dashboard', () => { expect(saved.tiles[0].config.colorRules).toBeUndefined(); }); + it('should round-trip number-tile backgroundChart through save and get', async () => { + const sourceId = ctx.traceSource._id.toString(); + const numberConfig = { + displayType: 'number', + sourceId, + select: [{ aggFn: 'count' }], + color: 'chart-green', + backgroundChart: { type: 'area', color: 'chart-blue' }, + }; + + const saveResult = await callTool( + ctx.client!, + 'clickstack_save_dashboard', + { + name: 'Number Background Chart', + tiles: [{ name: 'Number', config: numberConfig }], + }, + ); + expect(saveResult.isError).toBeFalsy(); + const saved = JSON.parse(getFirstText(saveResult)); + expect(saved.tiles[0].config).toMatchObject(numberConfig); + + const getResult = await callTool( + ctx.client!, + 'clickstack_get_dashboard', + { + id: saved.id, + }, + ); + expect(getResult.isError).toBeFalsy(); + const fetched = JSON.parse(getFirstText(getResult)); + expect(fetched.tiles[0].config).toMatchObject(numberConfig); + }); + + it('should reject a number-tile backgroundChart type outside line/area', async () => { + const sourceId = ctx.traceSource._id.toString(); + const result = await callTool(ctx.client!, 'clickstack_save_dashboard', { + name: 'Bad BackgroundChart', + tiles: [ + { + name: 'Number', + config: { + displayType: 'number', + sourceId, + select: [{ aggFn: 'count' }], + backgroundChart: { type: 'bar' }, + }, + }, + ], + }); + + expect(result.isError).toBe(true); + }); + + it('should drop backgroundChart on a raw SQL number tile', async () => { + // The raw SQL number tile schema has no backgroundChart (builder-only; + // a raw SQL tile returns a single value with no time dimension to + // bucket), so a sent value is stripped on save. + const connectionId = ctx.connection._id.toString(); + const saveResult = await callTool( + ctx.client!, + 'clickstack_save_dashboard', + { + name: 'SQL Number Background Chart', + tiles: [ + { + name: 'SLO', + config: { + configType: 'sql', + displayType: 'number', + connectionId, + sqlTemplate: 'SELECT 0.99 AS value LIMIT 1', + color: 'chart-success', + backgroundChart: { type: 'line' }, + }, + }, + ], + }, + ); + expect(saveResult.isError).toBeFalsy(); + const saved = JSON.parse(getFirstText(saveResult)); + expect(saved.tiles[0].config.color).toBe('chart-success'); + expect(saved.tiles[0].config.backgroundChart).toBeUndefined(); + }); + it('should preserve display fields on builder tiles through save, get, update, and re-get', async () => { const sourceId = ctx.traceSource._id.toString(); const numberFormat = { diff --git a/packages/api/src/mcp/prompts/dashboards/content.ts b/packages/api/src/mcp/prompts/dashboards/content.ts index 9e72381d9b..1ae7d62323 100644 --- a/packages/api/src/mcp/prompts/dashboards/content.ts +++ b/packages/api/src/mcp/prompts/dashboards/content.ts @@ -1028,6 +1028,23 @@ Colors are palette tokens, not hex. Order rules from least to most severe so the colorRules is for number builder tiles only. Raw SQL number tiles (configType: "sql") support color but not colorRules. +== NUMBER TILE BACKGROUND CHART == + +number tiles can show a faint background trend sparkline behind the value, derived from a time-bucketed version of the same query. Use it for SLO / error-budget tiles where the trend over the window matters as much as the current value. One field on the tile config: + + backgroundChart { type, color? }. type is "line" or "area". color is an optional palette token override; when unset the sparkline inherits the tile color. + +Example: an availability tile that shows the current value over a faint area trend: + config: { + displayType: "number", + sourceId: "...", + select: [{ aggFn: "avg", valueExpression: "Success", numberFormat: { output: "percent" } }], + color: "chart-green", + backgroundChart: { type: "area" } + } + +backgroundChart is for number builder tiles only. Raw SQL number tiles (configType: "sql") return a single value with no time dimension to bucket, so they do not support it. + == asRatio == Set asRatio: true on line / stacked_bar / table tiles with exactly 2 select items diff --git a/packages/api/src/mcp/tools/dashboards/schemas.ts b/packages/api/src/mcp/tools/dashboards/schemas.ts index d90e759af4..54ecd6d867 100644 --- a/packages/api/src/mcp/tools/dashboards/schemas.ts +++ b/packages/api/src/mcp/tools/dashboards/schemas.ts @@ -4,6 +4,7 @@ // readability. Reformatting all separators is out of scope here. import { AggregateFunctionSchema, + BackgroundChartSchema, ChartPaletteTokenSchema, DASHBOARD_CONTAINER_ID_MAX, DASHBOARD_MAX_CONTAINERS, @@ -60,6 +61,15 @@ const rawSqlNumberTileColorDescription = '"number", ignored otherwise. Raw SQL number tiles do not support ' + 'conditional colorRules.'; +const numberTileBackgroundChartDescription = + 'Optional background trend sparkline drawn behind the number, derived ' + + 'from a time-bucketed version of the same query (useful for SLO / ' + + 'error-budget tiles where the trend over the window matters). ' + + '{ type, color? }: type is "line" or "area"; color is an optional ' + + 'palette token override (the sparkline inherits the tile color when ' + + 'unset). Builder number tiles only; raw SQL number tiles have no time ' + + 'dimension to bucket. Example: { type: "area", color: "chart-blue" }.'; + const mcpNumberFormatSchema = z.object({ output: z .enum([ @@ -597,6 +607,9 @@ const mcpNumberTileSchema = mcpTileLayoutSchema.extend({ .max(10) .optional() .describe(numberTileColorRulesDescription), + backgroundChart: BackgroundChartSchema.optional().describe( + numberTileBackgroundChartDescription, + ), }), });