@blueprint-chart/mcp 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +27 -14
  2. package/dist/cli.js +15 -2
  3. package/dist/dsl/chartTypes.d.ts +16 -0
  4. package/dist/dsl/chartTypes.js +37 -0
  5. package/dist/dsl/chartTypes.test.d.ts +1 -0
  6. package/dist/dsl/chartTypes.test.js +32 -0
  7. package/dist/dsl/dataKey.d.ts +25 -0
  8. package/dist/dsl/dataKey.js +42 -0
  9. package/dist/dsl/dataKey.test.d.ts +1 -0
  10. package/dist/dsl/dataKey.test.js +35 -0
  11. package/dist/dsl/suggest.d.ts +1 -0
  12. package/dist/dsl/suggest.js +47 -0
  13. package/dist/dsl/suggest.test.d.ts +1 -0
  14. package/dist/dsl/suggest.test.js +20 -0
  15. package/dist/dsl/universalProperties.d.ts +30 -0
  16. package/dist/dsl/universalProperties.js +52 -0
  17. package/dist/dsl/universalProperties.test.d.ts +1 -0
  18. package/dist/dsl/universalProperties.test.js +26 -0
  19. package/dist/dsl/validate.d.ts +10 -0
  20. package/dist/dsl/validate.js +68 -0
  21. package/dist/dsl/validate.test.d.ts +1 -0
  22. package/dist/dsl/validate.test.js +73 -0
  23. package/dist/errors.d.ts +20 -1
  24. package/dist/errors.js +1 -0
  25. package/dist/errors.test.js +21 -0
  26. package/dist/lib/zodToJsonSchema.d.ts +10 -5
  27. package/dist/lib/zodToJsonSchema.js +14 -6
  28. package/dist/links/buildUrls.d.ts +14 -0
  29. package/dist/links/buildUrls.js +20 -0
  30. package/dist/links/buildUrls.test.d.ts +1 -0
  31. package/dist/links/buildUrls.test.js +28 -0
  32. package/dist/links/editorConfig.d.ts +4 -0
  33. package/dist/links/editorConfig.js +15 -0
  34. package/dist/links/editorConfig.test.d.ts +1 -0
  35. package/dist/links/editorConfig.test.js +28 -0
  36. package/dist/links/encode.d.ts +11 -0
  37. package/dist/links/encode.js +19 -0
  38. package/dist/links/encode.test.d.ts +1 -0
  39. package/dist/links/encode.test.js +37 -0
  40. package/dist/prompts/authorChart.js +23 -18
  41. package/dist/prompts/authorChart.test.js +6 -0
  42. package/dist/render/diagnose.d.ts +19 -0
  43. package/dist/render/diagnose.js +100 -0
  44. package/dist/render/diagnose.test.d.ts +1 -0
  45. package/dist/render/diagnose.test.js +53 -0
  46. package/dist/render/frame.d.ts +10 -0
  47. package/dist/render/frame.js +10 -0
  48. package/dist/render/frame.test.d.ts +1 -0
  49. package/dist/render/frame.test.js +12 -0
  50. package/dist/render/jsdomEnv.d.ts +2 -1
  51. package/dist/render/jsdomEnv.js +14 -1
  52. package/dist/render/jsdomEnv.test.js +36 -2
  53. package/dist/render/renderSceneState.d.ts +5 -1
  54. package/dist/render/renderSceneState.js +4 -3
  55. package/dist/render/renderSceneState.test.js +13 -7
  56. package/dist/render/validatePipeline.d.ts +23 -0
  57. package/dist/render/validatePipeline.js +41 -0
  58. package/dist/render/validatePipeline.test.d.ts +1 -0
  59. package/dist/render/validatePipeline.test.js +34 -0
  60. package/dist/resources/docsReader.d.ts +4 -1
  61. package/dist/resources/docsReader.js +23 -6
  62. package/dist/resources/docsReader.test.js +27 -2
  63. package/dist/resources/index.d.ts +1 -1
  64. package/dist/resources/samples.d.ts +1 -2
  65. package/dist/server.d.ts +9 -0
  66. package/dist/server.js +63 -5
  67. package/dist/server.test.js +101 -4
  68. package/dist/tools/describeChartType.d.ts +33 -0
  69. package/dist/tools/describeChartType.js +119 -0
  70. package/dist/tools/describeChartType.test.d.ts +1 -0
  71. package/dist/tools/describeChartType.test.js +58 -0
  72. package/dist/tools/exportChart.d.ts +17 -0
  73. package/dist/tools/exportChart.js +31 -0
  74. package/dist/tools/exportChart.test.d.ts +1 -0
  75. package/dist/tools/exportChart.test.js +43 -0
  76. package/dist/tools/getExample.d.ts +20 -0
  77. package/dist/tools/getExample.js +55 -0
  78. package/dist/tools/getExample.test.d.ts +1 -0
  79. package/dist/tools/getExample.test.js +40 -0
  80. package/dist/tools/getGrammar.d.ts +17 -0
  81. package/dist/tools/getGrammar.js +38 -0
  82. package/dist/tools/getGrammar.test.d.ts +1 -0
  83. package/dist/tools/getGrammar.test.js +24 -0
  84. package/dist/tools/inspect.d.ts +8 -1
  85. package/dist/tools/inspect.js +31 -7
  86. package/dist/tools/inspect.test.js +35 -13
  87. package/dist/tools/listChartTypes.d.ts +14 -0
  88. package/dist/tools/listChartTypes.js +42 -0
  89. package/dist/tools/listChartTypes.test.d.ts +1 -0
  90. package/dist/tools/listChartTypes.test.js +42 -0
  91. package/dist/tools/render.d.ts +14 -12
  92. package/dist/tools/render.js +96 -28
  93. package/dist/tools/render.test.js +137 -1
  94. package/dist/tools/validate.d.ts +11 -3
  95. package/dist/tools/validate.js +9 -1
  96. package/dist/tools/validate.test.js +17 -12
  97. package/dist/transports/http.d.ts +4 -2
  98. package/dist/transports/http.js +232 -23
  99. package/dist/transports/http.test.js +158 -22
  100. package/package.json +4 -2
  101. package/public/apple-touch-icon.png +0 -0
  102. package/public/favicon.png +0 -0
  103. package/public/favicon.svg +9 -0
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parse, samples } from '@blueprint-chart/lib';
3
+ import { validateAst } from './validate';
4
+ function ast(src) {
5
+ return parse(src);
6
+ }
7
+ describe('validateAst', () => {
8
+ it('returns empty issues for a known chart type with data', () => {
9
+ const a = ast('chart bar-vertical { data { "E" = 1 } }');
10
+ expect(validateAst(a)).toEqual([]);
11
+ });
12
+ it('reports E_UNKNOWN_CHART_TYPE with suggestion', () => {
13
+ const a = ast('chart bar { data { "E" = 1 } }');
14
+ const issues = validateAst(a);
15
+ expect(issues.length).toBeGreaterThanOrEqual(1);
16
+ const chartIssue = issues.find(i => i.code === 'E_UNKNOWN_CHART_TYPE');
17
+ expect(chartIssue).toBeDefined();
18
+ expect(chartIssue.path).toBe('chart');
19
+ expect(chartIssue.suggestion).toMatch(/^bar-/);
20
+ expect(chartIssue.context?.got).toBe('bar');
21
+ expect(Array.isArray(chartIssue.context?.known)).toBe(true);
22
+ });
23
+ it('reports E_EMPTY_DATA when data block is missing', () => {
24
+ const a = ast('chart bar-vertical { title = "x" }');
25
+ const issues = validateAst(a);
26
+ expect(issues.some(i => i.code === 'E_EMPTY_DATA')).toBe(true);
27
+ });
28
+ it('reports E_EMPTY_DATA when data block has zero entries', () => {
29
+ const a = ast('chart bar-vertical { data {} }');
30
+ const issues = validateAst(a);
31
+ expect(issues.some(i => i.code === 'E_EMPTY_DATA')).toBe(true);
32
+ });
33
+ it('does not double-report when chart type is unknown but data is fine', () => {
34
+ const a = ast('chart bar { data { "E" = 1 } }');
35
+ const issues = validateAst(a);
36
+ expect(issues.find(i => i.code === 'E_EMPTY_DATA')).toBeUndefined();
37
+ });
38
+ });
39
+ describe('validateAst — properties and data keys', () => {
40
+ it('reports E_UNKNOWN_PROPERTY for an unknown chart-level key', () => {
41
+ const a = ast('chart bar-vertical { totallyMadeUp = 1 data { "E" = 1 } }');
42
+ const issues = validateAst(a);
43
+ const unk = issues.find(i => i.code === 'E_UNKNOWN_PROPERTY');
44
+ expect(unk).toBeDefined();
45
+ expect(unk.context?.got).toBe('totallyMadeUp');
46
+ });
47
+ it('does not report a known per-chart-type property as unknown', () => {
48
+ const a = ast('chart bar-vertical { barGap = 0.2 data { "E" = 1 } }');
49
+ expect(validateAst(a).filter(i => i.code === 'E_UNKNOWN_PROPERTY')).toEqual([]);
50
+ });
51
+ it('does not report a known universal property as unknown', () => {
52
+ const a = ast('chart bar-vertical { title = "x" data { "E" = 1 } }');
53
+ expect(validateAst(a).filter(i => i.code === 'E_UNKNOWN_PROPERTY')).toEqual([]);
54
+ });
55
+ it('suggests a near-miss universal property', () => {
56
+ const a = ast('chart bar-vertical { titl = "x" data { "E" = 1 } }');
57
+ const issues = validateAst(a);
58
+ const unk = issues.find(i => i.code === 'E_UNKNOWN_PROPERTY');
59
+ expect(unk?.suggestion).toBe('title');
60
+ });
61
+ it('reports E_UNKNOWN_DATA_KEY for unquoted-identifier data keys when chart expects labels', () => {
62
+ const a = ast('chart bar-vertical { data { unquotedKey = 1 } }');
63
+ const issues = validateAst(a);
64
+ expect(issues.some(i => i.code === 'E_UNKNOWN_DATA_KEY')).toBe(true);
65
+ });
66
+ it('roundtrips every shipped sample with no errors', () => {
67
+ for (const sample of samples) {
68
+ const a = ast(sample.dsl);
69
+ const issues = validateAst(a);
70
+ expect(issues, `sample ${sample.id}: ${JSON.stringify(issues)}`).toEqual([]);
71
+ }
72
+ });
73
+ });
package/dist/errors.d.ts CHANGED
@@ -3,15 +3,32 @@ export declare const ErrorCode: {
3
3
  readonly E_PARSE: "E_PARSE";
4
4
  readonly E_SEMANTIC: "E_SEMANTIC";
5
5
  readonly E_RENDER: "E_RENDER";
6
+ readonly E_CONFIG: "E_CONFIG";
6
7
  readonly E_INTERNAL: "E_INTERNAL";
7
8
  };
8
9
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
9
10
  export interface ToolErrorEntry {
11
+ /**
12
+ * Item-level error code in `E_XXX` shape.
13
+ *
14
+ * Intentionally typed as `string` rather than the `ErrorCode` enum.
15
+ * Item-level codes are driven by the context that produced the error:
16
+ * - validation errors use `ValidationCode` values (e.g. `'E_UNKNOWN_PROPERTY'`)
17
+ * - render pre-flight uses `RenderDiagnosticCode` values (e.g. `'E_NO_DATA'`)
18
+ * - individual tools may define their own codes (e.g. `'E_UNKNOWN_SAMPLE'`)
19
+ *
20
+ * These codes differ from the top-level `ToolResult.code` field, which always
21
+ * uses the `ErrorCode` enum and categorises the failure class (parse, input,
22
+ * render, etc.). Item-level codes do not need to be enumerated here.
23
+ */
24
+ code?: string;
10
25
  path?: string;
11
26
  line?: number;
12
27
  column?: number;
13
28
  message: string;
14
29
  snippet?: string;
30
+ context?: Record<string, unknown>;
31
+ suggestion?: string;
15
32
  }
16
33
  export type ToolResult<T> = {
17
34
  ok: true;
@@ -22,7 +39,9 @@ export type ToolResult<T> = {
22
39
  errors: ToolErrorEntry[];
23
40
  };
24
41
  export declare function toolOk<T>(data: T): ToolResult<T>;
25
- export declare function toolError<T = never>(code: ErrorCode, errors: ToolErrorEntry[]): ToolResult<T>;
42
+ export declare function toolError<T = never>(code: ErrorCode, errors: ToolErrorEntry[]): Extract<ToolResult<T>, {
43
+ ok: false;
44
+ }>;
26
45
  export declare function isToolError<T>(r: ToolResult<T>): r is Extract<ToolResult<T>, {
27
46
  ok: false;
28
47
  }>;
package/dist/errors.js CHANGED
@@ -3,6 +3,7 @@ export const ErrorCode = {
3
3
  E_PARSE: 'E_PARSE',
4
4
  E_SEMANTIC: 'E_SEMANTIC',
5
5
  E_RENDER: 'E_RENDER',
6
+ E_CONFIG: 'E_CONFIG',
6
7
  E_INTERNAL: 'E_INTERNAL',
7
8
  };
8
9
  export function toolOk(data) {
@@ -20,4 +20,25 @@ describe('errors', () => {
20
20
  const r = toolError(ErrorCode.E_INTERNAL, [{ message: 'x' }]);
21
21
  expect(isToolError(r)).toBe(true);
22
22
  });
23
+ it('includes E_CONFIG for missing server configuration', () => {
24
+ expect(ErrorCode.E_CONFIG).toBe('E_CONFIG');
25
+ });
26
+ });
27
+ describe('ToolErrorEntry structured fields', () => {
28
+ it('accepts code, context, and suggestion on a tool-error entry', () => {
29
+ const entry = {
30
+ code: 'E_UNKNOWN_CHART_TYPE',
31
+ path: 'chart',
32
+ message: 'Unknown chart type "bar"',
33
+ context: { got: 'bar', knownTypes: ['bar-vertical'] },
34
+ suggestion: 'bar-vertical',
35
+ };
36
+ const r = toolError(ErrorCode.E_SEMANTIC, [entry]);
37
+ expect(r.ok).toBe(false);
38
+ if (!r.ok) {
39
+ expect(r.errors[0].code).toBe('E_UNKNOWN_CHART_TYPE');
40
+ expect(r.errors[0].context).toEqual({ got: 'bar', knownTypes: ['bar-vertical'] });
41
+ expect(r.errors[0].suggestion).toBe('bar-vertical');
42
+ }
43
+ });
23
44
  });
@@ -1,8 +1,13 @@
1
1
  import type { ZodTypeAny } from 'zod';
2
2
  /**
3
- * Permissive stub: returns a generic object schema that allows any properties.
4
- * MCP tool handlers do the precise runtime validation via Zod. If MCP clients
5
- * begin requiring precise JSON Schema for `listTools`, replace this body with
6
- * the `zod-to-json-schema` package.
3
+ * Convert a Zod schema to a JSON Schema object suitable for MCP `tools/list`.
4
+ *
5
+ * MCP clients (notably claude.ai web) introspect this schema to know what
6
+ * parameters a tool accepts. A permissive stub left tools effectively
7
+ * un-invokable because the param shapes weren't discoverable.
8
+ *
9
+ * `zod-to-json-schema` produces a `$schema`-prefixed draft-07 document; we
10
+ * unwrap that to just the inline schema body so it slots cleanly into the
11
+ * MCP tool descriptor.
7
12
  */
8
- export declare function zodToJsonSchema(_schema: ZodTypeAny): Record<string, unknown>;
13
+ export declare function zodToJsonSchema(schema: ZodTypeAny): Record<string, unknown>;
@@ -1,9 +1,17 @@
1
+ import { zodToJsonSchema as toJsonSchema } from 'zod-to-json-schema';
1
2
  /**
2
- * Permissive stub: returns a generic object schema that allows any properties.
3
- * MCP tool handlers do the precise runtime validation via Zod. If MCP clients
4
- * begin requiring precise JSON Schema for `listTools`, replace this body with
5
- * the `zod-to-json-schema` package.
3
+ * Convert a Zod schema to a JSON Schema object suitable for MCP `tools/list`.
4
+ *
5
+ * MCP clients (notably claude.ai web) introspect this schema to know what
6
+ * parameters a tool accepts. A permissive stub left tools effectively
7
+ * un-invokable because the param shapes weren't discoverable.
8
+ *
9
+ * `zod-to-json-schema` produces a `$schema`-prefixed draft-07 document; we
10
+ * unwrap that to just the inline schema body so it slots cleanly into the
11
+ * MCP tool descriptor.
6
12
  */
7
- export function zodToJsonSchema(_schema) {
8
- return { type: 'object', additionalProperties: true };
13
+ export function zodToJsonSchema(schema) {
14
+ const full = toJsonSchema(schema, { target: 'jsonSchema7', $refStrategy: 'none' });
15
+ const { $schema: _$schema, ...rest } = full;
16
+ return rest;
9
17
  }
@@ -0,0 +1,14 @@
1
+ export type DocUrlGroup = 'handbook' | 'guide' | 'charts' | 'reference/dsl' | 'reference/api';
2
+ /**
3
+ * Editable "open & copy" deep-link: hydrates a fresh editor session.
4
+ * The `/copy` route reads `bpc64` from the query string and decodes it as
5
+ * URL-SAFE base64 (`decodeUrlSafeBase64`). `encodeURIComponent` mirrors the
6
+ * editor/docs code; it is a no-op on url-safe base64 (alphabet is `[A-Za-z0-9-_]`)
7
+ * but kept for fidelity. NOTE: distinct from `/render?bpc64=` below, which
8
+ * decodes STANDARD base64 via `atob` despite the identical param name.
9
+ */
10
+ export declare function buildCopyUrl(source: string, editorBase: string): string;
11
+ /** Read-only render deep-link, suitable as an iframe `src` (standard base64). */
12
+ export declare function buildEmbedUrl(source: string, editorBase: string): string;
13
+ /** Public docs page URL for a docs group + slug. */
14
+ export declare function buildDocUrl(group: DocUrlGroup, slug: string, docsBase: string): string;
@@ -0,0 +1,20 @@
1
+ import { toUrlSafeB64, toStandardB64 } from './encode';
2
+ /**
3
+ * Editable "open & copy" deep-link: hydrates a fresh editor session.
4
+ * The `/copy` route reads `bpc64` from the query string and decodes it as
5
+ * URL-SAFE base64 (`decodeUrlSafeBase64`). `encodeURIComponent` mirrors the
6
+ * editor/docs code; it is a no-op on url-safe base64 (alphabet is `[A-Za-z0-9-_]`)
7
+ * but kept for fidelity. NOTE: distinct from `/render?bpc64=` below, which
8
+ * decodes STANDARD base64 via `atob` despite the identical param name.
9
+ */
10
+ export function buildCopyUrl(source, editorBase) {
11
+ return `${editorBase}/#/copy?bpc64=${encodeURIComponent(toUrlSafeB64(source))}`;
12
+ }
13
+ /** Read-only render deep-link, suitable as an iframe `src` (standard base64). */
14
+ export function buildEmbedUrl(source, editorBase) {
15
+ return `${editorBase}/#/render?bpc64=${encodeURIComponent(toStandardB64(source))}`;
16
+ }
17
+ /** Public docs page URL for a docs group + slug. */
18
+ export function buildDocUrl(group, slug, docsBase) {
19
+ return `${docsBase}/${group}/${slug}`;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildCopyUrl, buildEmbedUrl, buildDocUrl } from './buildUrls';
3
+ const EDITOR = 'https://blueprintchart.com';
4
+ const DOCS = 'https://docs.blueprintchart.com';
5
+ const SRC = 'chart bar-vertical {\n}\n';
6
+ describe('buildCopyUrl', () => {
7
+ it('builds a /copy?bpc64= deep-link with url-safe base64', () => {
8
+ expect(buildCopyUrl(SRC, EDITOR)).toBe('https://blueprintchart.com/#/copy?bpc64=Y2hhcnQgYmFyLXZlcnRpY2FsIHsKfQo');
9
+ });
10
+ it('produces a url-safe payload (no +, /, or = to encode)', () => {
11
+ const url = buildCopyUrl(SRC, EDITOR);
12
+ const payload = url.split('bpc64=')[1];
13
+ expect(payload).not.toMatch(/[+/=]|%/);
14
+ });
15
+ });
16
+ describe('buildEmbedUrl', () => {
17
+ it('builds a hash /render link with URI-encoded standard base64', () => {
18
+ expect(buildEmbedUrl(SRC, EDITOR)).toBe('https://blueprintchart.com/#/render?bpc64=Y2hhcnQgYmFyLXZlcnRpY2FsIHsKfQo%3D');
19
+ });
20
+ });
21
+ describe('buildDocUrl', () => {
22
+ it('maps the charts group to the /charts docs path', () => {
23
+ expect(buildDocUrl('charts', 'bar-vertical', DOCS)).toBe('https://docs.blueprintchart.com/charts/bar-vertical');
24
+ });
25
+ it('maps reference/dsl to a nested path', () => {
26
+ expect(buildDocUrl('reference/dsl', 'properties', DOCS)).toBe('https://docs.blueprintchart.com/reference/dsl/properties');
27
+ });
28
+ });
@@ -0,0 +1,4 @@
1
+ /** Editor app base URL (e.g. https://blueprintchart.com), or undefined when unset. */
2
+ export declare function getEditorBaseUrl(): string | undefined;
3
+ /** Docs site base URL (e.g. https://docs.blueprintchart.com), or undefined when unset. */
4
+ export declare function getDocsBaseUrl(): string | undefined;
@@ -0,0 +1,15 @@
1
+ function normalize(raw) {
2
+ const trimmed = raw?.trim();
3
+ if (!trimmed) {
4
+ return undefined;
5
+ }
6
+ return trimmed.replace(/\/+$/, '');
7
+ }
8
+ /** Editor app base URL (e.g. https://blueprintchart.com), or undefined when unset. */
9
+ export function getEditorBaseUrl() {
10
+ return normalize(process.env.BLUEPRINT_CHART_EDITOR_URL);
11
+ }
12
+ /** Docs site base URL (e.g. https://docs.blueprintchart.com), or undefined when unset. */
13
+ export function getDocsBaseUrl() {
14
+ return normalize(process.env.BLUEPRINT_CHART_DOCS_URL);
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { getEditorBaseUrl, getDocsBaseUrl } from './editorConfig';
3
+ afterEach(() => {
4
+ delete process.env.BLUEPRINT_CHART_EDITOR_URL;
5
+ delete process.env.BLUEPRINT_CHART_DOCS_URL;
6
+ });
7
+ describe('getEditorBaseUrl', () => {
8
+ it('returns undefined when unset', () => {
9
+ expect(getEditorBaseUrl()).toBeUndefined();
10
+ });
11
+ it('returns undefined when blank/whitespace', () => {
12
+ process.env.BLUEPRINT_CHART_EDITOR_URL = ' ';
13
+ expect(getEditorBaseUrl()).toBeUndefined();
14
+ });
15
+ it('strips trailing slashes and trims', () => {
16
+ process.env.BLUEPRINT_CHART_EDITOR_URL = ' https://blueprintchart.com// ';
17
+ expect(getEditorBaseUrl()).toBe('https://blueprintchart.com');
18
+ });
19
+ });
20
+ describe('getDocsBaseUrl', () => {
21
+ it('returns undefined when unset', () => {
22
+ expect(getDocsBaseUrl()).toBeUndefined();
23
+ });
24
+ it('normalizes a set value', () => {
25
+ process.env.BLUEPRINT_CHART_DOCS_URL = 'https://docs.blueprintchart.com/';
26
+ expect(getDocsBaseUrl()).toBe('https://docs.blueprintchart.com');
27
+ });
28
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * RFC 4648 §5 url-safe base64 with padding stripped — the encoding the editor's
3
+ * `/#/copy?bpc64=` route decodes (via `decodeUrlSafeBase64`). Mirrors
4
+ * `toUrlSafeB64` in the editor/docs.
5
+ */
6
+ export declare function toUrlSafeB64(input: string): string;
7
+ /**
8
+ * Standard (padded) base64 — the form the editor's `/#/render?bpc64=` route
9
+ * decodes via `atob`. Callers must `encodeURIComponent` it for the query string.
10
+ */
11
+ export declare function toStandardB64(input: string): string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * RFC 4648 §5 url-safe base64 with padding stripped — the encoding the editor's
3
+ * `/#/copy?bpc64=` route decodes (via `decodeUrlSafeBase64`). Mirrors
4
+ * `toUrlSafeB64` in the editor/docs.
5
+ */
6
+ export function toUrlSafeB64(input) {
7
+ return Buffer.from(input, 'utf-8')
8
+ .toString('base64')
9
+ .replace(/\+/g, '-')
10
+ .replace(/\//g, '_')
11
+ .replace(/=+$/, '');
12
+ }
13
+ /**
14
+ * Standard (padded) base64 — the form the editor's `/#/render?bpc64=` route
15
+ * decodes via `atob`. Callers must `encodeURIComponent` it for the query string.
16
+ */
17
+ export function toStandardB64(input) {
18
+ return Buffer.from(input, 'utf-8').toString('base64');
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { samples } from '@blueprint-chart/lib';
3
+ import { toUrlSafeB64, toStandardB64 } from './encode';
4
+ /** Decode a url-safe, unpadded base64 string back to UTF-8 (mirrors the editor's /copy decoder). */
5
+ function decodeUrlSafe(raw) {
6
+ const padded = raw.replace(/-/g, '+').replace(/_/g, '/');
7
+ const pad = padded.length % 4 === 0 ? '' : '='.repeat(4 - (padded.length % 4));
8
+ return Buffer.from(padded + pad, 'base64').toString('utf8');
9
+ }
10
+ describe('toUrlSafeB64', () => {
11
+ it('encodes ASCII with no padding and a url-safe alphabet', () => {
12
+ const out = toUrlSafeB64('chart bar-vertical {\n}\n');
13
+ expect(out).toBe('Y2hhcnQgYmFyLXZlcnRpY2FsIHsKfQo');
14
+ expect(out).not.toContain('=');
15
+ expect(out).not.toMatch(/[+/]/);
16
+ });
17
+ it('round-trips non-ASCII (en dash, euro) through UTF-8', () => {
18
+ const src = 'Price – €5';
19
+ expect(toUrlSafeB64(src)).toBe('UHJpY2Ug4oCTIOKCrDU');
20
+ expect(decodeUrlSafe(toUrlSafeB64(src))).toBe(src);
21
+ });
22
+ });
23
+ describe('toStandardB64', () => {
24
+ it('encodes with standard alphabet and padding (atob-compatible)', () => {
25
+ expect(toStandardB64('chart bar-vertical {\n}\n')).toBe('Y2hhcnQgYmFyLXZlcnRpY2FsIHsKfQo=');
26
+ });
27
+ });
28
+ describe('letter-frequency golden link', () => {
29
+ // The url-safe base64 of the letter-frequency sample, as hardcoded in the
30
+ // editor's LandingMcp.vue (`/#/copy?bpc64=<this>`). MCP output must match exactly.
31
+ const LANDING_B64 = 'Y2hhcnQgYmFyLXZlcnRpY2FsIHsKICB0aXRsZSA9ICJFIGlzIHRoZSBtb3N0IGZyZXF1ZW50IGxldHRlciBpbiBFbmdsaXNoIgogIGRlc2NyaXB0aW9uID0gIkhvdyBvZnRlbiBlYWNoIGxldHRlciBhcHBlYXJzIGluIHR5cGljYWwgRW5nbGlzaCB0ZXh0IgogIGJ5bGluZSA9ICJQaWVycmUgUm9tZXJhIgogIHNvdXJjZSA9ICJMZXdhbmQsIENyeXB0b2xvZ2ljYWwgTWF0aGVtYXRpY3MiCiAgc291cmNlVXJsID0gImh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0xldHRlcl9mcmVxdWVuY3kiCiAgbm90ZSA9ICJCYXNlZCBvbiBhbmFseXNpcyBvZiA0MCwwMDAgd29yZHMgZnJvbSBFbmdsaXNoIHByb3NlIgogIGNvbG9yUGFsZXR0ZSA9ICJMb25kb24iCiAgc29ydCA9IGRlc2NlbmRpbmcKICB2YWx1ZUxhYmVscyA9IHRydWUKICB2ZXJ0aWNhbExhYmVsUG9zaXRpb24gPSBvZmYKICB2ZXJ0aWNhbEdyaWRTdHlsZSA9IG5vbmUKCiAgaGlnaGxpZ2h0ICJFIgoKICBkYXRhIHsKICAgICJFIiA9IDEyLjcwCiAgICAiVCIgPSA5LjA2CiAgICAiQSIgPSA4LjE3CiAgICAiTyIgPSA3LjUxCiAgICAiSSIgPSA2Ljk3CiAgICAiTiIgPSA2Ljc1CiAgICAiUyIgPSA2LjMzCiAgICAiSCIgPSA2LjA5CiAgICAiUiIgPSA1Ljk5CiAgICAiRCIgPSA0LjI1CiAgfQp9Cg';
32
+ it('matches the editor landing-page copy link byte-for-byte', () => {
33
+ const sample = samples.find(s => s.id === 'letter-frequency');
34
+ expect(sample).toBeDefined();
35
+ expect(toUrlSafeB64(sample.dsl)).toBe(LANDING_B64);
36
+ });
37
+ });
@@ -1,30 +1,35 @@
1
1
  const BODY = `You are authoring a Blueprint Chart (\`.bpc\`) file for a user.
2
2
 
3
3
  Workflow:
4
- 1. Read \`bpc://grammar\` for the DSL syntax.
5
- 2. Read \`bpc://handbook/choosing\` and \`bpc://handbook/design-principles\` for dataviz judgment.
6
- 3. If unsure about the chart type, call \`recommend_chart_type\` with the user's column types and row count.
7
- 4. Read \`bpc://chart-types/<type>\` for the specific chart you'll use.
8
- 5. Look at \`bpc://samples/<id>\` for a working example in the same family.
9
- 6. Write the \`.bpc\` source.
10
- 7. Call \`validate_dsl\` on your draft. If errors, read the line/column from the response and fix them.
11
- 8. Call \`inspect_dsl\` to sanity-check the parsed structure.
12
- 9. Call \`render\` with format="png" to get a visual. If the chart looks wrong, iterate on the \`.bpc\` and re-render.
13
- 10. Return the final \`.bpc\` source AND the rendered chart to the user.
4
+ 1. Call \`list_chart_types\` to see what's renderable. (Or read \`bpc://handbook/choosing\` if your client supports resources.)
5
+ 2. If unsure which type to use, call \`recommend_chart_type({ columnTypes, rowCount, goal? })\`.
6
+ 3. Call \`describe_chart_type({ name: "<type>" })\` for properties, when-to-use, and a data-shape example.
7
+ 4. Call \`get_example({ chartType: "<type>" })\` (or \`{ name: "<sample-id>" }\`) to copy a canonical .bpc as a starting point.
8
+ 5. Write the \`.bpc\` source.
9
+ 6. Call \`validate_dsl\` — read \`errors[]\`: each entry has \`code\`, \`message\`, \`suggestion\`. Fix and retry.
10
+ 7. Call \`inspect_dsl\` to sanity-check structure: \`data.rowCount\` confirms rows parsed, \`hasHighlights\`/\`hasColorizes\` confirm overrides.
11
+ 8. Call \`render({ source, format: "png" })\` for a visual. If \`errors[]\` is non-empty, each entry has \`code\` and a usable \`suggestion\`.
12
+ 9. Return the final \`.bpc\` and rendered chart to the user.
13
+ 10. If the user wants to share or embed the chart, call \`export_chart({ source })\` and give them \`copyUrl\` (editable — anyone can open and copy it) or \`embedUrl\` (a read-only render URL for an iframe). Requires the server to have BLUEPRINT_CHART_EDITOR_URL configured; otherwise it returns \`E_CONFIG\`.
14
14
 
15
- Resources you can read:
16
- - \`bpc://grammar\` — DSL syntax reference (aggregate)
15
+ Resources you can read (if your client supports MCP resources):
16
+ - \`bpc://grammar\` — DSL syntax reference (use \`get_grammar\` as a tool equivalent)
17
17
  - \`bpc://handbook/{slug}\` — dataviz pedagogy (choosing, design-principles, color, typography, annotations, accessibility, ...)
18
18
  - \`bpc://guide/{slug}\` — Blueprint Chart guides (scenes, palettes, data-transforms, ...)
19
- - \`bpc://chart-types/{slug}\` — per-chart-type docs
20
- - \`bpc://samples/{id}\` — canonical \`.bpc\` examples
19
+ - \`bpc://chart-types/{slug}\` — per-chart-type docs (use \`describe_chart_type\` as a tool equivalent)
20
+ - \`bpc://samples/{id}\` — canonical \`.bpc\` examples (use \`get_example\` as a tool equivalent)
21
21
  - \`bpc://reference/dsl/{slug}\`, \`bpc://reference/api/{slug}\` — full reference
22
22
 
23
23
  Tools:
24
- - \`validate_dsl({source})\` — parse, return errors with line/column
25
- - \`inspect_dsl({source})\` — parsed summary (chart type, scenes, series, annotations)
26
- - \`recommend_chart_type({columnTypes, rowCount, goal?})\` — ranked suggestions
27
- - \`render({source, format, scene?, width?, height?})\` — SVG (default) or PNG
24
+ - \`validate_dsl({source})\` — parse; returns \`{ valid, errors[], warnings[] }\` — each error has \`code\`, \`message\`, \`suggestion\`
25
+ - \`inspect_dsl({source})\` — parsed summary: \`chartType\`, \`scenes\`, \`seriesCount\`, \`rowCount\`, \`hasHighlights\`, \`hasColorizes\`, etc.
26
+ - \`recommend_chart_type({columnTypes, rowCount, goal?})\` — ranked chart-type suggestions
27
+ - \`render({source, format, scene?, width?, height?})\` — SVG (default) or PNG; \`errors[]\` on failure (each has \`code\` + \`suggestion\`)
28
+ - \`list_chart_types()\` — list all renderable chart types (tool equivalent of \`bpc://handbook/choosing\`)
29
+ - \`describe_chart_type({name})\` — properties, when-to-use, data-shape for one chart type (tool equivalent of \`bpc://chart-types/{slug}\`)
30
+ - \`get_example({chartType?, name?})\` — fetch a canonical \`.bpc\` sample (tool equivalent of \`bpc://samples/{id}\`)
31
+ - \`get_grammar()\` — full DSL syntax reference (tool equivalent of \`bpc://grammar\`)
32
+ - \`export_chart({source})\` — validate then return \`{ copyUrl, embedUrl, frame }\` shareable editor URLs (returns \`E_CONFIG\` if the server has no editor URL configured)
28
33
 
29
34
  Be patient with errors — the feedback loop is the point.`;
30
35
  export function authorChartPrompt() {
@@ -10,4 +10,10 @@ describe('authorChartPrompt', () => {
10
10
  expect(first.content.text).toMatch(/bpc:\/\/grammar/);
11
11
  expect(first.content.text).toMatch(/validate_dsl/);
12
12
  });
13
+ it('mentions export_chart and the export step', () => {
14
+ const prompt = authorChartPrompt();
15
+ const text = prompt.messages[0].content.text;
16
+ expect(text).toContain('export_chart');
17
+ expect(text).toMatch(/copyUrl/);
18
+ });
13
19
  });
@@ -0,0 +1,19 @@
1
+ export type RenderDiagnosticCode = 'E_PARSE' | 'E_UNKNOWN_CHART_TYPE' | 'E_NO_DATA' | 'E_NO_RESOLVED_SERIES' | 'E_UNKNOWN_SCENE_INDEX' | 'E_UNRESOLVED_COLORIZE' | 'E_UNRESOLVED_HIGHLIGHT';
2
+ export interface RenderDiagnostic {
3
+ code: RenderDiagnosticCode;
4
+ path: string;
5
+ message: string;
6
+ context?: Record<string, unknown>;
7
+ suggestion?: string;
8
+ }
9
+ export interface DiagnoseOptions {
10
+ sceneIndex?: number;
11
+ }
12
+ type DiagnoseResult = {
13
+ ok: true;
14
+ } | {
15
+ ok: false;
16
+ diagnostics: RenderDiagnostic[];
17
+ };
18
+ export declare function diagnoseRender(source: string, opts?: DiagnoseOptions): DiagnoseResult;
19
+ export {};
@@ -0,0 +1,100 @@
1
+ import { parse, astToDefinition, resolveScene, getChart } from '@blueprint-chart/lib';
2
+ import { canonicalChartType, listCanonicalChartTypes } from '../dsl/chartTypes';
3
+ import { nearestSuggestion } from '../dsl/suggest';
4
+ function colorizeTargets(ast) {
5
+ return (ast.colorizes ?? []).map(c => c.target);
6
+ }
7
+ function highlightTargets(ast) {
8
+ return (ast.highlights ?? []).map(h => h.target);
9
+ }
10
+ export function diagnoseRender(source, opts = {}) {
11
+ let ast;
12
+ try {
13
+ ast = parse(source);
14
+ }
15
+ catch (err) {
16
+ return {
17
+ ok: false,
18
+ diagnostics: [{
19
+ code: 'E_PARSE',
20
+ path: 'source',
21
+ message: err instanceof Error ? err.message : String(err),
22
+ }],
23
+ };
24
+ }
25
+ const diagnostics = [];
26
+ // Unknown chart type — silent no-op in render-chart.ts:52-55
27
+ const canonical = canonicalChartType(ast.chartType);
28
+ if (!canonical || !getChart(canonical)) {
29
+ const known = listCanonicalChartTypes();
30
+ diagnostics.push({
31
+ code: 'E_UNKNOWN_CHART_TYPE',
32
+ path: 'chart',
33
+ message: `No renderer registered for chart type "${ast.chartType}".`,
34
+ context: { got: ast.chartType, known },
35
+ suggestion: nearestSuggestion(ast.chartType, known),
36
+ });
37
+ return { ok: false, diagnostics };
38
+ }
39
+ const definition = astToDefinition(ast);
40
+ // Empty data — silent return in render-chart.ts:25-31
41
+ if (definition.data.labels.length === 0) {
42
+ diagnostics.push({
43
+ code: 'E_NO_DATA',
44
+ path: 'data',
45
+ message: 'Chart has no parsed data rows. Add quoted-string labels with numeric values to the data block.',
46
+ });
47
+ }
48
+ // Scene index out of range
49
+ const sceneCount = (ast.scenes ?? []).length;
50
+ if (opts.sceneIndex !== undefined && opts.sceneIndex >= sceneCount) {
51
+ diagnostics.push({
52
+ code: 'E_UNKNOWN_SCENE_INDEX',
53
+ path: 'scene',
54
+ message: `Requested scene index ${opts.sceneIndex} but chart has ${sceneCount} scene(s).`,
55
+ context: { requested: opts.sceneIndex, availableSceneCount: sceneCount },
56
+ });
57
+ }
58
+ // Unresolved colorize/highlight targets
59
+ const labels = new Set(definition.data.labels);
60
+ const seriesNames = new Set();
61
+ for (const s of definition.data.series ?? []) {
62
+ if (s.name) {
63
+ seriesNames.add(s.name);
64
+ }
65
+ }
66
+ for (const target of colorizeTargets(ast)) {
67
+ if (!labels.has(target) && !seriesNames.has(target)) {
68
+ diagnostics.push({
69
+ code: 'E_UNRESOLVED_COLORIZE',
70
+ path: 'colorize',
71
+ message: `colorize "${target}" does not match any data label or series name.`,
72
+ context: { target, availableLabels: [...labels], availableSeries: [...seriesNames] },
73
+ suggestion: nearestSuggestion(target, [...labels, ...seriesNames]),
74
+ });
75
+ }
76
+ }
77
+ for (const target of highlightTargets(ast)) {
78
+ if (!labels.has(target) && !seriesNames.has(target)) {
79
+ diagnostics.push({
80
+ code: 'E_UNRESOLVED_HIGHLIGHT',
81
+ path: 'highlight',
82
+ message: `highlight "${target}" does not match any data label or series name.`,
83
+ context: { target, availableLabels: [...labels], availableSeries: [...seriesNames] },
84
+ suggestion: nearestSuggestion(target, [...labels, ...seriesNames]),
85
+ });
86
+ }
87
+ }
88
+ // Resolve scene (defensive — surfaces internal errors)
89
+ try {
90
+ resolveScene(definition, opts.sceneIndex);
91
+ }
92
+ catch (err) {
93
+ diagnostics.push({
94
+ code: 'E_NO_RESOLVED_SERIES',
95
+ path: 'scene',
96
+ message: `Failed to resolve scene state: ${err instanceof Error ? err.message : String(err)}`,
97
+ });
98
+ }
99
+ return diagnostics.length === 0 ? { ok: true } : { ok: false, diagnostics };
100
+ }
@@ -0,0 +1 @@
1
+ export {};