@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
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { samples } from '@blueprint-chart/lib';
3
+ import { diagnoseRender } from './diagnose';
4
+ describe('diagnoseRender', () => {
5
+ it('returns ok for every shipped sample', () => {
6
+ for (const s of samples) {
7
+ const r = diagnoseRender(s.dsl);
8
+ expect(r.ok, `sample ${s.id}: ${JSON.stringify(r)}`).toBe(true);
9
+ }
10
+ });
11
+ it('reports E_UNKNOWN_CHART_TYPE for chart bar', () => {
12
+ const r = diagnoseRender('chart bar { data { "E" = 1 } }');
13
+ expect(r.ok).toBe(false);
14
+ if (!r.ok) {
15
+ const issue = r.diagnostics.find(d => d.code === 'E_UNKNOWN_CHART_TYPE');
16
+ expect(issue).toBeDefined();
17
+ expect(issue.suggestion).toMatch(/^bar-/);
18
+ }
19
+ });
20
+ it('reports E_NO_DATA when data is empty', () => {
21
+ const r = diagnoseRender('chart bar-vertical {}');
22
+ expect(r.ok).toBe(false);
23
+ if (!r.ok) {
24
+ expect(r.diagnostics.some(d => d.code === 'E_NO_DATA')).toBe(true);
25
+ }
26
+ });
27
+ it('reports E_UNKNOWN_SCENE_INDEX when sceneIndex is out of range', () => {
28
+ const r = diagnoseRender('chart bar-vertical { data { "E" = 1 } }', { sceneIndex: 5 });
29
+ expect(r.ok).toBe(false);
30
+ if (!r.ok) {
31
+ const issue = r.diagnostics.find(d => d.code === 'E_UNKNOWN_SCENE_INDEX');
32
+ expect(issue).toBeDefined();
33
+ expect(issue.context?.availableSceneCount).toBe(0);
34
+ }
35
+ });
36
+ it('reports E_UNRESOLVED_COLORIZE when colorize target is not a label', () => {
37
+ const r = diagnoseRender('chart bar-vertical { data { "E" = 1 } colorize "Z" { color = "red" } }');
38
+ expect(r.ok).toBe(false);
39
+ if (!r.ok) {
40
+ const issue = r.diagnostics.find(d => d.code === 'E_UNRESOLVED_COLORIZE');
41
+ expect(issue).toBeDefined();
42
+ expect(issue.context?.target).toBe('Z');
43
+ expect(issue.context?.availableLabels).toEqual(['E']);
44
+ }
45
+ });
46
+ it('reports E_PARSE on malformed source', () => {
47
+ const r = diagnoseRender('@@@ not valid');
48
+ expect(r.ok).toBe(false);
49
+ if (!r.ok) {
50
+ expect(r.diagnostics[0].code).toBe('E_PARSE');
51
+ }
52
+ });
53
+ });
@@ -0,0 +1,10 @@
1
+ import type { ChartNode } from '@blueprint-chart/lib';
2
+ export interface FrameMetadata {
3
+ title?: string;
4
+ description?: string;
5
+ byline?: string;
6
+ source?: string;
7
+ sourceUrl?: string;
8
+ note?: string;
9
+ }
10
+ export declare function extractFrameMetadata(ast: ChartNode): FrameMetadata;
@@ -0,0 +1,10 @@
1
+ const FRAME_KEYS = ['title', 'description', 'byline', 'source', 'sourceUrl', 'note'];
2
+ export function extractFrameMetadata(ast) {
3
+ const frame = {};
4
+ for (const prop of ast.properties ?? []) {
5
+ if (FRAME_KEYS.includes(prop.key)) {
6
+ frame[prop.key] = String(prop.value).replace(/^"|"$/g, '');
7
+ }
8
+ }
9
+ return frame;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parse } from '@blueprint-chart/lib';
3
+ import { extractFrameMetadata } from './frame';
4
+ describe('extractFrameMetadata', () => {
5
+ it('pulls frame keys off the chart AST and strips surrounding quotes', () => {
6
+ const ast = parse('chart bar-vertical {\n title = "Hi"\n source = "ICIJ"\n data {\n "A" = 1\n }\n}\n');
7
+ const frame = extractFrameMetadata(ast);
8
+ expect(frame.title).toBe('Hi');
9
+ expect(frame.source).toBe('ICIJ');
10
+ expect(frame.byline).toBeUndefined();
11
+ });
12
+ });
@@ -2,7 +2,8 @@ import { type DOMWindow } from 'jsdom';
2
2
  export interface JsdomEnv {
3
3
  window: DOMWindow;
4
4
  container: HTMLElement;
5
- serialize: () => string;
5
+ serializeSvg: () => string;
6
+ serializeFrame: () => string | undefined;
6
7
  cleanup: () => void;
7
8
  }
8
9
  export interface JsdomEnvOptions {
@@ -19,10 +19,23 @@ export function createJsdomEnv(opts) {
19
19
  return {
20
20
  window: dom.window,
21
21
  container,
22
- serialize: () => {
22
+ serializeSvg: () => {
23
+ // When thumbnail=false the lib wraps the SVG in a div.bc-frame. The chart
24
+ // SVG lives inside div.bc-frame-body. Extract it precisely so we don't
25
+ // accidentally grab the small logo SVG in the footer.
26
+ const frameBody = container.querySelector('.bc-frame-body');
27
+ if (frameBody) {
28
+ const svg = frameBody.querySelector('svg');
29
+ return svg ? svg.outerHTML : '';
30
+ }
31
+ // Fallback for bare renders (no frame): grab the direct SVG.
23
32
  const svg = container.querySelector('svg');
24
33
  return svg ? svg.outerHTML : '';
25
34
  },
35
+ serializeFrame: () => {
36
+ const frame = container.querySelector('.bc-frame');
37
+ return frame ? frame.outerHTML : undefined;
38
+ },
26
39
  cleanup: () => dom.window.close(),
27
40
  };
28
41
  }
@@ -7,12 +7,46 @@ describe('createJsdomEnv', () => {
7
7
  expect(env.container.ownerDocument).toBeDefined();
8
8
  expect(env.window.SVGElement).toBeDefined();
9
9
  });
10
- it('exposes a serialize() that returns SVG markup', () => {
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
- expect(env.serialize()).toMatch(/<svg[^>]*viewBox="0 0 100 100"/);
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): string;
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: true,
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.serialize();
62
+ const svg = env.serializeSvg();
63
63
  if (!svg) {
64
64
  throw new Error('renderBpc produced no SVG output');
65
65
  }
66
- return svg;
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 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);
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 svg = renderSceneState(samples[0].dsl, { sceneIndex: 999, width: 400, height: 300 });
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
- uri: `${GROUP_TO_URI[group]}${entry.slug}`,
15
- name: entry.title,
16
- description: entry.blurb,
17
- mimeType: 'text/markdown',
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();
@@ -1,4 +1,4 @@
1
- export declare function listResources(): import("./docsReader").UriResource[];
1
+ export declare function listResources(): import("./samples").SampleResource[];
2
2
  export declare function readResource(uri: string): {
3
3
  uri: string;
4
4
  mimeType: string;
@@ -1,4 +1,4 @@
1
- interface SampleResource {
1
+ export interface SampleResource {
2
2
  uri: string;
3
3
  name: string;
4
4
  description?: string;
@@ -10,4 +10,3 @@ export declare function readSampleResource(uri: string): {
10
10
  mimeType: string;
11
11
  text: string;
12
12
  };
13
- export {};
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,45 @@
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';
7
15
  import { listResources, readResource } from './resources/index';
8
16
  import { authorChartPrompt } from './prompts/authorChart';
9
17
  import { zodToJsonSchema } from './lib/zodToJsonSchema';
10
- const TOOLS = {
18
+ // Server metadata is embedded into every `initialize` response's `serverInfo`.
19
+ // Icons are advertised only when `MCP_PUBLIC_URL` is set (e.g. on a hosted
20
+ // deployment) — they must be absolute URLs because some MCP clients reject
21
+ // `data:` URIs. For stdio / local use the env var is typically unset and the
22
+ // `icons` field is omitted entirely.
23
+ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
24
+ const PKG = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
25
+ function buildServerIcons() {
26
+ const baseUrl = process.env.MCP_PUBLIC_URL?.trim().replace(/\/+$/, '');
27
+ if (!baseUrl) {
28
+ return undefined;
29
+ }
30
+ return [
31
+ { src: `${baseUrl}/favicon.svg`, mimeType: 'image/svg+xml' },
32
+ { src: `${baseUrl}/favicon.png`, mimeType: 'image/png', sizes: ['256x256'] },
33
+ ];
34
+ }
35
+ export const TOOLS = {
11
36
  validate_dsl: {
12
- description: 'Parse a .bpc source string. Return ok or precise parse errors with line/column.',
37
+ 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
38
  inputSchema: ValidateInputSchema,
14
39
  handler: args => validateDsl(args),
15
40
  },
16
41
  inspect_dsl: {
17
- description: 'Parse a .bpc source and return a structured summary: chartType, scenes, series count, annotations, etc.',
42
+ 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
43
  inputSchema: InspectInputSchema,
19
44
  handler: args => inspectDsl(args),
20
45
  },
@@ -24,10 +49,35 @@ const TOOLS = {
24
49
  handler: args => recommendChartType(args),
25
50
  },
26
51
  render: {
27
- description: 'Render a .bpc source to SVG (default) or PNG. Accepts scene index, width, height.',
52
+ 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
53
  inputSchema: RenderInputSchema,
29
54
  handler: args => renderTool(args),
30
55
  },
56
+ list_chart_types: {
57
+ 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.',
58
+ inputSchema: ListChartTypesInputSchema,
59
+ handler: () => listChartTypes(),
60
+ },
61
+ describe_chart_type: {
62
+ 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.',
63
+ inputSchema: DescribeChartTypeInputSchema,
64
+ handler: args => describeChartType(args),
65
+ },
66
+ get_example: {
67
+ 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.',
68
+ inputSchema: GetExampleInputSchema,
69
+ handler: args => getExample(args),
70
+ },
71
+ get_grammar: {
72
+ 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.',
73
+ inputSchema: GetGrammarInputSchema,
74
+ handler: args => getGrammar(args),
75
+ },
76
+ export_chart: {
77
+ 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).',
78
+ inputSchema: ExportChartInputSchema,
79
+ handler: args => exportChart(args),
80
+ },
31
81
  };
32
82
  function formatToolResult(result) {
33
83
  if (result.ok) {
@@ -39,7 +89,15 @@ function formatToolResult(result) {
39
89
  };
40
90
  }
41
91
  export function createServer() {
42
- const server = new Server({ name: '@blueprint-chart/mcp', version: '0.1.0' }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
92
+ const icons = buildServerIcons();
93
+ const server = new Server({
94
+ name: '@blueprint-chart/mcp',
95
+ title: 'Blueprint Chart',
96
+ version: PKG.version,
97
+ description: PKG.description,
98
+ websiteUrl: PKG.homepage,
99
+ ...(icons && { icons }),
100
+ }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
43
101
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
44
102
  tools: Object.entries(TOOLS).map(([name, def]) => ({
45
103
  name,