@blueprint-chart/mcp 0.1.0

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/bin/blueprint-chart-mcp.js +15 -0
  4. package/bin/loader.mjs +36 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +123 -0
  7. package/dist/errors.d.ts +28 -0
  8. package/dist/errors.js +16 -0
  9. package/dist/errors.test.d.ts +1 -0
  10. package/dist/errors.test.js +23 -0
  11. package/dist/lib/zodToJsonSchema.d.ts +8 -0
  12. package/dist/lib/zodToJsonSchema.js +9 -0
  13. package/dist/parse.d.ts +5 -0
  14. package/dist/parse.js +25 -0
  15. package/dist/parse.test.d.ts +1 -0
  16. package/dist/parse.test.js +29 -0
  17. package/dist/prompts/authorChart.d.ts +12 -0
  18. package/dist/prompts/authorChart.js +35 -0
  19. package/dist/prompts/authorChart.test.d.ts +1 -0
  20. package/dist/prompts/authorChart.test.js +13 -0
  21. package/dist/render/jsdomEnv.d.ts +12 -0
  22. package/dist/render/jsdomEnv.js +28 -0
  23. package/dist/render/jsdomEnv.test.d.ts +1 -0
  24. package/dist/render/jsdomEnv.test.js +22 -0
  25. package/dist/render/rasterize.d.ts +5 -0
  26. package/dist/render/rasterize.js +14 -0
  27. package/dist/render/rasterize.test.d.ts +1 -0
  28. package/dist/render/rasterize.test.js +24 -0
  29. package/dist/render/renderSceneState.d.ts +21 -0
  30. package/dist/render/renderSceneState.js +71 -0
  31. package/dist/render/renderSceneState.test.d.ts +1 -0
  32. package/dist/render/renderSceneState.test.js +18 -0
  33. package/dist/render/textShim.d.ts +12 -0
  34. package/dist/render/textShim.js +78 -0
  35. package/dist/render/textShim.test.d.ts +1 -0
  36. package/dist/render/textShim.test.js +31 -0
  37. package/dist/resources/docsReader.d.ts +14 -0
  38. package/dist/resources/docsReader.js +50 -0
  39. package/dist/resources/docsReader.test.d.ts +1 -0
  40. package/dist/resources/docsReader.test.js +24 -0
  41. package/dist/resources/index.d.ts +6 -0
  42. package/dist/resources/index.js +11 -0
  43. package/dist/resources/samples.d.ts +13 -0
  44. package/dist/resources/samples.js +21 -0
  45. package/dist/resources/samples.test.d.ts +1 -0
  46. package/dist/resources/samples.test.js +18 -0
  47. package/dist/server.d.ts +2 -0
  48. package/dist/server.js +86 -0
  49. package/dist/server.test.d.ts +1 -0
  50. package/dist/server.test.js +68 -0
  51. package/dist/smoke.test.d.ts +1 -0
  52. package/dist/smoke.test.js +11 -0
  53. package/dist/tools/inspect.d.ts +26 -0
  54. package/dist/tools/inspect.js +37 -0
  55. package/dist/tools/inspect.test.d.ts +1 -0
  56. package/dist/tools/inspect.test.js +29 -0
  57. package/dist/tools/recommend.d.ts +21 -0
  58. package/dist/tools/recommend.js +17 -0
  59. package/dist/tools/recommend.test.d.ts +1 -0
  60. package/dist/tools/recommend.test.js +33 -0
  61. package/dist/tools/render.d.ts +35 -0
  62. package/dist/tools/render.js +63 -0
  63. package/dist/tools/render.test.d.ts +1 -0
  64. package/dist/tools/render.test.js +39 -0
  65. package/dist/tools/validate.d.ts +13 -0
  66. package/dist/tools/validate.js +13 -0
  67. package/dist/tools/validate.test.d.ts +1 -0
  68. package/dist/tools/validate.test.js +27 -0
  69. package/dist/transports/http.d.ts +21 -0
  70. package/dist/transports/http.js +193 -0
  71. package/dist/transports/http.test.d.ts +1 -0
  72. package/dist/transports/http.test.js +85 -0
  73. package/dist/transports/stdio.d.ts +1 -0
  74. package/dist/transports/stdio.js +7 -0
  75. package/package.json +70 -0
@@ -0,0 +1,12 @@
1
+ import { type DOMWindow } from 'jsdom';
2
+ export interface JsdomEnv {
3
+ window: DOMWindow;
4
+ container: HTMLElement;
5
+ serialize: () => string;
6
+ cleanup: () => void;
7
+ }
8
+ export interface JsdomEnvOptions {
9
+ width: number;
10
+ height: number;
11
+ }
12
+ export declare function createJsdomEnv(opts: JsdomEnvOptions): JsdomEnv;
@@ -0,0 +1,28 @@
1
+ import { JSDOM, VirtualConsole } from 'jsdom';
2
+ // @blueprint-chart/lib internally calls `canvas.getContext('2d')` for text
3
+ // measurement before falling back to declared widths. jsdom logs that as a
4
+ // "Not implemented" error to stderr even though the lib handles the null
5
+ // return gracefully. Drop just those messages; forward anything else.
6
+ function createQuietVirtualConsole() {
7
+ const vc = new VirtualConsole();
8
+ vc.on('jsdomError', (err) => {
9
+ if (err.message && err.message.startsWith('Not implemented')) {
10
+ return;
11
+ }
12
+ console.error(err);
13
+ });
14
+ return vc;
15
+ }
16
+ export function createJsdomEnv(opts) {
17
+ const dom = new JSDOM(`<!DOCTYPE html><html><body><div id="root" style="width:${opts.width}px;height:${opts.height}px"></div></body></html>`, { pretendToBeVisual: true, virtualConsole: createQuietVirtualConsole() });
18
+ const container = dom.window.document.getElementById('root');
19
+ return {
20
+ window: dom.window,
21
+ container,
22
+ serialize: () => {
23
+ const svg = container.querySelector('svg');
24
+ return svg ? svg.outerHTML : '';
25
+ },
26
+ cleanup: () => dom.window.close(),
27
+ };
28
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createJsdomEnv } from './jsdomEnv';
3
+ describe('createJsdomEnv', () => {
4
+ it('returns a document with an SVG element appended', () => {
5
+ const env = createJsdomEnv({ width: 600, height: 400 });
6
+ expect(env.container.tagName).toBe('DIV');
7
+ expect(env.container.ownerDocument).toBeDefined();
8
+ expect(env.window.SVGElement).toBeDefined();
9
+ });
10
+ it('exposes a serialize() that returns SVG markup', () => {
11
+ const env = createJsdomEnv({ width: 600, height: 400 });
12
+ const svg = env.window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
13
+ svg.setAttribute('viewBox', '0 0 100 100');
14
+ env.container.appendChild(svg);
15
+ expect(env.serialize()).toMatch(/<svg[^>]*viewBox="0 0 100 100"/);
16
+ });
17
+ it('cleanup() closes the window', () => {
18
+ const env = createJsdomEnv({ width: 100, height: 100 });
19
+ env.cleanup();
20
+ expect(() => env.window.document.createElement('div')).toThrow();
21
+ });
22
+ });
@@ -0,0 +1,5 @@
1
+ export interface RasterizeOptions {
2
+ width?: number;
3
+ height?: number;
4
+ }
5
+ export declare function rasterizeToPng(svg: string, opts?: RasterizeOptions): Promise<Buffer>;
@@ -0,0 +1,14 @@
1
+ import { Resvg } from '@resvg/resvg-js';
2
+ export async function rasterizeToPng(svg, opts = {}) {
3
+ const fitTo = opts.width
4
+ ? { mode: 'width', value: opts.width }
5
+ : opts.height
6
+ ? { mode: 'height', value: opts.height }
7
+ : { mode: 'original' };
8
+ const resvg = new Resvg(svg, {
9
+ fitTo,
10
+ font: { loadSystemFonts: true },
11
+ });
12
+ const image = resvg.render();
13
+ return Buffer.from(image.asPng());
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { rasterizeToPng } from './rasterize';
3
+ const SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="50">
4
+ <rect width="100" height="50" fill="red"/>
5
+ <text x="10" y="30" font-size="14" font-family="sans-serif">Hi</text>
6
+ </svg>`;
7
+ describe('rasterizeToPng', () => {
8
+ it('returns a PNG buffer for a valid SVG', async () => {
9
+ const buf = await rasterizeToPng(SVG);
10
+ expect(buf.length).toBeGreaterThan(100);
11
+ // PNG file signature
12
+ expect(buf[0]).toBe(0x89);
13
+ expect(buf[1]).toBe(0x50);
14
+ expect(buf[2]).toBe(0x4e);
15
+ expect(buf[3]).toBe(0x47);
16
+ });
17
+ it('throws on invalid SVG', async () => {
18
+ await expect(rasterizeToPng('not svg at all')).rejects.toThrow();
19
+ });
20
+ it('honors width override', async () => {
21
+ const buf = await rasterizeToPng(SVG, { width: 200 });
22
+ expect(buf.length).toBeGreaterThan(100);
23
+ });
24
+ });
@@ -0,0 +1,21 @@
1
+ export interface RenderSceneStateOptions {
2
+ sceneIndex?: number;
3
+ width: number;
4
+ height: number;
5
+ theme?: string;
6
+ }
7
+ /**
8
+ * Render a `.bpc` source string to SVG markup in a headless jsdom environment.
9
+ *
10
+ * Composition:
11
+ * - `createJsdomEnv` provisions a window + container + serialize/cleanup hooks.
12
+ * - `installTextShim` patches `window.SVGElement.prototype` so D3 layout
13
+ * primitives (axes, legends, annotations) read realistic text widths from
14
+ * `@napi-rs/canvas` instead of jsdom's zero defaults.
15
+ * - `renderBpc` from `@blueprint-chart/lib` parses the source, resolves the
16
+ * requested scene, and mounts SVG into the container via D3.
17
+ *
18
+ * Throws when `renderBpc` cannot produce SVG output (invalid source, etc.) —
19
+ * callers (the `render` tool) catch and translate to a structured ToolResult.
20
+ */
21
+ export declare function renderSceneState(source: string, opts: RenderSceneStateOptions): string;
@@ -0,0 +1,71 @@
1
+ import { renderBpc } from '@blueprint-chart/lib';
2
+ import { createJsdomEnv } from './jsdomEnv';
3
+ import { installTextShim } from './textShim';
4
+ /**
5
+ * Globals that `@blueprint-chart/lib` (and the D3 code it bundles) reads at
6
+ * call time. Verified against `node_modules/@blueprint-chart/lib/dist/index.js`
7
+ * via:
8
+ *
9
+ * grep -oE '\b(Element|getComputedStyle|requestAnimationFrame|window|document)\b' \
10
+ * node_modules/@blueprint-chart/lib/dist/index.js | sort -u
11
+ *
12
+ * The lib bundle has no top-level uses of these names — they are only invoked
13
+ * inside functions reached from `renderBpc` — so swapping the globals around
14
+ * the call and restoring them in `finally` is sufficient. No module-load
15
+ * `globals.ts` shim is required.
16
+ */
17
+ const FORWARDED_GLOBALS = [
18
+ 'window',
19
+ 'document',
20
+ 'Element',
21
+ 'getComputedStyle',
22
+ 'requestAnimationFrame',
23
+ ];
24
+ /**
25
+ * Render a `.bpc` source string to SVG markup in a headless jsdom environment.
26
+ *
27
+ * Composition:
28
+ * - `createJsdomEnv` provisions a window + container + serialize/cleanup hooks.
29
+ * - `installTextShim` patches `window.SVGElement.prototype` so D3 layout
30
+ * primitives (axes, legends, annotations) read realistic text widths from
31
+ * `@napi-rs/canvas` instead of jsdom's zero defaults.
32
+ * - `renderBpc` from `@blueprint-chart/lib` parses the source, resolves the
33
+ * requested scene, and mounts SVG into the container via D3.
34
+ *
35
+ * Throws when `renderBpc` cannot produce SVG output (invalid source, etc.) —
36
+ * callers (the `render` tool) catch and translate to a structured ToolResult.
37
+ */
38
+ export function renderSceneState(source, opts) {
39
+ const env = createJsdomEnv({ width: opts.width, height: opts.height });
40
+ try {
41
+ installTextShim(env.window);
42
+ const jsdomWindow = env.window;
43
+ const globals = globalThis;
44
+ const prev = {};
45
+ for (const key of FORWARDED_GLOBALS) {
46
+ prev[key] = globals[key];
47
+ globals[key] = jsdomWindow[key];
48
+ }
49
+ try {
50
+ renderBpc(env.container, source, {
51
+ sceneIndex: opts.sceneIndex,
52
+ thumbnail: true,
53
+ transition: false,
54
+ theme: opts.theme,
55
+ });
56
+ }
57
+ finally {
58
+ for (const key of FORWARDED_GLOBALS) {
59
+ globals[key] = prev[key];
60
+ }
61
+ }
62
+ const svg = env.serialize();
63
+ if (!svg) {
64
+ throw new Error('renderBpc produced no SVG output');
65
+ }
66
+ return svg;
67
+ }
68
+ finally {
69
+ env.cleanup();
70
+ }
71
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { samples } from '@blueprint-chart/lib';
3
+ import { renderSceneState } from './renderSceneState';
4
+ describe('renderSceneState', () => {
5
+ it('returns non-empty SVG for the first sample, scene 0', () => {
6
+ const svg = renderSceneState(samples[0].dsl, { sceneIndex: 0, width: 800, height: 500 });
7
+ expect(svg).toMatch(/^<svg/);
8
+ expect(svg).toMatch(/<\/svg>$/);
9
+ expect(svg.length).toBeGreaterThan(500);
10
+ });
11
+ it('clamps sceneIndex when out of bounds', () => {
12
+ const svg = renderSceneState(samples[0].dsl, { sceneIndex: 999, width: 400, height: 300 });
13
+ expect(svg).toMatch(/^<svg/);
14
+ });
15
+ it('throws for unparseable input (callers catch)', () => {
16
+ expect(() => renderSceneState('@@@ nope', { width: 400, height: 300 })).toThrow();
17
+ });
18
+ });
@@ -0,0 +1,12 @@
1
+ import type { DOMWindow } from 'jsdom';
2
+ /**
3
+ * Patch jsdom's SVG element prototypes so D3 layout primitives that rely on
4
+ * text measurement (axes, legends, annotations) return realistic widths.
5
+ *
6
+ * jsdom 26 does not expose `SVGTextContentElement` and does not subclass
7
+ * `<svg:text>` from `SVGGraphicsElement` — text elements are plain
8
+ * `SVGElement` instances. We therefore patch `SVGElement.prototype` so the
9
+ * methods exist on the actual prototype chain, while also patching the
10
+ * spec-correct prototypes if they happen to be available.
11
+ */
12
+ export declare function installTextShim(window: DOMWindow): void;
@@ -0,0 +1,78 @@
1
+ import { createCanvas, GlobalFonts } from '@napi-rs/canvas';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, resolve } from 'node:path';
4
+ // Register a bundled font under "sans-serif" so text-width measurements are
5
+ // deterministic regardless of which system fonts the host machine has. Without
6
+ // this, `ctx.measureText` resolves "sans-serif" through fontconfig and picks
7
+ // whatever it finds (DejaVu Sans on Debian/Docker, Noto Sans on Ubuntu CI,
8
+ // system fonts on macOS) — and each one yields slightly different metrics,
9
+ // breaking golden-render snapshots.
10
+ //
11
+ // Path resolves correctly both pre-build (tsx running src/) and post-build
12
+ // (node running dist/) because the build step copies fonts/ alongside the JS.
13
+ const fontsDir = resolve(dirname(fileURLToPath(import.meta.url)), 'fonts');
14
+ GlobalFonts.registerFromPath(resolve(fontsDir, 'DejaVuSans.ttf'), 'sans-serif');
15
+ GlobalFonts.registerFromPath(resolve(fontsDir, 'DejaVuSans-Bold.ttf'), 'sans-serif');
16
+ const canvas = createCanvas(1, 1);
17
+ const ctx = canvas.getContext('2d');
18
+ function measure(text, fontSize, fontFamily, fontWeight) {
19
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
20
+ return ctx.measureText(text);
21
+ }
22
+ function fontFor(el) {
23
+ const fontSize = parseFloat(el.getAttribute('font-size') ?? '12') || 12;
24
+ const fontFamily = el.getAttribute('font-family') ?? 'sans-serif';
25
+ const fontWeight = el.getAttribute('font-weight') ?? 'normal';
26
+ return { fontSize, fontFamily, fontWeight };
27
+ }
28
+ /**
29
+ * Patch jsdom's SVG element prototypes so D3 layout primitives that rely on
30
+ * text measurement (axes, legends, annotations) return realistic widths.
31
+ *
32
+ * jsdom 26 does not expose `SVGTextContentElement` and does not subclass
33
+ * `<svg:text>` from `SVGGraphicsElement` — text elements are plain
34
+ * `SVGElement` instances. We therefore patch `SVGElement.prototype` so the
35
+ * methods exist on the actual prototype chain, while also patching the
36
+ * spec-correct prototypes if they happen to be available.
37
+ */
38
+ export function installTextShim(window) {
39
+ const svgElementProto = window.SVGElement?.prototype;
40
+ const computedTextLength = function () {
41
+ const { fontSize, fontFamily, fontWeight } = fontFor(this);
42
+ return measure(this.textContent ?? '', fontSize, fontFamily, fontWeight).width;
43
+ };
44
+ const subStringLength = function (offset, count) {
45
+ const txt = (this.textContent ?? '').substring(offset, offset + count);
46
+ const { fontSize, fontFamily, fontWeight } = fontFor(this);
47
+ return measure(txt, fontSize, fontFamily, fontWeight).width;
48
+ };
49
+ const getBBox = function () {
50
+ if (this.tagName === 'text') {
51
+ const { fontSize, fontFamily, fontWeight } = fontFor(this);
52
+ const m = measure(this.textContent ?? '', fontSize, fontFamily, fontWeight);
53
+ return { x: 0, y: -fontSize, width: m.width, height: fontSize };
54
+ }
55
+ // For non-text elements, fall back to declared attrs.
56
+ const x = parseFloat(this.getAttribute('x') ?? '0') || 0;
57
+ const y = parseFloat(this.getAttribute('y') ?? '0') || 0;
58
+ const width = parseFloat(this.getAttribute('width') ?? '0') || 0;
59
+ const height = parseFloat(this.getAttribute('height') ?? '0') || 0;
60
+ return { x, y, width, height };
61
+ };
62
+ if (svgElementProto) {
63
+ svgElementProto.getComputedTextLength = computedTextLength;
64
+ svgElementProto.getSubStringLength = subStringLength;
65
+ svgElementProto.getBBox = getBBox;
66
+ }
67
+ // Spec-correct prototypes — patch when jsdom (or a future version) exposes them.
68
+ const textContentProto = window
69
+ .SVGTextContentElement?.prototype;
70
+ if (textContentProto) {
71
+ textContentProto.getComputedTextLength = computedTextLength;
72
+ textContentProto.getSubStringLength = subStringLength;
73
+ }
74
+ const graphicsProto = window.SVGGraphicsElement?.prototype;
75
+ if (graphicsProto) {
76
+ graphicsProto.getBBox = getBBox;
77
+ }
78
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createJsdomEnv } from './jsdomEnv';
3
+ import { installTextShim } from './textShim';
4
+ describe('textShim', () => {
5
+ it('makes getComputedTextLength return a non-zero value for non-empty text', () => {
6
+ const env = createJsdomEnv({ width: 400, height: 100 });
7
+ installTextShim(env.window);
8
+ const svg = env.window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
9
+ const text = env.window.document.createElementNS('http://www.w3.org/2000/svg', 'text');
10
+ text.textContent = 'Hello, world';
11
+ text.setAttribute('font-size', '14');
12
+ text.setAttribute('font-family', 'sans-serif');
13
+ svg.appendChild(text);
14
+ env.container.appendChild(svg);
15
+ const length = text.getComputedTextLength();
16
+ expect(length).toBeGreaterThan(0);
17
+ });
18
+ it('getBBox returns width matching getComputedTextLength', () => {
19
+ const env = createJsdomEnv({ width: 400, height: 100 });
20
+ installTextShim(env.window);
21
+ const svg = env.window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
22
+ const text = env.window.document.createElementNS('http://www.w3.org/2000/svg', 'text');
23
+ text.textContent = 'Hi';
24
+ text.setAttribute('font-size', '14');
25
+ svg.appendChild(text);
26
+ env.container.appendChild(svg);
27
+ const bbox = text.getBBox();
28
+ const length = text.getComputedTextLength();
29
+ expect(bbox.width).toBeCloseTo(length, 0);
30
+ });
31
+ });
@@ -0,0 +1,14 @@
1
+ import { type DocEntry } from '@blueprint-chart/docs';
2
+ export interface UriResource {
3
+ uri: string;
4
+ name: string;
5
+ description?: string;
6
+ mimeType: string;
7
+ }
8
+ export declare function listAllResources(): UriResource[];
9
+ export declare function readResource(uri: string): {
10
+ uri: string;
11
+ mimeType: string;
12
+ text: string;
13
+ };
14
+ export declare function resourceEntries(): DocEntry[];
@@ -0,0 +1,50 @@
1
+ import { getDoc, listDocs } from '@blueprint-chart/docs';
2
+ const GROUP_TO_URI = {
3
+ 'handbook': 'bpc://handbook/',
4
+ 'guide': 'bpc://guide/',
5
+ 'charts': 'bpc://chart-types/',
6
+ 'reference/dsl': 'bpc://reference/dsl/',
7
+ 'reference/api': 'bpc://reference/api/',
8
+ };
9
+ const URI_TO_GROUP = Object.entries(GROUP_TO_URI).map(([group, prefix]) => ({ prefix, group }));
10
+ const GRAMMAR_URI = 'bpc://grammar';
11
+ export function listAllResources() {
12
+ const groups = Object.keys(GROUP_TO_URI);
13
+ const docResources = groups.flatMap(group => listDocs(group).map(entry => ({
14
+ uri: `${GROUP_TO_URI[group]}${entry.slug}`,
15
+ name: entry.title,
16
+ description: entry.blurb,
17
+ mimeType: 'text/markdown',
18
+ })));
19
+ return [
20
+ {
21
+ uri: GRAMMAR_URI,
22
+ name: 'BPC Grammar (aggregate)',
23
+ description: 'Full DSL grammar reference, concatenated from reference/dsl pages.',
24
+ mimeType: 'text/markdown',
25
+ },
26
+ ...docResources,
27
+ ];
28
+ }
29
+ export function readResource(uri) {
30
+ if (uri === GRAMMAR_URI) {
31
+ const pages = listDocs('reference/dsl');
32
+ const sections = pages.map((entry) => {
33
+ const { content } = getDoc('reference/dsl', entry.slug);
34
+ return `# ${entry.title}\n\n${content}`;
35
+ });
36
+ return { uri, mimeType: 'text/markdown', text: sections.join('\n\n---\n\n') };
37
+ }
38
+ for (const { prefix, group } of URI_TO_GROUP) {
39
+ if (uri.startsWith(prefix)) {
40
+ const slug = uri.slice(prefix.length);
41
+ const { content } = getDoc(group, slug);
42
+ return { uri, mimeType: 'text/markdown', text: content };
43
+ }
44
+ }
45
+ throw new Error(`Unknown resource URI: ${uri}`);
46
+ }
47
+ export function resourceEntries() {
48
+ const groups = Object.keys(GROUP_TO_URI);
49
+ return groups.flatMap(g => listDocs(g));
50
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { listAllResources, readResource } from './docsReader';
3
+ describe('docsReader', () => {
4
+ it('lists handbook entries', () => {
5
+ const list = listAllResources();
6
+ expect(list.some(r => r.uri.startsWith('bpc://handbook/'))).toBe(true);
7
+ });
8
+ it('reads a known handbook page', () => {
9
+ const list = listAllResources();
10
+ const first = list.find(r => r.uri.startsWith('bpc://handbook/'));
11
+ const doc = readResource(first.uri);
12
+ expect(doc.mimeType).toBe('text/markdown');
13
+ expect(doc.text.length).toBeGreaterThan(100);
14
+ });
15
+ it('throws on unknown URI', () => {
16
+ expect(() => readResource('bpc://handbook/does-not-exist')).toThrow();
17
+ });
18
+ it('exposes bpc://grammar as an aggregate of reference/dsl pages', () => {
19
+ const list = listAllResources();
20
+ expect(list.some(r => r.uri === 'bpc://grammar')).toBe(true);
21
+ const doc = readResource('bpc://grammar');
22
+ expect(doc.text.length).toBeGreaterThan(500);
23
+ });
24
+ });
@@ -0,0 +1,6 @@
1
+ export declare function listResources(): import("./docsReader").UriResource[];
2
+ export declare function readResource(uri: string): {
3
+ uri: string;
4
+ mimeType: string;
5
+ text: string;
6
+ };
@@ -0,0 +1,11 @@
1
+ import { listAllResources as listDocsResources, readResource as readDocsResource } from './docsReader';
2
+ import { listSampleResources, readSampleResource } from './samples';
3
+ export function listResources() {
4
+ return [...listDocsResources(), ...listSampleResources()];
5
+ }
6
+ export function readResource(uri) {
7
+ if (uri.startsWith('bpc://samples/')) {
8
+ return readSampleResource(uri);
9
+ }
10
+ return readDocsResource(uri);
11
+ }
@@ -0,0 +1,13 @@
1
+ interface SampleResource {
2
+ uri: string;
3
+ name: string;
4
+ description?: string;
5
+ mimeType: string;
6
+ }
7
+ export declare function listSampleResources(): SampleResource[];
8
+ export declare function readSampleResource(uri: string): {
9
+ uri: string;
10
+ mimeType: string;
11
+ text: string;
12
+ };
13
+ export {};
@@ -0,0 +1,21 @@
1
+ import { samples } from '@blueprint-chart/lib';
2
+ const PREFIX = 'bpc://samples/';
3
+ export function listSampleResources() {
4
+ return samples.map(s => ({
5
+ uri: `${PREFIX}${s.id}`,
6
+ name: s.title,
7
+ description: s.description,
8
+ mimeType: 'text/plain',
9
+ }));
10
+ }
11
+ export function readSampleResource(uri) {
12
+ if (!uri.startsWith(PREFIX)) {
13
+ throw new Error(`Not a sample URI: ${uri}`);
14
+ }
15
+ const id = uri.slice(PREFIX.length);
16
+ const sample = samples.find(s => s.id === id);
17
+ if (!sample) {
18
+ throw new Error(`Unknown sample: ${id}`);
19
+ }
20
+ return { uri, mimeType: 'text/plain', text: sample.dsl };
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { samples } from '@blueprint-chart/lib';
3
+ import { listSampleResources, readSampleResource } from './samples';
4
+ describe('samples resources', () => {
5
+ it('lists one resource per lib sample', () => {
6
+ const list = listSampleResources();
7
+ expect(list.length).toBe(samples.length);
8
+ });
9
+ it('reads a sample as text/plain bpc', () => {
10
+ const first = listSampleResources()[0];
11
+ const doc = readSampleResource(first.uri);
12
+ expect(doc.mimeType).toBe('text/plain');
13
+ expect(doc.text.length).toBeGreaterThan(20);
14
+ });
15
+ it('throws on unknown sample id', () => {
16
+ expect(() => readSampleResource('bpc://samples/does-not-exist')).toThrow();
17
+ });
18
+ });
@@ -0,0 +1,2 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ export declare function createServer(): Server;
package/dist/server.js ADDED
@@ -0,0 +1,86 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
3
+ import { validateDsl, ValidateInputSchema } from './tools/validate';
4
+ import { inspectDsl, InspectInputSchema } from './tools/inspect';
5
+ import { recommendChartType, RecommendInputSchema } from './tools/recommend';
6
+ import { renderTool, RenderInputSchema } from './tools/render';
7
+ import { listResources, readResource } from './resources/index';
8
+ import { authorChartPrompt } from './prompts/authorChart';
9
+ import { zodToJsonSchema } from './lib/zodToJsonSchema';
10
+ const TOOLS = {
11
+ validate_dsl: {
12
+ description: 'Parse a .bpc source string. Return ok or precise parse errors with line/column.',
13
+ inputSchema: ValidateInputSchema,
14
+ handler: args => validateDsl(args),
15
+ },
16
+ inspect_dsl: {
17
+ description: 'Parse a .bpc source and return a structured summary: chartType, scenes, series count, annotations, etc.',
18
+ inputSchema: InspectInputSchema,
19
+ handler: args => inspectDsl(args),
20
+ },
21
+ recommend_chart_type: {
22
+ description: 'Given an array of column types and a row count, return ranked chart-type recommendations.',
23
+ inputSchema: RecommendInputSchema,
24
+ handler: args => recommendChartType(args),
25
+ },
26
+ render: {
27
+ description: 'Render a .bpc source to SVG (default) or PNG. Accepts scene index, width, height.',
28
+ inputSchema: RenderInputSchema,
29
+ handler: args => renderTool(args),
30
+ },
31
+ };
32
+ function formatToolResult(result) {
33
+ if (result.ok) {
34
+ return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
35
+ }
36
+ return {
37
+ isError: true,
38
+ content: [{ type: 'text', text: JSON.stringify({ code: result.code, errors: result.errors }, null, 2) }],
39
+ };
40
+ }
41
+ export function createServer() {
42
+ const server = new Server({ name: '@blueprint-chart/mcp', version: '0.1.0' }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
43
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
44
+ tools: Object.entries(TOOLS).map(([name, def]) => ({
45
+ name,
46
+ description: def.description,
47
+ inputSchema: zodToJsonSchema(def.inputSchema),
48
+ })),
49
+ }));
50
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
51
+ const tool = TOOLS[req.params.name];
52
+ if (!tool) {
53
+ return {
54
+ isError: true,
55
+ content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
56
+ };
57
+ }
58
+ const result = await tool.handler(req.params.arguments ?? {});
59
+ return formatToolResult(result);
60
+ });
61
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
62
+ resources: listResources(),
63
+ }));
64
+ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
65
+ const { uri } = req.params;
66
+ const doc = readResource(uri);
67
+ return { contents: [{ uri: doc.uri, mimeType: doc.mimeType, text: doc.text }] };
68
+ });
69
+ const PROMPTS = {
70
+ author_chart: authorChartPrompt(),
71
+ };
72
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
73
+ prompts: Object.entries(PROMPTS).map(([name, p]) => ({
74
+ name,
75
+ description: p.description,
76
+ })),
77
+ }));
78
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
79
+ const prompt = PROMPTS[req.params.name];
80
+ if (!prompt) {
81
+ throw new Error(`Unknown prompt: ${req.params.name}`);
82
+ }
83
+ return prompt;
84
+ });
85
+ return server;
86
+ }
@@ -0,0 +1 @@
1
+ export {};