Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcp-number-tile-background-chart.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
85 changes: 85 additions & 0 deletions packages/api/src/mcp/__tests__/dashboards/saveDashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
17 changes: 17 additions & 0 deletions packages/api/src/mcp/prompts/dashboards/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions packages/api/src/mcp/tools/dashboards/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// readability. Reformatting all separators is out of scope here.
import {
AggregateFunctionSchema,
BackgroundChartSchema,
ChartPaletteTokenSchema,
DASHBOARD_CONTAINER_ID_MAX,
DASHBOARD_MAX_CONTAINERS,
Expand Down Expand Up @@ -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" }.';
Comment on lines +68 to +71

@pulpdrew pulpdrew Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something worth eval-ing sometime - does it make a difference for AI performance whether the fields (type and color here are .described at the top-level vs on the individual schema fields?


const mcpNumberFormatSchema = z.object({
output: z
.enum([
Expand Down Expand Up @@ -597,6 +607,9 @@ const mcpNumberTileSchema = mcpTileLayoutSchema.extend({
.max(10)
.optional()
.describe(numberTileColorRulesDescription),
backgroundChart: BackgroundChartSchema.optional().describe(
numberTileBackgroundChartDescription,
),
}),
});

Expand Down
Loading