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
6 changes: 5 additions & 1 deletion mcp/create-chart/src/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ describe("handleRenderChart", () => {
// up and mangle.
expect(ready).not.toMatch(/\\"/);
expect(meta.type).toBe("pie");
expect(meta.schema_version).toBe(1);
expect(meta.artifact_kind).toBe("chart_spec");
expect(typeof meta.chart_id).toBe("string");
expect((meta.chart_id as string).startsWith("pie-")).toBe(true);
expect(typeof meta.bytes).toBe("number");
Expand All @@ -232,6 +234,7 @@ describe("handleRenderChart", () => {
const inner = ready.replace(/^```chart\n/, "").replace(/\n```$/, "");
const spec = JSON.parse(inner);
expect(spec.type).toBe("bar");
expect(spec.schema_version).toBe(1);
expect(spec.data.series[0].values).toEqual([10, 20]);
expect(spec.title).toBe("Demo");
expect(spec).not.toHaveProperty("extra_garbage");
Expand All @@ -247,10 +250,11 @@ describe("handleRenderChart", () => {
const expectedDir = path.resolve(tmp, "chart-render");
expect(existsSync(expectedDir)).toBe(true);
const expectedFile = path.join(expectedDir, `${meta.chart_id as string}.json`);
expect(meta.svg_path).toBe(expectedFile);
expect(meta.svg_path).toBe("");
expect(meta.spec_path).toBe(expectedFile);
expect(existsSync(expectedFile)).toBe(true);
const onDisk = JSON.parse(readFileSync(expectedFile, "utf8"));
expect(onDisk.schema_version).toBe(1);
expect(onDisk.type).toBe("line");
expect(onDisk.data.series[0].points).toEqual([{ x: 1, y: 2 }]);
expect(readdirSync(expectedDir)).toContain(`${meta.chart_id as string}.json`);
Expand Down
8 changes: 6 additions & 2 deletions mcp/create-chart/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import type { RenderChartArgs, RenderChartResult } from "./types.js";

const CHART_SPEC_VERSION = 1;

export const RENDER_CHART_INPUT_SCHEMA = {
type: "object",
required: ["type", "data"],
Expand Down Expand Up @@ -46,7 +48,7 @@ export async function handleRenderChart(rawArgs: unknown): Promise<{
const args = validate(rawArgs);
const id = newChartId(args.type);

const spec = JSON.stringify(args);
const spec = JSON.stringify({ ...args, schema_version: CHART_SPEC_VERSION });
const markdownEmbed = "```chart\n" + spec + "\n```";

let specPath: string | undefined;
Expand All @@ -60,10 +62,12 @@ export async function handleRenderChart(rawArgs: unknown): Promise<{
}

const result: RenderChartResult = {
schema_version: CHART_SPEC_VERSION,
chart_id: id,
type: args.type,
artifact_kind: "chart_spec",
spec_path: specPath ?? "",
svg_path: specPath ?? "",
svg_path: "",
bytes: Buffer.byteLength(spec, "utf8"),
embed_instructions:
"Paste the READY_TO_PASTE block above verbatim into your reply where the chart should appear. Do not modify the JSON, add backslashes, escape non-ASCII characters, convert to ```svg, or inline an <img>.",
Expand Down
7 changes: 7 additions & 0 deletions mcp/create-chart/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface LineSeries {
}

export interface ChartCommonOpts {
schema_version?: 1;
title?: string;
width?: number;
height?: number;
Expand All @@ -35,9 +36,15 @@ export type RenderChartArgs =
| ({ type: "line"; data: { series: LineSeries[] } } & ChartCommonOpts);

export interface RenderChartResult {
schema_version: 1;
chart_id: string;
type: "pie" | "bar" | "line";
artifact_kind: "chart_spec";
spec_path: string;
/**
* Kept for backwards-compatible metadata shape. Empty because render_chart
* persists a JSON chart spec; the portal renders SVG client-side.
*/
svg_path: string;
bytes: number;
embed_instructions: string;
Expand Down
10 changes: 10 additions & 0 deletions portal-web/src/components/chat/ChartRenderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe("tryParseChartSpec", () => {
'{"type":"pie","data":{"slices":[{"label":"a","value":1},{"label":"b","value":2}]}}',
)
expect(spec?.type).toBe("pie")
expect(spec?.schema_version).toBe(1)
expect(spec).toMatchObject({ data: { slices: [{ label: "a", value: 1 }, { label: "b", value: 2 }] } })
})

Expand All @@ -30,6 +31,15 @@ describe("tryParseChartSpec", () => {
expect(tryParseChartSpec('{"type":"pie"}')).toBeNull()
})

it("accepts missing schema_version as v1 but rejects unknown versions", () => {
expect(
tryParseChartSpec('{"schema_version":1,"type":"pie","data":{"slices":[{"label":"a","value":1}]}}')?.schema_version,
).toBe(1)
expect(
tryParseChartSpec('{"schema_version":2,"type":"pie","data":{"slices":[{"label":"a","value":1}]}}'),
).toBeNull()
})

// The chart spec round-trips through the LLM as text; the model sometimes
// double-escapes non-ASCII, leaving literal \uXXXX sequences after JSON.parse.
it("decodes stray \\uXXXX escapes in title and labels", () => {
Expand Down
Loading
Loading