@blueprint-chart/mcp 0.1.1 → 0.1.4
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.
- package/README.md +31 -15
- package/dist/cli.js +15 -2
- package/dist/dsl/capabilityMatrix.d.ts +22 -0
- package/dist/dsl/capabilityMatrix.js +37 -0
- package/dist/dsl/capabilityMatrix.test.d.ts +1 -0
- package/dist/dsl/capabilityMatrix.test.js +49 -0
- package/dist/dsl/chartTypes.d.ts +16 -0
- package/dist/dsl/chartTypes.js +37 -0
- package/dist/dsl/chartTypes.test.d.ts +1 -0
- package/dist/dsl/chartTypes.test.js +32 -0
- package/dist/dsl/dataKey.d.ts +25 -0
- package/dist/dsl/dataKey.js +42 -0
- package/dist/dsl/dataKey.test.d.ts +1 -0
- package/dist/dsl/dataKey.test.js +35 -0
- package/dist/dsl/goalRanking.d.ts +7 -0
- package/dist/dsl/goalRanking.js +76 -0
- package/dist/dsl/goalRanking.test.d.ts +1 -0
- package/dist/dsl/goalRanking.test.js +83 -0
- package/dist/dsl/parseErrorHints.d.ts +12 -0
- package/dist/dsl/parseErrorHints.js +32 -0
- package/dist/dsl/parseErrorHints.test.d.ts +1 -0
- package/dist/dsl/parseErrorHints.test.js +26 -0
- package/dist/dsl/semanticWarnings.d.ts +7 -0
- package/dist/dsl/semanticWarnings.js +66 -0
- package/dist/dsl/semanticWarnings.test.d.ts +1 -0
- package/dist/dsl/semanticWarnings.test.js +32 -0
- package/dist/dsl/suggest.d.ts +1 -0
- package/dist/dsl/suggest.js +66 -0
- package/dist/dsl/suggest.test.d.ts +1 -0
- package/dist/dsl/suggest.test.js +34 -0
- package/dist/dsl/universalProperties.d.ts +30 -0
- package/dist/dsl/universalProperties.js +52 -0
- package/dist/dsl/universalProperties.test.d.ts +1 -0
- package/dist/dsl/universalProperties.test.js +26 -0
- package/dist/dsl/validate.d.ts +10 -0
- package/dist/dsl/validate.js +68 -0
- package/dist/dsl/validate.test.d.ts +1 -0
- package/dist/dsl/validate.test.js +73 -0
- package/dist/errors.d.ts +20 -1
- package/dist/errors.js +1 -0
- package/dist/errors.test.js +21 -0
- package/dist/lib/zodToJsonSchema.d.ts +10 -5
- package/dist/lib/zodToJsonSchema.js +14 -6
- package/dist/links/buildUrls.d.ts +14 -0
- package/dist/links/buildUrls.js +20 -0
- package/dist/links/buildUrls.test.d.ts +1 -0
- package/dist/links/buildUrls.test.js +28 -0
- package/dist/links/editorConfig.d.ts +4 -0
- package/dist/links/editorConfig.js +15 -0
- package/dist/links/editorConfig.test.d.ts +1 -0
- package/dist/links/editorConfig.test.js +28 -0
- package/dist/links/encode.d.ts +11 -0
- package/dist/links/encode.js +19 -0
- package/dist/links/encode.test.d.ts +1 -0
- package/dist/links/encode.test.js +37 -0
- package/dist/parse.js +14 -6
- package/dist/parse.test.js +8 -0
- package/dist/prompts/authorChart.js +23 -18
- package/dist/prompts/authorChart.test.js +6 -0
- package/dist/render/diagnose.d.ts +19 -0
- package/dist/render/diagnose.js +100 -0
- package/dist/render/diagnose.test.d.ts +1 -0
- package/dist/render/diagnose.test.js +53 -0
- package/dist/render/frame.d.ts +10 -0
- package/dist/render/frame.js +10 -0
- package/dist/render/frame.test.d.ts +1 -0
- package/dist/render/frame.test.js +12 -0
- package/dist/render/jsdomEnv.d.ts +2 -1
- package/dist/render/jsdomEnv.js +14 -1
- package/dist/render/jsdomEnv.test.js +36 -2
- package/dist/render/renderSceneState.d.ts +5 -1
- package/dist/render/renderSceneState.js +4 -3
- package/dist/render/renderSceneState.test.js +13 -7
- package/dist/render/validatePipeline.d.ts +23 -0
- package/dist/render/validatePipeline.js +41 -0
- package/dist/render/validatePipeline.test.d.ts +1 -0
- package/dist/render/validatePipeline.test.js +34 -0
- package/dist/resources/docsReader.d.ts +4 -1
- package/dist/resources/docsReader.js +23 -6
- package/dist/resources/docsReader.test.js +27 -2
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/samples.d.ts +1 -2
- package/dist/server.d.ts +9 -0
- package/dist/server.js +75 -5
- package/dist/server.test.js +105 -4
- package/dist/tools/describeChartType.d.ts +41 -0
- package/dist/tools/describeChartType.js +143 -0
- package/dist/tools/describeChartType.test.d.ts +1 -0
- package/dist/tools/describeChartType.test.js +78 -0
- package/dist/tools/exportChart.d.ts +17 -0
- package/dist/tools/exportChart.js +31 -0
- package/dist/tools/exportChart.test.d.ts +1 -0
- package/dist/tools/exportChart.test.js +43 -0
- package/dist/tools/getExample.d.ts +20 -0
- package/dist/tools/getExample.js +55 -0
- package/dist/tools/getExample.test.d.ts +1 -0
- package/dist/tools/getExample.test.js +40 -0
- package/dist/tools/getGrammar.d.ts +17 -0
- package/dist/tools/getGrammar.js +38 -0
- package/dist/tools/getGrammar.test.d.ts +1 -0
- package/dist/tools/getGrammar.test.js +35 -0
- package/dist/tools/inspect.d.ts +8 -1
- package/dist/tools/inspect.js +40 -7
- package/dist/tools/inspect.test.js +62 -13
- package/dist/tools/listChartTypes.d.ts +14 -0
- package/dist/tools/listChartTypes.js +42 -0
- package/dist/tools/listChartTypes.test.d.ts +1 -0
- package/dist/tools/listChartTypes.test.js +42 -0
- package/dist/tools/listPalettes.d.ts +13 -0
- package/dist/tools/listPalettes.js +12 -0
- package/dist/tools/listPalettes.test.d.ts +1 -0
- package/dist/tools/listPalettes.test.js +15 -0
- package/dist/tools/recommend.js +3 -1
- package/dist/tools/recommend.test.js +40 -0
- package/dist/tools/render.d.ts +14 -12
- package/dist/tools/render.js +96 -28
- package/dist/tools/render.test.js +137 -1
- package/dist/tools/searchExamples.d.ts +28 -0
- package/dist/tools/searchExamples.js +54 -0
- package/dist/tools/searchExamples.test.d.ts +1 -0
- package/dist/tools/searchExamples.test.js +32 -0
- package/dist/tools/validate.d.ts +9 -3
- package/dist/tools/validate.js +11 -1
- package/dist/tools/validate.test.js +33 -11
- package/dist/transports/http.d.ts +4 -2
- package/dist/transports/http.js +232 -23
- package/dist/transports/http.test.js +158 -22
- package/package.json +5 -3
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +9 -0
|
@@ -7,12 +7,46 @@ describe('createJsdomEnv', () => {
|
|
|
7
7
|
expect(env.container.ownerDocument).toBeDefined();
|
|
8
8
|
expect(env.window.SVGElement).toBeDefined();
|
|
9
9
|
});
|
|
10
|
-
it('
|
|
10
|
+
it('serializeSvg() returns bare SVG markup', () => {
|
|
11
11
|
const env = createJsdomEnv({ width: 600, height: 400 });
|
|
12
12
|
const svg = env.window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
13
13
|
svg.setAttribute('viewBox', '0 0 100 100');
|
|
14
14
|
env.container.appendChild(svg);
|
|
15
|
-
|
|
15
|
+
const result = env.serializeSvg();
|
|
16
|
+
expect(result).toMatch(/<svg[^>]*viewBox="0 0 100 100"/);
|
|
17
|
+
expect(result).toMatch(/^<svg/);
|
|
18
|
+
});
|
|
19
|
+
it('serializeFrame() returns undefined when no bc-frame is present', () => {
|
|
20
|
+
const env = createJsdomEnv({ width: 600, height: 400 });
|
|
21
|
+
const svg = env.window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
22
|
+
env.container.appendChild(svg);
|
|
23
|
+
expect(env.serializeFrame()).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
it('serializeFrame() returns frame outerHTML when bc-frame is present', () => {
|
|
26
|
+
const env = createJsdomEnv({ width: 600, height: 400 });
|
|
27
|
+
const frame = env.window.document.createElement('div');
|
|
28
|
+
frame.className = 'bc-frame';
|
|
29
|
+
frame.innerHTML = '<div class="bc-frame-body"><svg></svg></div>';
|
|
30
|
+
env.container.appendChild(frame);
|
|
31
|
+
const result = env.serializeFrame();
|
|
32
|
+
expect(result).toBeDefined();
|
|
33
|
+
expect(result).toContain('<div class="bc-frame"');
|
|
34
|
+
expect(result).toContain('<svg>');
|
|
35
|
+
});
|
|
36
|
+
it('serializeSvg() extracts inner svg from bc-frame-body when frame is present', () => {
|
|
37
|
+
const env = createJsdomEnv({ width: 600, height: 400 });
|
|
38
|
+
const frame = env.window.document.createElement('div');
|
|
39
|
+
frame.className = 'bc-frame';
|
|
40
|
+
const body = env.window.document.createElement('div');
|
|
41
|
+
body.className = 'bc-frame-body';
|
|
42
|
+
const svg = env.window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
43
|
+
svg.setAttribute('viewBox', '0 0 200 200');
|
|
44
|
+
body.appendChild(svg);
|
|
45
|
+
frame.appendChild(body);
|
|
46
|
+
env.container.appendChild(frame);
|
|
47
|
+
const result = env.serializeSvg();
|
|
48
|
+
expect(result).toMatch(/^<svg/);
|
|
49
|
+
expect(result).toMatch(/viewBox="0 0 200 200"/);
|
|
16
50
|
});
|
|
17
51
|
it('cleanup() closes the window', () => {
|
|
18
52
|
const env = createJsdomEnv({ width: 100, height: 100 });
|
|
@@ -4,6 +4,10 @@ export interface RenderSceneStateOptions {
|
|
|
4
4
|
height: number;
|
|
5
5
|
theme?: string;
|
|
6
6
|
}
|
|
7
|
+
export interface RenderSceneStateResult {
|
|
8
|
+
svg: string;
|
|
9
|
+
html?: string;
|
|
10
|
+
}
|
|
7
11
|
/**
|
|
8
12
|
* Render a `.bpc` source string to SVG markup in a headless jsdom environment.
|
|
9
13
|
*
|
|
@@ -18,4 +22,4 @@ export interface RenderSceneStateOptions {
|
|
|
18
22
|
* Throws when `renderBpc` cannot produce SVG output (invalid source, etc.) —
|
|
19
23
|
* callers (the `render` tool) catch and translate to a structured ToolResult.
|
|
20
24
|
*/
|
|
21
|
-
export declare function renderSceneState(source: string, opts: RenderSceneStateOptions):
|
|
25
|
+
export declare function renderSceneState(source: string, opts: RenderSceneStateOptions): RenderSceneStateResult;
|
|
@@ -49,7 +49,7 @@ export function renderSceneState(source, opts) {
|
|
|
49
49
|
try {
|
|
50
50
|
renderBpc(env.container, source, {
|
|
51
51
|
sceneIndex: opts.sceneIndex,
|
|
52
|
-
thumbnail:
|
|
52
|
+
thumbnail: false,
|
|
53
53
|
transition: false,
|
|
54
54
|
theme: opts.theme,
|
|
55
55
|
});
|
|
@@ -59,11 +59,12 @@ export function renderSceneState(source, opts) {
|
|
|
59
59
|
globals[key] = prev[key];
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
const svg = env.
|
|
62
|
+
const svg = env.serializeSvg();
|
|
63
63
|
if (!svg) {
|
|
64
64
|
throw new Error('renderBpc produced no SVG output');
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
const html = env.serializeFrame();
|
|
67
|
+
return { svg, html };
|
|
67
68
|
}
|
|
68
69
|
finally {
|
|
69
70
|
env.cleanup();
|
|
@@ -2,15 +2,21 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { samples } from '@blueprint-chart/lib';
|
|
3
3
|
import { renderSceneState } from './renderSceneState';
|
|
4
4
|
describe('renderSceneState', () => {
|
|
5
|
-
it('returns
|
|
6
|
-
const
|
|
7
|
-
expect(svg).toMatch(/^<svg/);
|
|
8
|
-
expect(svg).
|
|
9
|
-
expect(svg.length).toBeGreaterThan(500);
|
|
5
|
+
it('returns pure SVG (no HTML wrapper) for the first sample, scene 0', () => {
|
|
6
|
+
const result = renderSceneState(samples[0].dsl, { sceneIndex: 0, width: 800, height: 500 });
|
|
7
|
+
expect(result.svg).toMatch(/^<svg/);
|
|
8
|
+
expect(result.svg).toContain('</svg>');
|
|
9
|
+
expect(result.svg.length).toBeGreaterThan(500);
|
|
10
|
+
});
|
|
11
|
+
it('returns frame html containing bc-frame and the svg', () => {
|
|
12
|
+
const result = renderSceneState(samples[0].dsl, { sceneIndex: 0, width: 800, height: 500 });
|
|
13
|
+
expect(result.html).toBeDefined();
|
|
14
|
+
expect(result.html).toContain('<div class="bc-frame"');
|
|
15
|
+
expect(result.html).toContain('<svg');
|
|
10
16
|
});
|
|
11
17
|
it('clamps sceneIndex when out of bounds', () => {
|
|
12
|
-
const
|
|
13
|
-
expect(svg).toMatch(/^<svg/);
|
|
18
|
+
const result = renderSceneState(samples[0].dsl, { sceneIndex: 999, width: 400, height: 300 });
|
|
19
|
+
expect(result.svg).toMatch(/^<svg/);
|
|
14
20
|
});
|
|
15
21
|
it('throws for unparseable input (callers catch)', () => {
|
|
16
22
|
expect(() => renderSceneState('@@@ nope', { width: 400, height: 300 })).toThrow();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ChartNode } from '@blueprint-chart/lib';
|
|
2
|
+
import { type ToolResult } from '../errors';
|
|
3
|
+
export type ValidatePipelineResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
ast: ChartNode;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
error: Extract<ToolResult<never>, {
|
|
9
|
+
ok: false;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
export interface ValidatePipelineOptions {
|
|
13
|
+
sceneIndex?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Run the three-layer validation a renderable chart must pass:
|
|
17
|
+
* 1. parse (E_PARSE)
|
|
18
|
+
* 2. semantic validation — unknown types/props, empty data (E_SEMANTIC)
|
|
19
|
+
* 3. render diagnostic — colorize/highlight resolution, scene index (E_RENDER)
|
|
20
|
+
* On success returns the parsed AST. On failure returns the structured error
|
|
21
|
+
* exactly as `render` would, so callers share one set of error semantics.
|
|
22
|
+
*/
|
|
23
|
+
export declare function validatePipeline(source: string, opts?: ValidatePipelineOptions): ValidatePipelineResult;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseDsl } from '../parse';
|
|
2
|
+
import { validateAst } from '../dsl/validate';
|
|
3
|
+
import { diagnoseRender } from './diagnose';
|
|
4
|
+
import { ErrorCode, toolError } from '../errors';
|
|
5
|
+
/**
|
|
6
|
+
* Run the three-layer validation a renderable chart must pass:
|
|
7
|
+
* 1. parse (E_PARSE)
|
|
8
|
+
* 2. semantic validation — unknown types/props, empty data (E_SEMANTIC)
|
|
9
|
+
* 3. render diagnostic — colorize/highlight resolution, scene index (E_RENDER)
|
|
10
|
+
* On success returns the parsed AST. On failure returns the structured error
|
|
11
|
+
* exactly as `render` would, so callers share one set of error semantics.
|
|
12
|
+
*/
|
|
13
|
+
export function validatePipeline(source, opts = {}) {
|
|
14
|
+
const parseResult = parseDsl(source);
|
|
15
|
+
if (!parseResult.ok) {
|
|
16
|
+
return { ok: false, error: parseResult };
|
|
17
|
+
}
|
|
18
|
+
const issues = validateAst(parseResult.data.ast);
|
|
19
|
+
if (issues.length > 0) {
|
|
20
|
+
const entries = issues.map(i => ({
|
|
21
|
+
code: i.code,
|
|
22
|
+
path: i.path,
|
|
23
|
+
message: i.message,
|
|
24
|
+
suggestion: i.suggestion,
|
|
25
|
+
context: i.context,
|
|
26
|
+
}));
|
|
27
|
+
return { ok: false, error: toolError(ErrorCode.E_SEMANTIC, entries) };
|
|
28
|
+
}
|
|
29
|
+
const diag = diagnoseRender(source, { sceneIndex: opts.sceneIndex });
|
|
30
|
+
if (!diag.ok) {
|
|
31
|
+
const entries = diag.diagnostics.map(d => ({
|
|
32
|
+
code: d.code,
|
|
33
|
+
path: d.path,
|
|
34
|
+
message: d.message,
|
|
35
|
+
suggestion: d.suggestion,
|
|
36
|
+
context: d.context,
|
|
37
|
+
}));
|
|
38
|
+
return { ok: false, error: toolError(ErrorCode.E_RENDER, entries) };
|
|
39
|
+
}
|
|
40
|
+
return { ok: true, ast: parseResult.data.ast };
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validatePipeline } from './validatePipeline';
|
|
3
|
+
import { ErrorCode } from '../errors';
|
|
4
|
+
describe('validatePipeline', () => {
|
|
5
|
+
it('returns ok with the parsed AST for a valid source', () => {
|
|
6
|
+
const result = validatePipeline('chart bar-vertical {\n data {\n "A" = 1\n }\n}\n');
|
|
7
|
+
expect(result.ok).toBe(true);
|
|
8
|
+
if (result.ok) {
|
|
9
|
+
expect(result.ast.chartType).toBe('bar-vertical');
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
it('returns E_PARSE on a syntax error', () => {
|
|
13
|
+
const result = validatePipeline('chart bar-vertical {');
|
|
14
|
+
expect(result.ok).toBe(false);
|
|
15
|
+
if (!result.ok) {
|
|
16
|
+
expect(result.error.code).toBe(ErrorCode.E_PARSE);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
it('returns E_SEMANTIC for an unknown chart type', () => {
|
|
20
|
+
const result = validatePipeline('chart not-a-chart {\n data {\n "A" = 1\n }\n}\n');
|
|
21
|
+
expect(result.ok).toBe(false);
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
expect(result.error.code).toBe(ErrorCode.E_SEMANTIC);
|
|
24
|
+
expect(result.error.errors[0]?.suggestion).toBeDefined();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
it('returns E_RENDER when a render diagnostic fails (unresolved highlight)', () => {
|
|
28
|
+
const result = validatePipeline('chart bar-vertical {\n highlight "Z"\n data {\n "A" = 1\n }\n}\n');
|
|
29
|
+
expect(result.ok).toBe(false);
|
|
30
|
+
if (!result.ok) {
|
|
31
|
+
expect(result.error.code).toBe(ErrorCode.E_RENDER);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { type DocEntry } from '@blueprint-chart/docs';
|
|
1
|
+
import { type DocGroup, type DocEntry } from '@blueprint-chart/docs';
|
|
2
2
|
export interface UriResource {
|
|
3
3
|
uri: string;
|
|
4
4
|
name: string;
|
|
5
5
|
description?: string;
|
|
6
6
|
mimeType: string;
|
|
7
|
+
docsUrl?: string;
|
|
7
8
|
}
|
|
9
|
+
/** Public docs page URL for a docs group + slug, or undefined when docs base is unset. */
|
|
10
|
+
export declare function publicDocUrl(group: DocGroup, slug: string): string | undefined;
|
|
8
11
|
export declare function listAllResources(): UriResource[];
|
|
9
12
|
export declare function readResource(uri: string): {
|
|
10
13
|
uri: string;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getDoc, listDocs } from '@blueprint-chart/docs';
|
|
2
|
+
import { getDocsBaseUrl } from '../links/editorConfig';
|
|
3
|
+
import { buildDocUrl } from '../links/buildUrls';
|
|
2
4
|
const GROUP_TO_URI = {
|
|
3
5
|
'handbook': 'bpc://handbook/',
|
|
4
6
|
'guide': 'bpc://guide/',
|
|
@@ -8,14 +10,29 @@ const GROUP_TO_URI = {
|
|
|
8
10
|
};
|
|
9
11
|
const URI_TO_GROUP = Object.entries(GROUP_TO_URI).map(([group, prefix]) => ({ prefix, group }));
|
|
10
12
|
const GRAMMAR_URI = 'bpc://grammar';
|
|
13
|
+
/** Public docs page URL for a docs group + slug, or undefined when docs base is unset. */
|
|
14
|
+
export function publicDocUrl(group, slug) {
|
|
15
|
+
const base = getDocsBaseUrl();
|
|
16
|
+
if (!base) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return buildDocUrl(group, slug, base);
|
|
20
|
+
}
|
|
11
21
|
export function listAllResources() {
|
|
12
22
|
const groups = Object.keys(GROUP_TO_URI);
|
|
13
|
-
const docResources = groups.flatMap(group => listDocs(group).map(entry =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
const docResources = groups.flatMap(group => listDocs(group).map((entry) => {
|
|
24
|
+
const resource = {
|
|
25
|
+
uri: `${GROUP_TO_URI[group]}${entry.slug}`,
|
|
26
|
+
name: entry.title,
|
|
27
|
+
description: entry.blurb,
|
|
28
|
+
mimeType: 'text/markdown',
|
|
29
|
+
};
|
|
30
|
+
const docsUrl = publicDocUrl(group, entry.slug);
|
|
31
|
+
if (docsUrl) {
|
|
32
|
+
resource.docsUrl = docsUrl;
|
|
33
|
+
}
|
|
34
|
+
return resource;
|
|
35
|
+
}));
|
|
19
36
|
return [
|
|
20
37
|
{
|
|
21
38
|
uri: GRAMMAR_URI,
|
|
@@ -1,5 +1,30 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { listAllResources, readResource } from './docsReader';
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { listAllResources, publicDocUrl, readResource } from './docsReader';
|
|
3
|
+
afterEach(() => {
|
|
4
|
+
delete process.env.BLUEPRINT_CHART_DOCS_URL;
|
|
5
|
+
});
|
|
6
|
+
describe('publicDocUrl', () => {
|
|
7
|
+
it('returns undefined when docs base is unset', () => {
|
|
8
|
+
expect(publicDocUrl('charts', 'bar-vertical')).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
it('builds a public docs URL when configured', () => {
|
|
11
|
+
process.env.BLUEPRINT_CHART_DOCS_URL = 'https://docs.blueprintchart.com';
|
|
12
|
+
expect(publicDocUrl('charts', 'bar-vertical')).toBe('https://docs.blueprintchart.com/charts/bar-vertical');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe('listAllResources docsUrl', () => {
|
|
16
|
+
it('omits docsUrl when docs base is unset', () => {
|
|
17
|
+
const grammar = listAllResources().find(r => r.uri === 'bpc://grammar');
|
|
18
|
+
const charts = listAllResources().find(r => r.uri.startsWith('bpc://chart-types/'));
|
|
19
|
+
expect(grammar?.docsUrl).toBeUndefined();
|
|
20
|
+
expect(charts?.docsUrl).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
it('includes docsUrl on doc resources when configured', () => {
|
|
23
|
+
process.env.BLUEPRINT_CHART_DOCS_URL = 'https://docs.blueprintchart.com';
|
|
24
|
+
const charts = listAllResources().find(r => r.uri.startsWith('bpc://chart-types/'));
|
|
25
|
+
expect(charts?.docsUrl).toMatch(/^https:\/\/docs\.blueprintchart\.com\/charts\//);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
3
28
|
describe('docsReader', () => {
|
|
4
29
|
it('lists handbook entries', () => {
|
|
5
30
|
const list = listAllResources();
|
package/dist/server.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { zodToJsonSchema } from './lib/zodToJsonSchema';
|
|
3
|
+
import type { ToolResult } from './errors';
|
|
4
|
+
interface ToolDef {
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: Parameters<typeof zodToJsonSchema>[0];
|
|
7
|
+
handler: (args: unknown) => ToolResult<unknown> | Promise<ToolResult<unknown>>;
|
|
8
|
+
}
|
|
9
|
+
export declare const TOOLS: Record<string, ToolDef>;
|
|
2
10
|
export declare function createServer(): Server;
|
|
11
|
+
export {};
|
package/dist/server.js
CHANGED
|
@@ -1,20 +1,47 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
3
6
|
import { validateDsl, ValidateInputSchema } from './tools/validate';
|
|
4
7
|
import { inspectDsl, InspectInputSchema } from './tools/inspect';
|
|
5
8
|
import { recommendChartType, RecommendInputSchema } from './tools/recommend';
|
|
6
9
|
import { renderTool, RenderInputSchema } from './tools/render';
|
|
10
|
+
import { listChartTypes, ListChartTypesInputSchema } from './tools/listChartTypes';
|
|
11
|
+
import { describeChartType, DescribeChartTypeInputSchema } from './tools/describeChartType';
|
|
12
|
+
import { getExample, GetExampleInputSchema } from './tools/getExample';
|
|
13
|
+
import { getGrammar, GetGrammarInputSchema } from './tools/getGrammar';
|
|
14
|
+
import { exportChart, ExportChartInputSchema } from './tools/exportChart';
|
|
15
|
+
import { searchExamples, SearchExamplesInputSchema } from './tools/searchExamples';
|
|
16
|
+
import { listPalettesTool, ListPalettesInputSchema } from './tools/listPalettes';
|
|
7
17
|
import { listResources, readResource } from './resources/index';
|
|
8
18
|
import { authorChartPrompt } from './prompts/authorChart';
|
|
9
19
|
import { zodToJsonSchema } from './lib/zodToJsonSchema';
|
|
10
|
-
|
|
20
|
+
// Server metadata is embedded into every `initialize` response's `serverInfo`.
|
|
21
|
+
// Icons are advertised only when `MCP_PUBLIC_URL` is set (e.g. on a hosted
|
|
22
|
+
// deployment) — they must be absolute URLs because some MCP clients reject
|
|
23
|
+
// `data:` URIs. For stdio / local use the env var is typically unset and the
|
|
24
|
+
// `icons` field is omitted entirely.
|
|
25
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
27
|
+
function buildServerIcons() {
|
|
28
|
+
const baseUrl = process.env.MCP_PUBLIC_URL?.trim().replace(/\/+$/, '');
|
|
29
|
+
if (!baseUrl) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return [
|
|
33
|
+
{ src: `${baseUrl}/favicon.svg`, mimeType: 'image/svg+xml' },
|
|
34
|
+
{ src: `${baseUrl}/favicon.png`, mimeType: 'image/png', sizes: ['256x256'] },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
export const TOOLS = {
|
|
11
38
|
validate_dsl: {
|
|
12
|
-
description: 'Parse a .bpc source
|
|
39
|
+
description: 'Parse and semantically validate a .bpc source. Returns { valid, errors[], warnings[] }. Errors include unknown chart types, unknown properties, and empty data blocks with nearest-neighbour suggestions.',
|
|
13
40
|
inputSchema: ValidateInputSchema,
|
|
14
41
|
handler: args => validateDsl(args),
|
|
15
42
|
},
|
|
16
43
|
inspect_dsl: {
|
|
17
|
-
description: 'Parse a .bpc source and return a structured summary: chartType, scenes,
|
|
44
|
+
description: 'Parse a .bpc source and return a structured summary: chartType, scenes, data (rowCount, entryCount, labels, seriesNames, multiSeries), annotation/colorize/highlight/area-fill presence flags, series count.',
|
|
18
45
|
inputSchema: InspectInputSchema,
|
|
19
46
|
handler: args => inspectDsl(args),
|
|
20
47
|
},
|
|
@@ -24,10 +51,45 @@ const TOOLS = {
|
|
|
24
51
|
handler: args => recommendChartType(args),
|
|
25
52
|
},
|
|
26
53
|
render: {
|
|
27
|
-
description: 'Render a .bpc source to SVG (default) or
|
|
54
|
+
description: 'Render a .bpc source to SVG (default), PNG, or HTML. Always returns structured frame metadata (title, description, byline, source, sourceUrl, note). Pass `save: <path>` to write the primary output to disk and omit it from the response — useful when the LLM client cannot display binary payloads inline. Saving requires MCP_ALLOW_FS_WRITE=1.',
|
|
28
55
|
inputSchema: RenderInputSchema,
|
|
29
56
|
handler: args => renderTool(args),
|
|
30
57
|
},
|
|
58
|
+
list_chart_types: {
|
|
59
|
+
description: 'List every chart type the renderer supports, with aliases and one-line summaries. Call this before writing .bpc if unsure which type to use.',
|
|
60
|
+
inputSchema: ListChartTypesInputSchema,
|
|
61
|
+
handler: () => listChartTypes(),
|
|
62
|
+
},
|
|
63
|
+
describe_chart_type: {
|
|
64
|
+
description: 'Return everything an LLM needs to write a .bpc for a given chart type. Input: { chartType: "bar-horizontal" } (or any canonical/alias name from list_chart_types). Returns summary, when-to-use, when-NOT-to-use, full property list with enum choices, data-shape example, and a pointer to a canonical sample.',
|
|
65
|
+
inputSchema: DescribeChartTypeInputSchema,
|
|
66
|
+
handler: args => describeChartType(args),
|
|
67
|
+
},
|
|
68
|
+
get_example: {
|
|
69
|
+
description: 'Return a canonical .bpc example. Pass { name } for a specific sample id, { chartType } for the first sample of that type, or no args for a starter sample.',
|
|
70
|
+
inputSchema: GetExampleInputSchema,
|
|
71
|
+
handler: args => getExample(args),
|
|
72
|
+
},
|
|
73
|
+
search_examples: {
|
|
74
|
+
description: 'Find canonical .bpc examples by topic keywords and/or chart type. Returns ranked pointers { id, title, description, chartType } — call get_example with an id to fetch the full DSL.',
|
|
75
|
+
inputSchema: SearchExamplesInputSchema,
|
|
76
|
+
handler: args => searchExamples(args),
|
|
77
|
+
},
|
|
78
|
+
list_palettes: {
|
|
79
|
+
description: 'List every named colour palette with its label and hex colours, for use in `colorPalette = "<name>"`.',
|
|
80
|
+
inputSchema: ListPalettesInputSchema,
|
|
81
|
+
handler: () => listPalettesTool(),
|
|
82
|
+
},
|
|
83
|
+
get_grammar: {
|
|
84
|
+
description: 'Return the .bpc DSL grammar as markdown. Pass { section: "chart" | "data" | "properties" | "scenes" | "annotations" } for a focused subset, or no args for the full grammar.',
|
|
85
|
+
inputSchema: GetGrammarInputSchema,
|
|
86
|
+
handler: args => getGrammar(args),
|
|
87
|
+
},
|
|
88
|
+
export_chart: {
|
|
89
|
+
description: 'Turn a validated .bpc source into shareable editor URLs. Validates the source through the same parse/semantic/render pipeline as `render`; on success returns { copyUrl, embedUrl, frame }. copyUrl opens an editable copy in the editor ("anyone can open this to view and copy"); embedUrl is a read-only render target suitable as an iframe src. Returns E_CONFIG if the server has no editor base URL configured (BLUEPRINT_CHART_EDITOR_URL).',
|
|
90
|
+
inputSchema: ExportChartInputSchema,
|
|
91
|
+
handler: args => exportChart(args),
|
|
92
|
+
},
|
|
31
93
|
};
|
|
32
94
|
function formatToolResult(result) {
|
|
33
95
|
if (result.ok) {
|
|
@@ -39,7 +101,15 @@ function formatToolResult(result) {
|
|
|
39
101
|
};
|
|
40
102
|
}
|
|
41
103
|
export function createServer() {
|
|
42
|
-
const
|
|
104
|
+
const icons = buildServerIcons();
|
|
105
|
+
const server = new Server({
|
|
106
|
+
name: '@blueprint-chart/mcp',
|
|
107
|
+
title: 'Blueprint Chart',
|
|
108
|
+
version: PKG.version,
|
|
109
|
+
description: PKG.description,
|
|
110
|
+
websiteUrl: PKG.homepage,
|
|
111
|
+
...(icons && { icons }),
|
|
112
|
+
}, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
43
113
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
44
114
|
tools: Object.entries(TOOLS).map(([name, def]) => ({
|
|
45
115
|
name,
|
package/dist/server.test.js
CHANGED
|
@@ -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,60 @@ async function connectInMemory() {
|
|
|
11
11
|
await client.connect(clientT);
|
|
12
12
|
return { client, server };
|
|
13
13
|
}
|
|
14
|
+
describe('TOOLS registry', () => {
|
|
15
|
+
it('contains the eleven 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
|
+
'list_palettes',
|
|
24
|
+
'recommend_chart_type',
|
|
25
|
+
'render',
|
|
26
|
+
'search_examples',
|
|
27
|
+
'validate_dsl',
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
14
31
|
describe('server', () => {
|
|
15
|
-
it('lists
|
|
32
|
+
it('lists 11 tools', async () => {
|
|
16
33
|
const { client } = await connectInMemory();
|
|
17
34
|
const r = await client.listTools();
|
|
18
35
|
const names = r.tools.map(t => t.name).sort();
|
|
19
|
-
expect(names).toEqual([
|
|
36
|
+
expect(names).toEqual([
|
|
37
|
+
'describe_chart_type',
|
|
38
|
+
'export_chart',
|
|
39
|
+
'get_example',
|
|
40
|
+
'get_grammar',
|
|
41
|
+
'inspect_dsl',
|
|
42
|
+
'list_chart_types',
|
|
43
|
+
'list_palettes',
|
|
44
|
+
'recommend_chart_type',
|
|
45
|
+
'render',
|
|
46
|
+
'search_examples',
|
|
47
|
+
'validate_dsl',
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
it('publishes JSON Schemas with concrete properties (not a permissive stub)', async () => {
|
|
51
|
+
const { client } = await connectInMemory();
|
|
52
|
+
const r = await client.listTools();
|
|
53
|
+
for (const tool of r.tools) {
|
|
54
|
+
const schema = tool.inputSchema;
|
|
55
|
+
expect(schema.type, `${tool.name}: schema.type`).toBe('object');
|
|
56
|
+
// Discovery tools like list_chart_types take no params — their `properties`
|
|
57
|
+
// object exists but is empty. Every other tool has at least one property.
|
|
58
|
+
expect(schema.properties, `${tool.name}: schema.properties`).toBeDefined();
|
|
59
|
+
}
|
|
60
|
+
const validate = r.tools.find(t => t.name === 'validate_dsl');
|
|
61
|
+
const validateSchema = validate.inputSchema;
|
|
62
|
+
expect(validateSchema.properties.source).toBeDefined();
|
|
63
|
+
expect(validateSchema.required).toEqual(['source']);
|
|
64
|
+
const render = r.tools.find(t => t.name === 'render');
|
|
65
|
+
const renderSchema = render.inputSchema;
|
|
66
|
+
expect(renderSchema.properties.format).toBeDefined();
|
|
67
|
+
expect(renderSchema.properties.save).toBeDefined();
|
|
20
68
|
});
|
|
21
69
|
it('calls validate_dsl successfully for a sample', async () => {
|
|
22
70
|
const { client } = await connectInMemory();
|
|
@@ -58,6 +106,32 @@ describe('server', () => {
|
|
|
58
106
|
const first = r.contents[0];
|
|
59
107
|
expect(first.text).toMatch(/chart\s+\w/);
|
|
60
108
|
});
|
|
109
|
+
describe('serverInfo metadata', () => {
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
vi.unstubAllEnvs();
|
|
112
|
+
});
|
|
113
|
+
it('always advertises title + dynamic version', async () => {
|
|
114
|
+
vi.stubEnv('MCP_PUBLIC_URL', '');
|
|
115
|
+
const { client } = await connectInMemory();
|
|
116
|
+
const info = client.getServerVersion();
|
|
117
|
+
expect(info?.title).toBe('Blueprint Chart');
|
|
118
|
+
expect(info?.version).toMatch(/^\d+\.\d+\.\d+/);
|
|
119
|
+
});
|
|
120
|
+
it('omits icons when MCP_PUBLIC_URL is unset (stdio / local use)', async () => {
|
|
121
|
+
vi.stubEnv('MCP_PUBLIC_URL', '');
|
|
122
|
+
const { client } = await connectInMemory();
|
|
123
|
+
expect(client.getServerVersion()?.icons).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
it('advertises absolute-URL icons when MCP_PUBLIC_URL is set', async () => {
|
|
126
|
+
vi.stubEnv('MCP_PUBLIC_URL', 'https://mcp.example.com/');
|
|
127
|
+
const { client } = await connectInMemory();
|
|
128
|
+
const icons = client.getServerVersion()?.icons;
|
|
129
|
+
expect(icons).toEqual([
|
|
130
|
+
{ src: 'https://mcp.example.com/favicon.svg', mimeType: 'image/svg+xml' },
|
|
131
|
+
{ src: 'https://mcp.example.com/favicon.png', mimeType: 'image/png', sizes: ['256x256'] },
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
61
135
|
it('exposes author_chart prompt', async () => {
|
|
62
136
|
const { client } = await connectInMemory();
|
|
63
137
|
const prompts = await client.listPrompts();
|
|
@@ -65,4 +139,31 @@ describe('server', () => {
|
|
|
65
139
|
const got = await client.getPrompt({ name: 'author_chart' });
|
|
66
140
|
expect(got.messages.length).toBeGreaterThan(0);
|
|
67
141
|
});
|
|
142
|
+
describe('export_chart', () => {
|
|
143
|
+
const VALID_SOURCE = 'chart bar-vertical {\n data {\n "A" = 1\n }\n}\n';
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
delete process.env.BLUEPRINT_CHART_EDITOR_URL;
|
|
146
|
+
});
|
|
147
|
+
it('appears in tools/list with an object-type inputSchema', async () => {
|
|
148
|
+
const { client } = await connectInMemory();
|
|
149
|
+
const r = await client.listTools();
|
|
150
|
+
const tool = r.tools.find(t => t.name === 'export_chart');
|
|
151
|
+
expect(tool).toBeDefined();
|
|
152
|
+
expect(tool.inputSchema.type).toBe('object');
|
|
153
|
+
});
|
|
154
|
+
it('returns E_CONFIG when BLUEPRINT_CHART_EDITOR_URL is unset', async () => {
|
|
155
|
+
delete process.env.BLUEPRINT_CHART_EDITOR_URL;
|
|
156
|
+
const { client } = await connectInMemory();
|
|
157
|
+
const res = await client.callTool({ name: 'export_chart', arguments: { source: VALID_SOURCE } });
|
|
158
|
+
expect(res.isError).toBe(true);
|
|
159
|
+
expect(JSON.stringify(res.content)).toMatch(/E_CONFIG/);
|
|
160
|
+
});
|
|
161
|
+
it('returns copyUrl with #/copy?bpc64= when BLUEPRINT_CHART_EDITOR_URL is set', async () => {
|
|
162
|
+
process.env.BLUEPRINT_CHART_EDITOR_URL = 'https://blueprintchart.com';
|
|
163
|
+
const { client } = await connectInMemory();
|
|
164
|
+
const res = await client.callTool({ name: 'export_chart', arguments: { source: VALID_SOURCE } });
|
|
165
|
+
expect(res.isError).toBeFalsy();
|
|
166
|
+
expect(JSON.stringify(res.content)).toMatch(/#\/copy\?bpc64=/);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
68
169
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type ToolResult } from '../errors';
|
|
3
|
+
import { type CapabilityStatus } from '../dsl/capabilityMatrix';
|
|
4
|
+
export declare const DescribeChartTypeInputSchema: z.ZodObject<{
|
|
5
|
+
chartType: z.ZodString;
|
|
6
|
+
}, "strict", z.ZodTypeAny, {
|
|
7
|
+
chartType: string;
|
|
8
|
+
}, {
|
|
9
|
+
chartType: string;
|
|
10
|
+
}>;
|
|
11
|
+
export type DescribeChartTypeInput = z.infer<typeof DescribeChartTypeInputSchema>;
|
|
12
|
+
export interface ChartTypeProperty {
|
|
13
|
+
key: string;
|
|
14
|
+
type: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
choices?: string[];
|
|
17
|
+
default?: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface ChartTypeDirective {
|
|
20
|
+
name: string;
|
|
21
|
+
status: CapabilityStatus;
|
|
22
|
+
description: string;
|
|
23
|
+
note?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ChartTypeDataShape {
|
|
26
|
+
kind: 'single-series' | 'multi-series' | 'unknown';
|
|
27
|
+
example: string;
|
|
28
|
+
}
|
|
29
|
+
export interface DescribeChartTypeOutput {
|
|
30
|
+
name: string;
|
|
31
|
+
aliases: string[];
|
|
32
|
+
summary: string;
|
|
33
|
+
whenToUse: string[];
|
|
34
|
+
whenNotToUse: string[];
|
|
35
|
+
properties: ChartTypeProperty[];
|
|
36
|
+
directives: ChartTypeDirective[];
|
|
37
|
+
dataShape: ChartTypeDataShape;
|
|
38
|
+
exampleSlug?: string;
|
|
39
|
+
docsUrl?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function describeChartType(input: unknown): ToolResult<DescribeChartTypeOutput>;
|