@blueprint-chart/mcp 0.1.0 → 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 +15 -13
  101. package/public/apple-touch-icon.png +0 -0
  102. package/public/favicon.png +0 -0
  103. package/public/favicon.svg +9 -0
@@ -1,8 +1,8 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
3
  import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
4
4
  import { samples } from '@blueprint-chart/lib';
5
- import { createServer } from './server';
5
+ import { createServer, TOOLS } from './server';
6
6
  async function connectInMemory() {
7
7
  const server = createServer();
8
8
  const [clientT, serverT] = InMemoryTransport.createLinkedPair();
@@ -11,12 +11,56 @@ async function connectInMemory() {
11
11
  await client.connect(clientT);
12
12
  return { client, server };
13
13
  }
14
+ describe('TOOLS registry', () => {
15
+ it('contains the nine expected tool names', () => {
16
+ expect(Object.keys(TOOLS).sort()).toEqual([
17
+ 'describe_chart_type',
18
+ 'export_chart',
19
+ 'get_example',
20
+ 'get_grammar',
21
+ 'inspect_dsl',
22
+ 'list_chart_types',
23
+ 'recommend_chart_type',
24
+ 'render',
25
+ 'validate_dsl',
26
+ ]);
27
+ });
28
+ });
14
29
  describe('server', () => {
15
- it('lists 4 tools', async () => {
30
+ it('lists 9 tools', async () => {
16
31
  const { client } = await connectInMemory();
17
32
  const r = await client.listTools();
18
33
  const names = r.tools.map(t => t.name).sort();
19
- expect(names).toEqual(['inspect_dsl', 'recommend_chart_type', 'render', 'validate_dsl']);
34
+ expect(names).toEqual([
35
+ 'describe_chart_type',
36
+ 'export_chart',
37
+ 'get_example',
38
+ 'get_grammar',
39
+ 'inspect_dsl',
40
+ 'list_chart_types',
41
+ 'recommend_chart_type',
42
+ 'render',
43
+ 'validate_dsl',
44
+ ]);
45
+ });
46
+ it('publishes JSON Schemas with concrete properties (not a permissive stub)', async () => {
47
+ const { client } = await connectInMemory();
48
+ const r = await client.listTools();
49
+ for (const tool of r.tools) {
50
+ const schema = tool.inputSchema;
51
+ expect(schema.type, `${tool.name}: schema.type`).toBe('object');
52
+ // Discovery tools like list_chart_types take no params — their `properties`
53
+ // object exists but is empty. Every other tool has at least one property.
54
+ expect(schema.properties, `${tool.name}: schema.properties`).toBeDefined();
55
+ }
56
+ const validate = r.tools.find(t => t.name === 'validate_dsl');
57
+ const validateSchema = validate.inputSchema;
58
+ expect(validateSchema.properties.source).toBeDefined();
59
+ expect(validateSchema.required).toEqual(['source']);
60
+ const render = r.tools.find(t => t.name === 'render');
61
+ const renderSchema = render.inputSchema;
62
+ expect(renderSchema.properties.format).toBeDefined();
63
+ expect(renderSchema.properties.save).toBeDefined();
20
64
  });
21
65
  it('calls validate_dsl successfully for a sample', async () => {
22
66
  const { client } = await connectInMemory();
@@ -58,6 +102,32 @@ describe('server', () => {
58
102
  const first = r.contents[0];
59
103
  expect(first.text).toMatch(/chart\s+\w/);
60
104
  });
105
+ describe('serverInfo metadata', () => {
106
+ afterEach(() => {
107
+ vi.unstubAllEnvs();
108
+ });
109
+ it('always advertises title + dynamic version', async () => {
110
+ vi.stubEnv('MCP_PUBLIC_URL', '');
111
+ const { client } = await connectInMemory();
112
+ const info = client.getServerVersion();
113
+ expect(info?.title).toBe('Blueprint Chart');
114
+ expect(info?.version).toMatch(/^\d+\.\d+\.\d+/);
115
+ });
116
+ it('omits icons when MCP_PUBLIC_URL is unset (stdio / local use)', async () => {
117
+ vi.stubEnv('MCP_PUBLIC_URL', '');
118
+ const { client } = await connectInMemory();
119
+ expect(client.getServerVersion()?.icons).toBeUndefined();
120
+ });
121
+ it('advertises absolute-URL icons when MCP_PUBLIC_URL is set', async () => {
122
+ vi.stubEnv('MCP_PUBLIC_URL', 'https://mcp.example.com/');
123
+ const { client } = await connectInMemory();
124
+ const icons = client.getServerVersion()?.icons;
125
+ expect(icons).toEqual([
126
+ { src: 'https://mcp.example.com/favicon.svg', mimeType: 'image/svg+xml' },
127
+ { src: 'https://mcp.example.com/favicon.png', mimeType: 'image/png', sizes: ['256x256'] },
128
+ ]);
129
+ });
130
+ });
61
131
  it('exposes author_chart prompt', async () => {
62
132
  const { client } = await connectInMemory();
63
133
  const prompts = await client.listPrompts();
@@ -65,4 +135,31 @@ describe('server', () => {
65
135
  const got = await client.getPrompt({ name: 'author_chart' });
66
136
  expect(got.messages.length).toBeGreaterThan(0);
67
137
  });
138
+ describe('export_chart', () => {
139
+ const VALID_SOURCE = 'chart bar-vertical {\n data {\n "A" = 1\n }\n}\n';
140
+ afterEach(() => {
141
+ delete process.env.BLUEPRINT_CHART_EDITOR_URL;
142
+ });
143
+ it('appears in tools/list with an object-type inputSchema', async () => {
144
+ const { client } = await connectInMemory();
145
+ const r = await client.listTools();
146
+ const tool = r.tools.find(t => t.name === 'export_chart');
147
+ expect(tool).toBeDefined();
148
+ expect(tool.inputSchema.type).toBe('object');
149
+ });
150
+ it('returns E_CONFIG when BLUEPRINT_CHART_EDITOR_URL is unset', async () => {
151
+ delete process.env.BLUEPRINT_CHART_EDITOR_URL;
152
+ const { client } = await connectInMemory();
153
+ const res = await client.callTool({ name: 'export_chart', arguments: { source: VALID_SOURCE } });
154
+ expect(res.isError).toBe(true);
155
+ expect(JSON.stringify(res.content)).toMatch(/E_CONFIG/);
156
+ });
157
+ it('returns copyUrl with #/copy?bpc64= when BLUEPRINT_CHART_EDITOR_URL is set', async () => {
158
+ process.env.BLUEPRINT_CHART_EDITOR_URL = 'https://blueprintchart.com';
159
+ const { client } = await connectInMemory();
160
+ const res = await client.callTool({ name: 'export_chart', arguments: { source: VALID_SOURCE } });
161
+ expect(res.isError).toBeFalsy();
162
+ expect(JSON.stringify(res.content)).toMatch(/#\/copy\?bpc64=/);
163
+ });
164
+ });
68
165
  });
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ import { type ToolResult } from '../errors';
3
+ export declare const DescribeChartTypeInputSchema: z.ZodObject<{
4
+ chartType: z.ZodString;
5
+ }, "strict", z.ZodTypeAny, {
6
+ chartType: string;
7
+ }, {
8
+ chartType: string;
9
+ }>;
10
+ export type DescribeChartTypeInput = z.infer<typeof DescribeChartTypeInputSchema>;
11
+ export interface ChartTypeProperty {
12
+ key: string;
13
+ type: string;
14
+ description?: string;
15
+ choices?: string[];
16
+ default?: unknown;
17
+ }
18
+ export interface ChartTypeDataShape {
19
+ kind: 'single-series' | 'multi-series' | 'unknown';
20
+ example: string;
21
+ }
22
+ export interface DescribeChartTypeOutput {
23
+ name: string;
24
+ aliases: string[];
25
+ summary: string;
26
+ whenToUse: string[];
27
+ whenNotToUse: string[];
28
+ properties: ChartTypeProperty[];
29
+ dataShape: ChartTypeDataShape;
30
+ exampleSlug?: string;
31
+ docsUrl?: string;
32
+ }
33
+ export declare function describeChartType(input: unknown): ToolResult<DescribeChartTypeOutput>;
@@ -0,0 +1,119 @@
1
+ import { z } from 'zod';
2
+ import { getChartOptions, samples } from '@blueprint-chart/lib';
3
+ import { getDoc, listDocs } from '@blueprint-chart/docs';
4
+ import { aliasesFor, canonicalChartType, listCanonicalChartTypes } from '../dsl/chartTypes';
5
+ import { nearestSuggestion } from '../dsl/suggest';
6
+ import { UNIVERSAL_PROPERTIES, UNIVERSAL_PROPERTY_META } from '../dsl/universalProperties';
7
+ import { ErrorCode, toolError, toolOk } from '../errors';
8
+ import { publicDocUrl } from '../resources/docsReader';
9
+ export const DescribeChartTypeInputSchema = z.object({
10
+ chartType: z.string(),
11
+ }).strict();
12
+ function extractDocSections(name) {
13
+ const entries = listDocs('charts');
14
+ const entry = entries.find(e => e.slug === name);
15
+ if (!entry) {
16
+ return { summary: '', whenToUse: [], whenNotToUse: [], example: '' };
17
+ }
18
+ let content;
19
+ try {
20
+ content = getDoc('charts', name).content;
21
+ }
22
+ catch {
23
+ return { summary: '', whenToUse: [], whenNotToUse: [], example: '' };
24
+ }
25
+ const summary = content.match(/^>\s*(.+)$/m)?.[1]?.trim() ?? '';
26
+ const sectionBullets = (heading) => {
27
+ const re = new RegExp(`##\\s+${heading}[\\s\\S]*?(?=^##\\s+|\\Z)`, 'm');
28
+ const block = content.match(re)?.[0] ?? '';
29
+ return Array.from(block.matchAll(/^-\s+(.+)$/gm)).map(m => m[1].trim());
30
+ };
31
+ const example = content.match(/```bpc\n([\s\S]*?)```/)?.[1]?.trim() ?? '';
32
+ return {
33
+ summary,
34
+ whenToUse: sectionBullets('When to use'),
35
+ whenNotToUse: sectionBullets('When NOT to use'),
36
+ example,
37
+ };
38
+ }
39
+ function mapOption(opt) {
40
+ const prop = {
41
+ key: opt.key,
42
+ type: String(opt.type),
43
+ };
44
+ if (opt.label) {
45
+ prop.description = opt.label;
46
+ }
47
+ if (opt.choices !== undefined && opt.choices.length > 0) {
48
+ prop.choices = opt.choices.map(c => String(c.value));
49
+ }
50
+ if (opt.default !== undefined) {
51
+ prop.default = opt.default;
52
+ }
53
+ return prop;
54
+ }
55
+ function buildProperties(canonical) {
56
+ const registeredOptions = getChartOptions(canonical);
57
+ const registeredKeys = new Set(registeredOptions.map(o => o.key));
58
+ const props = registeredOptions.map(mapOption);
59
+ // Merge in universal properties that are not already present in the
60
+ // per-type registry. This ensures discoverable properties like `sort`,
61
+ // `title`, and `colors` appear for every chart type.
62
+ for (const key of UNIVERSAL_PROPERTIES) {
63
+ if (!registeredKeys.has(key)) {
64
+ const meta = UNIVERSAL_PROPERTY_META[key];
65
+ if (meta) {
66
+ props.push({ key, ...meta });
67
+ }
68
+ else {
69
+ props.push({ key, type: 'text' });
70
+ }
71
+ }
72
+ }
73
+ return props;
74
+ }
75
+ function inferDataShape(name, example) {
76
+ if (example.includes('_series')) {
77
+ return { kind: 'multi-series', example };
78
+ }
79
+ if (example.includes('data')) {
80
+ return { kind: 'single-series', example };
81
+ }
82
+ return { kind: 'unknown', example: 'data {\n "Label" = 1.0\n}' };
83
+ }
84
+ export function describeChartType(input) {
85
+ const parsed = DescribeChartTypeInputSchema.safeParse(input);
86
+ if (!parsed.success) {
87
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
88
+ }
89
+ const canonical = canonicalChartType(parsed.data.chartType);
90
+ if (!canonical) {
91
+ const known = listCanonicalChartTypes();
92
+ return toolError(ErrorCode.E_INPUT, [{
93
+ code: 'E_UNKNOWN_CHART_TYPE',
94
+ path: 'chartType',
95
+ message: `Unknown chart type "${parsed.data.chartType}". Known types: ${known.join(', ')}.`,
96
+ suggestion: nearestSuggestion(parsed.data.chartType, known),
97
+ context: { got: parsed.data.chartType, known },
98
+ }]);
99
+ }
100
+ const doc = extractDocSections(canonical);
101
+ const properties = buildProperties(canonical);
102
+ const sample = samples.find(s => s.chartType === canonical);
103
+ const exampleText = doc.example || sample?.dsl || '';
104
+ const output = {
105
+ name: canonical,
106
+ aliases: aliasesFor(canonical),
107
+ summary: doc.summary,
108
+ whenToUse: doc.whenToUse,
109
+ whenNotToUse: doc.whenNotToUse,
110
+ properties,
111
+ dataShape: inferDataShape(canonical, exampleText),
112
+ exampleSlug: sample?.id,
113
+ };
114
+ const docsUrl = publicDocUrl('charts', canonical);
115
+ if (docsUrl) {
116
+ output.docsUrl = docsUrl;
117
+ }
118
+ return toolOk(output);
119
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { describeChartType } from './describeChartType';
3
+ describe('describe_chart_type', () => {
4
+ afterEach(() => {
5
+ delete process.env.BLUEPRINT_CHART_DOCS_URL;
6
+ });
7
+ it('returns properties + summary for bar-horizontal', () => {
8
+ const r = describeChartType({ chartType: 'bar-horizontal' });
9
+ expect(r.ok).toBe(true);
10
+ if (r.ok) {
11
+ expect(r.data.name).toBe('bar-horizontal');
12
+ expect(r.data.aliases).toContain('horizontal-bar');
13
+ expect(r.data.summary.length).toBeGreaterThan(0);
14
+ expect(r.data.properties.length).toBeGreaterThan(0);
15
+ const sort = r.data.properties.find(p => p.key === 'sort');
16
+ expect(sort).toBeDefined();
17
+ expect(sort?.type).toBeDefined();
18
+ }
19
+ });
20
+ it('accepts an alias and normalizes', () => {
21
+ const r = describeChartType({ chartType: 'horizontal-bar' });
22
+ expect(r.ok).toBe(true);
23
+ if (r.ok) {
24
+ expect(r.data.name).toBe('bar-horizontal');
25
+ }
26
+ });
27
+ it('errors with structured suggestion on unknown name', () => {
28
+ const r = describeChartType({ chartType: 'bar' });
29
+ expect(r.ok).toBe(false);
30
+ if (!r.ok) {
31
+ expect(r.errors[0].code).toBe('E_UNKNOWN_CHART_TYPE');
32
+ expect(r.errors[0].suggestion).toMatch(/^bar-/);
33
+ }
34
+ });
35
+ it('includes a starter dataShape example', () => {
36
+ const r = describeChartType({ chartType: 'bar-vertical' });
37
+ expect(r.ok).toBe(true);
38
+ if (r.ok) {
39
+ expect(r.data.dataShape.kind).toMatch(/single-series|multi-series/);
40
+ expect(r.data.dataShape.example).toContain('data');
41
+ }
42
+ });
43
+ it('omits docsUrl when docs base is unset', () => {
44
+ const result = describeChartType({ chartType: 'bar-vertical' });
45
+ expect(result.ok).toBe(true);
46
+ if (result.ok) {
47
+ expect(result.data.docsUrl).toBeUndefined();
48
+ }
49
+ });
50
+ it('includes a top-level docsUrl when docs base is set', () => {
51
+ process.env.BLUEPRINT_CHART_DOCS_URL = 'https://docs.blueprintchart.com';
52
+ const result = describeChartType({ chartType: 'bar-vertical' });
53
+ expect(result.ok).toBe(true);
54
+ if (result.ok) {
55
+ expect(result.data.docsUrl).toBe('https://docs.blueprintchart.com/charts/bar-vertical');
56
+ }
57
+ });
58
+ });
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import { type FrameMetadata } from '../render/frame';
3
+ import { type ToolResult } from '../errors';
4
+ export declare const ExportChartInputSchema: z.ZodObject<{
5
+ source: z.ZodString;
6
+ }, "strict", z.ZodTypeAny, {
7
+ source: string;
8
+ }, {
9
+ source: string;
10
+ }>;
11
+ export type ExportChartInput = z.infer<typeof ExportChartInputSchema>;
12
+ export interface ExportChartOutput {
13
+ copyUrl: string;
14
+ embedUrl: string;
15
+ frame: FrameMetadata;
16
+ }
17
+ export declare function exportChart(input: unknown): ToolResult<ExportChartOutput>;
@@ -0,0 +1,31 @@
1
+ import { z } from 'zod';
2
+ import { getEditorBaseUrl } from '../links/editorConfig';
3
+ import { buildCopyUrl, buildEmbedUrl } from '../links/buildUrls';
4
+ import { validatePipeline } from '../render/validatePipeline';
5
+ import { extractFrameMetadata } from '../render/frame';
6
+ import { ErrorCode, toolError, toolOk } from '../errors';
7
+ export const ExportChartInputSchema = z.object({
8
+ source: z.string(),
9
+ }).strict();
10
+ export function exportChart(input) {
11
+ const parsed = ExportChartInputSchema.safeParse(input);
12
+ if (!parsed.success) {
13
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
14
+ }
15
+ const editorBase = getEditorBaseUrl();
16
+ if (!editorBase) {
17
+ return toolError(ErrorCode.E_CONFIG, [{
18
+ code: 'E_EDITOR_URL_UNSET',
19
+ message: 'Link export is not configured. Set BLUEPRINT_CHART_EDITOR_URL to enable.',
20
+ }]);
21
+ }
22
+ const validated = validatePipeline(parsed.data.source);
23
+ if (!validated.ok) {
24
+ return validated.error;
25
+ }
26
+ return toolOk({
27
+ copyUrl: buildCopyUrl(parsed.data.source, editorBase),
28
+ embedUrl: buildEmbedUrl(parsed.data.source, editorBase),
29
+ frame: extractFrameMetadata(validated.ast),
30
+ });
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { exportChart } from './exportChart';
3
+ import { ErrorCode } from '../errors';
4
+ const VALID = 'chart bar-vertical {\n title = "Hi"\n data {\n "A" = 1\n }\n}\n';
5
+ afterEach(() => {
6
+ delete process.env.BLUEPRINT_CHART_EDITOR_URL;
7
+ });
8
+ describe('exportChart', () => {
9
+ it('returns E_CONFIG when the editor base URL is unset', () => {
10
+ const result = exportChart({ source: VALID });
11
+ expect(result.ok).toBe(false);
12
+ if (!result.ok) {
13
+ expect(result.code).toBe(ErrorCode.E_CONFIG);
14
+ expect(result.errors[0]?.message).toMatch(/BLUEPRINT_CHART_EDITOR_URL/);
15
+ }
16
+ });
17
+ it('returns E_INPUT when source is missing', () => {
18
+ process.env.BLUEPRINT_CHART_EDITOR_URL = 'https://blueprintchart.com';
19
+ const result = exportChart({});
20
+ expect(result.ok).toBe(false);
21
+ if (!result.ok) {
22
+ expect(result.code).toBe(ErrorCode.E_INPUT);
23
+ }
24
+ });
25
+ it('propagates E_PARSE from the shared validation pipeline', () => {
26
+ process.env.BLUEPRINT_CHART_EDITOR_URL = 'https://blueprintchart.com';
27
+ const result = exportChart({ source: 'chart bar-vertical {' });
28
+ expect(result.ok).toBe(false);
29
+ if (!result.ok) {
30
+ expect(result.code).toBe(ErrorCode.E_PARSE);
31
+ }
32
+ });
33
+ it('returns copy + embed URLs and frame for a valid source', () => {
34
+ process.env.BLUEPRINT_CHART_EDITOR_URL = 'https://blueprintchart.com';
35
+ const result = exportChart({ source: VALID });
36
+ expect(result.ok).toBe(true);
37
+ if (result.ok) {
38
+ expect(result.data.copyUrl).toMatch(/^https:\/\/blueprintchart\.com\/#\/copy\?bpc64=/);
39
+ expect(result.data.embedUrl).toMatch(/^https:\/\/blueprintchart\.com\/#\/render\?bpc64=/);
40
+ expect(result.data.frame.title).toBe('Hi');
41
+ }
42
+ });
43
+ });
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod';
2
+ import { type ToolResult } from '../errors';
3
+ export declare const GetExampleInputSchema: z.ZodObject<{
4
+ chartType: z.ZodOptional<z.ZodString>;
5
+ name: z.ZodOptional<z.ZodString>;
6
+ }, "strict", z.ZodTypeAny, {
7
+ name?: string | undefined;
8
+ chartType?: string | undefined;
9
+ }, {
10
+ name?: string | undefined;
11
+ chartType?: string | undefined;
12
+ }>;
13
+ export type GetExampleInput = z.infer<typeof GetExampleInputSchema>;
14
+ export interface GetExampleOutput {
15
+ id: string;
16
+ title: string;
17
+ chartType: string;
18
+ dsl: string;
19
+ }
20
+ export declare function getExample(input: unknown): ToolResult<GetExampleOutput>;
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod';
2
+ import { samples } from '@blueprint-chart/lib';
3
+ import { canonicalChartType } from '../dsl/chartTypes';
4
+ import { ErrorCode, toolError, toolOk } from '../errors';
5
+ export const GetExampleInputSchema = z.object({
6
+ chartType: z.string().optional(),
7
+ name: z.string().optional(),
8
+ }).strict();
9
+ const STARTER_SAMPLE_ID = 'letter-frequency';
10
+ export function getExample(input) {
11
+ const parsed = GetExampleInputSchema.safeParse(input);
12
+ if (!parsed.success) {
13
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
14
+ }
15
+ const { chartType, name } = parsed.data;
16
+ if (name) {
17
+ const found = samples.find(s => s.id === name);
18
+ if (!found) {
19
+ return toolError(ErrorCode.E_INPUT, [{
20
+ code: 'E_UNKNOWN_SAMPLE',
21
+ path: 'name',
22
+ message: `No sample with id "${name}". Try one of: ${samples.map(s => s.id).join(', ')}.`,
23
+ context: { knownIds: samples.map(s => s.id) },
24
+ }]);
25
+ }
26
+ return toolOk({ id: found.id, title: found.title, chartType: found.chartType, dsl: found.dsl });
27
+ }
28
+ if (chartType) {
29
+ const canonical = canonicalChartType(chartType);
30
+ if (!canonical) {
31
+ return toolError(ErrorCode.E_INPUT, [{
32
+ code: 'E_UNKNOWN_CHART_TYPE',
33
+ path: 'chartType',
34
+ message: `Unknown chart type "${chartType}".`,
35
+ context: { got: chartType },
36
+ }]);
37
+ }
38
+ const found = samples.find(s => s.chartType === canonical);
39
+ if (!found) {
40
+ return toolError(ErrorCode.E_INPUT, [{
41
+ code: 'E_NO_SAMPLE_FOR_TYPE',
42
+ path: 'chartType',
43
+ message: `No sample is shipped for chart type "${canonical}".`,
44
+ }]);
45
+ }
46
+ return toolOk({ id: found.id, title: found.title, chartType: found.chartType, dsl: found.dsl });
47
+ }
48
+ const starter = samples.find(s => s.id === STARTER_SAMPLE_ID) ?? samples[0];
49
+ if (!starter) {
50
+ return toolError(ErrorCode.E_INTERNAL, [{
51
+ message: 'No samples available — lib export is empty.',
52
+ }]);
53
+ }
54
+ return toolOk({ id: starter.id, title: starter.title, chartType: starter.chartType, dsl: starter.dsl });
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getExample } from './getExample';
3
+ describe('get_example', () => {
4
+ it('returns the starter sample when no args', () => {
5
+ const r = getExample({});
6
+ expect(r.ok).toBe(true);
7
+ if (r.ok) {
8
+ expect(r.data.id).toBe('letter-frequency');
9
+ expect(r.data.dsl).toContain('chart bar-vertical');
10
+ }
11
+ });
12
+ it('returns a sample by id', () => {
13
+ const r = getExample({ name: 'letter-frequency' });
14
+ expect(r.ok).toBe(true);
15
+ if (r.ok) {
16
+ expect(r.data.chartType).toBe('bar-vertical');
17
+ }
18
+ });
19
+ it('returns the first matching sample for a chartType', () => {
20
+ const r = getExample({ chartType: 'bar-horizontal' });
21
+ expect(r.ok).toBe(true);
22
+ if (r.ok) {
23
+ expect(r.data.chartType).toBe('bar-horizontal');
24
+ }
25
+ });
26
+ it('errors with E_INPUT for an unknown id', () => {
27
+ const r = getExample({ name: 'made-up-sample' });
28
+ expect(r.ok).toBe(false);
29
+ if (!r.ok) {
30
+ expect(r.code).toBe('E_INPUT');
31
+ }
32
+ });
33
+ it('errors with E_INPUT for an unknown chart type', () => {
34
+ const r = getExample({ chartType: 'bar' });
35
+ expect(r.ok).toBe(false);
36
+ if (!r.ok) {
37
+ expect(r.code).toBe('E_INPUT');
38
+ }
39
+ });
40
+ });
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import { type ToolResult } from '../errors';
3
+ declare const SectionSchema: z.ZodDefault<z.ZodEnum<["all", "chart", "properties", "scenes", "annotations"]>>;
4
+ export declare const GetGrammarInputSchema: z.ZodObject<{
5
+ section: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "chart", "properties", "scenes", "annotations"]>>>;
6
+ }, "strict", z.ZodTypeAny, {
7
+ section?: "chart" | "all" | "properties" | "scenes" | "annotations" | undefined;
8
+ }, {
9
+ section?: "chart" | "all" | "properties" | "scenes" | "annotations" | undefined;
10
+ }>;
11
+ export type GetGrammarInput = z.infer<typeof GetGrammarInputSchema>;
12
+ export interface GetGrammarOutput {
13
+ section: z.infer<typeof SectionSchema>;
14
+ text: string;
15
+ }
16
+ export declare function getGrammar(input: unknown): ToolResult<GetGrammarOutput>;
17
+ export {};
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ import { getDoc, listDocs } from '@blueprint-chart/docs';
3
+ import { ErrorCode, toolError, toolOk } from '../errors';
4
+ const SectionSchema = z.enum(['all', 'chart', 'properties', 'scenes', 'annotations']).default('all');
5
+ export const GetGrammarInputSchema = z.object({
6
+ section: SectionSchema.optional(),
7
+ }).strict();
8
+ const SECTION_TO_SLUG = {
9
+ chart: 'index',
10
+ properties: 'properties',
11
+ scenes: 'scenes-and-transforms',
12
+ annotations: 'annotations',
13
+ };
14
+ export function getGrammar(input) {
15
+ const parsed = GetGrammarInputSchema.safeParse(input);
16
+ if (!parsed.success) {
17
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
18
+ }
19
+ const section = parsed.data.section ?? 'all';
20
+ if (section === 'all') {
21
+ const pages = listDocs('reference/dsl');
22
+ const text = pages.map((entry) => {
23
+ const { content } = getDoc('reference/dsl', entry.slug);
24
+ return `# ${entry.title}\n\n${content}`;
25
+ }).join('\n\n---\n\n');
26
+ return toolOk({ section: 'all', text });
27
+ }
28
+ const slug = SECTION_TO_SLUG[section];
29
+ try {
30
+ const { content } = getDoc('reference/dsl', slug);
31
+ return toolOk({ section, text: content });
32
+ }
33
+ catch (err) {
34
+ return toolError(ErrorCode.E_INPUT, [{
35
+ message: `Grammar section "${section}" not found: ${err instanceof Error ? err.message : String(err)}`,
36
+ }]);
37
+ }
38
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getGrammar } from './getGrammar';
3
+ describe('get_grammar', () => {
4
+ it('returns the full grammar by default', () => {
5
+ const r = getGrammar({});
6
+ expect(r.ok).toBe(true);
7
+ if (r.ok) {
8
+ expect(r.data.section).toBe('all');
9
+ expect(r.data.text.length).toBeGreaterThan(100);
10
+ }
11
+ });
12
+ it('returns a single section when requested', () => {
13
+ const r = getGrammar({ section: 'properties' });
14
+ expect(r.ok).toBe(true);
15
+ if (r.ok) {
16
+ expect(r.data.section).toBe('properties');
17
+ expect(r.data.text.toLowerCase()).toContain('properties');
18
+ }
19
+ });
20
+ it('errors with E_INPUT for an unknown section', () => {
21
+ const r = getGrammar({ section: 'totally-made-up' });
22
+ expect(r.ok).toBe(false);
23
+ });
24
+ });