@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.
Files changed (131) hide show
  1. package/README.md +31 -15
  2. package/dist/cli.js +15 -2
  3. package/dist/dsl/capabilityMatrix.d.ts +22 -0
  4. package/dist/dsl/capabilityMatrix.js +37 -0
  5. package/dist/dsl/capabilityMatrix.test.d.ts +1 -0
  6. package/dist/dsl/capabilityMatrix.test.js +49 -0
  7. package/dist/dsl/chartTypes.d.ts +16 -0
  8. package/dist/dsl/chartTypes.js +37 -0
  9. package/dist/dsl/chartTypes.test.d.ts +1 -0
  10. package/dist/dsl/chartTypes.test.js +32 -0
  11. package/dist/dsl/dataKey.d.ts +25 -0
  12. package/dist/dsl/dataKey.js +42 -0
  13. package/dist/dsl/dataKey.test.d.ts +1 -0
  14. package/dist/dsl/dataKey.test.js +35 -0
  15. package/dist/dsl/goalRanking.d.ts +7 -0
  16. package/dist/dsl/goalRanking.js +76 -0
  17. package/dist/dsl/goalRanking.test.d.ts +1 -0
  18. package/dist/dsl/goalRanking.test.js +83 -0
  19. package/dist/dsl/parseErrorHints.d.ts +12 -0
  20. package/dist/dsl/parseErrorHints.js +32 -0
  21. package/dist/dsl/parseErrorHints.test.d.ts +1 -0
  22. package/dist/dsl/parseErrorHints.test.js +26 -0
  23. package/dist/dsl/semanticWarnings.d.ts +7 -0
  24. package/dist/dsl/semanticWarnings.js +66 -0
  25. package/dist/dsl/semanticWarnings.test.d.ts +1 -0
  26. package/dist/dsl/semanticWarnings.test.js +32 -0
  27. package/dist/dsl/suggest.d.ts +1 -0
  28. package/dist/dsl/suggest.js +66 -0
  29. package/dist/dsl/suggest.test.d.ts +1 -0
  30. package/dist/dsl/suggest.test.js +34 -0
  31. package/dist/dsl/universalProperties.d.ts +30 -0
  32. package/dist/dsl/universalProperties.js +52 -0
  33. package/dist/dsl/universalProperties.test.d.ts +1 -0
  34. package/dist/dsl/universalProperties.test.js +26 -0
  35. package/dist/dsl/validate.d.ts +10 -0
  36. package/dist/dsl/validate.js +68 -0
  37. package/dist/dsl/validate.test.d.ts +1 -0
  38. package/dist/dsl/validate.test.js +73 -0
  39. package/dist/errors.d.ts +20 -1
  40. package/dist/errors.js +1 -0
  41. package/dist/errors.test.js +21 -0
  42. package/dist/lib/zodToJsonSchema.d.ts +10 -5
  43. package/dist/lib/zodToJsonSchema.js +14 -6
  44. package/dist/links/buildUrls.d.ts +14 -0
  45. package/dist/links/buildUrls.js +20 -0
  46. package/dist/links/buildUrls.test.d.ts +1 -0
  47. package/dist/links/buildUrls.test.js +28 -0
  48. package/dist/links/editorConfig.d.ts +4 -0
  49. package/dist/links/editorConfig.js +15 -0
  50. package/dist/links/editorConfig.test.d.ts +1 -0
  51. package/dist/links/editorConfig.test.js +28 -0
  52. package/dist/links/encode.d.ts +11 -0
  53. package/dist/links/encode.js +19 -0
  54. package/dist/links/encode.test.d.ts +1 -0
  55. package/dist/links/encode.test.js +37 -0
  56. package/dist/parse.js +14 -6
  57. package/dist/parse.test.js +8 -0
  58. package/dist/prompts/authorChart.js +23 -18
  59. package/dist/prompts/authorChart.test.js +6 -0
  60. package/dist/render/diagnose.d.ts +19 -0
  61. package/dist/render/diagnose.js +100 -0
  62. package/dist/render/diagnose.test.d.ts +1 -0
  63. package/dist/render/diagnose.test.js +53 -0
  64. package/dist/render/frame.d.ts +10 -0
  65. package/dist/render/frame.js +10 -0
  66. package/dist/render/frame.test.d.ts +1 -0
  67. package/dist/render/frame.test.js +12 -0
  68. package/dist/render/jsdomEnv.d.ts +2 -1
  69. package/dist/render/jsdomEnv.js +14 -1
  70. package/dist/render/jsdomEnv.test.js +36 -2
  71. package/dist/render/renderSceneState.d.ts +5 -1
  72. package/dist/render/renderSceneState.js +4 -3
  73. package/dist/render/renderSceneState.test.js +13 -7
  74. package/dist/render/validatePipeline.d.ts +23 -0
  75. package/dist/render/validatePipeline.js +41 -0
  76. package/dist/render/validatePipeline.test.d.ts +1 -0
  77. package/dist/render/validatePipeline.test.js +34 -0
  78. package/dist/resources/docsReader.d.ts +4 -1
  79. package/dist/resources/docsReader.js +23 -6
  80. package/dist/resources/docsReader.test.js +27 -2
  81. package/dist/resources/index.d.ts +1 -1
  82. package/dist/resources/samples.d.ts +1 -2
  83. package/dist/server.d.ts +9 -0
  84. package/dist/server.js +75 -5
  85. package/dist/server.test.js +105 -4
  86. package/dist/tools/describeChartType.d.ts +41 -0
  87. package/dist/tools/describeChartType.js +143 -0
  88. package/dist/tools/describeChartType.test.d.ts +1 -0
  89. package/dist/tools/describeChartType.test.js +78 -0
  90. package/dist/tools/exportChart.d.ts +17 -0
  91. package/dist/tools/exportChart.js +31 -0
  92. package/dist/tools/exportChart.test.d.ts +1 -0
  93. package/dist/tools/exportChart.test.js +43 -0
  94. package/dist/tools/getExample.d.ts +20 -0
  95. package/dist/tools/getExample.js +55 -0
  96. package/dist/tools/getExample.test.d.ts +1 -0
  97. package/dist/tools/getExample.test.js +40 -0
  98. package/dist/tools/getGrammar.d.ts +17 -0
  99. package/dist/tools/getGrammar.js +38 -0
  100. package/dist/tools/getGrammar.test.d.ts +1 -0
  101. package/dist/tools/getGrammar.test.js +35 -0
  102. package/dist/tools/inspect.d.ts +8 -1
  103. package/dist/tools/inspect.js +40 -7
  104. package/dist/tools/inspect.test.js +62 -13
  105. package/dist/tools/listChartTypes.d.ts +14 -0
  106. package/dist/tools/listChartTypes.js +42 -0
  107. package/dist/tools/listChartTypes.test.d.ts +1 -0
  108. package/dist/tools/listChartTypes.test.js +42 -0
  109. package/dist/tools/listPalettes.d.ts +13 -0
  110. package/dist/tools/listPalettes.js +12 -0
  111. package/dist/tools/listPalettes.test.d.ts +1 -0
  112. package/dist/tools/listPalettes.test.js +15 -0
  113. package/dist/tools/recommend.js +3 -1
  114. package/dist/tools/recommend.test.js +40 -0
  115. package/dist/tools/render.d.ts +14 -12
  116. package/dist/tools/render.js +96 -28
  117. package/dist/tools/render.test.js +137 -1
  118. package/dist/tools/searchExamples.d.ts +28 -0
  119. package/dist/tools/searchExamples.js +54 -0
  120. package/dist/tools/searchExamples.test.d.ts +1 -0
  121. package/dist/tools/searchExamples.test.js +32 -0
  122. package/dist/tools/validate.d.ts +9 -3
  123. package/dist/tools/validate.js +11 -1
  124. package/dist/tools/validate.test.js +33 -11
  125. package/dist/transports/http.d.ts +4 -2
  126. package/dist/transports/http.js +232 -23
  127. package/dist/transports/http.test.js +158 -22
  128. package/package.json +5 -3
  129. package/public/apple-touch-icon.png +0 -0
  130. package/public/favicon.png +0 -0
  131. package/public/favicon.svg +9 -0
@@ -2,13 +2,15 @@ import { describe, expect, it } from 'vitest';
2
2
  import { samples } from '@blueprint-chart/lib';
3
3
  import { renderTool } from './render';
4
4
  describe('render', () => {
5
- it('returns SVG by default', async () => {
5
+ it('returns pure SVG by default', async () => {
6
6
  const r = await renderTool({ source: samples[0].dsl });
7
7
  expect(r.ok).toBe(true);
8
8
  if (r.ok) {
9
9
  expect(r.data.mimeType).toBe('image/svg+xml');
10
10
  expect(r.data.svg).toMatch(/^<svg/);
11
11
  expect(r.data.png).toBeUndefined();
12
+ expect(r.data.html).toBeUndefined();
13
+ expect(r.data.frame).toBeDefined();
12
14
  }
13
15
  });
14
16
  it('returns both SVG and PNG when format=png', async () => {
@@ -19,6 +21,7 @@ describe('render', () => {
19
21
  expect(r.data.svg).toMatch(/^<svg/);
20
22
  expect(r.data.png).toBeTypeOf('string'); // base64
21
23
  expect(r.data.png.length).toBeGreaterThan(100);
24
+ expect(r.data.frame).toBeDefined();
22
25
  }
23
26
  });
24
27
  it('forwards parse errors', async () => {
@@ -37,3 +40,136 @@ describe('render', () => {
37
40
  }
38
41
  });
39
42
  });
43
+ describe('render — frame defaults', () => {
44
+ it('format=svg returns pure SVG without HTML wrapper', async () => {
45
+ const { samples } = await import('@blueprint-chart/lib');
46
+ const sample = samples.find(s => s.id === 'letter-frequency');
47
+ const r = await renderTool({ source: sample.dsl, format: 'svg' });
48
+ expect(r.ok).toBe(true);
49
+ if (r.ok) {
50
+ expect(r.data.svg).toMatch(/^<svg/);
51
+ expect(r.data.svg).not.toContain('<div class="bc-frame"');
52
+ expect(r.data.html).toBeUndefined();
53
+ }
54
+ });
55
+ it('format=svg on letter-frequency returns correct frame.title', async () => {
56
+ const { samples } = await import('@blueprint-chart/lib');
57
+ const sample = samples.find(s => s.id === 'letter-frequency');
58
+ const r = await renderTool({ source: sample.dsl, format: 'svg' });
59
+ expect(r.ok).toBe(true);
60
+ if (r.ok) {
61
+ expect(r.data.frame.title).toBe('E is the most frequent letter in English');
62
+ }
63
+ });
64
+ it('format=html returns html containing bc-frame and svg', async () => {
65
+ const { samples } = await import('@blueprint-chart/lib');
66
+ const sample = samples.find(s => s.id === 'letter-frequency');
67
+ const r = await renderTool({ source: sample.dsl, format: 'html' });
68
+ expect(r.ok).toBe(true);
69
+ if (r.ok) {
70
+ expect(r.data.mimeType).toBe('text/html');
71
+ expect(r.data.html).toContain('<div class="bc-frame"');
72
+ expect(r.data.html).toContain('<svg');
73
+ expect(r.data.svg).toMatch(/^<svg/);
74
+ expect(r.data.frame).toBeDefined();
75
+ }
76
+ });
77
+ });
78
+ describe('render — save option', () => {
79
+ it('errors when MCP_ALLOW_FS_WRITE is unset', async () => {
80
+ delete process.env.MCP_ALLOW_FS_WRITE;
81
+ const r = await renderTool({
82
+ source: 'chart bar-vertical { data { "E" = 1 } }',
83
+ format: 'png',
84
+ save: '/tmp/test.png',
85
+ });
86
+ expect(r.ok).toBe(false);
87
+ if (!r.ok) {
88
+ expect(r.errors[0].code).toBe('E_FS_WRITE_DISABLED');
89
+ }
90
+ });
91
+ it('writes the PNG when MCP_ALLOW_FS_WRITE=1', async () => {
92
+ process.env.MCP_ALLOW_FS_WRITE = '1';
93
+ const { samples } = await import('@blueprint-chart/lib');
94
+ const sample = samples.find(s => s.id === 'letter-frequency');
95
+ const tmp = `/tmp/mcp-render-test-${Date.now()}.png`;
96
+ try {
97
+ const r = await renderTool({ source: sample.dsl, format: 'png', save: tmp });
98
+ expect(r.ok).toBe(true);
99
+ if (r.ok) {
100
+ expect(r.data.savedTo).toBe(tmp);
101
+ expect(r.data.png).toBeUndefined(); // inline payload omitted
102
+ const { statSync } = await import('node:fs');
103
+ expect(statSync(tmp).size).toBeGreaterThan(1000); // real PNG, not stub
104
+ }
105
+ }
106
+ finally {
107
+ const { unlinkSync, existsSync } = await import('node:fs');
108
+ if (existsSync(tmp)) {
109
+ unlinkSync(tmp);
110
+ }
111
+ delete process.env.MCP_ALLOW_FS_WRITE;
112
+ }
113
+ });
114
+ it('writes the SVG when format=svg and save provided', async () => {
115
+ process.env.MCP_ALLOW_FS_WRITE = '1';
116
+ const tmp = `/tmp/mcp-render-test-${Date.now()}.svg`;
117
+ try {
118
+ const r = await renderTool({
119
+ source: 'chart bar-vertical { title = "x" data { "E" = 1 } }',
120
+ format: 'svg',
121
+ save: tmp,
122
+ });
123
+ expect(r.ok).toBe(true);
124
+ if (r.ok) {
125
+ expect(r.data.savedTo).toBe(tmp);
126
+ expect(r.data.svg).toBeUndefined();
127
+ }
128
+ }
129
+ finally {
130
+ const { unlinkSync, existsSync } = await import('node:fs');
131
+ if (existsSync(tmp)) {
132
+ unlinkSync(tmp);
133
+ }
134
+ delete process.env.MCP_ALLOW_FS_WRITE;
135
+ }
136
+ });
137
+ });
138
+ describe('render — structured diagnostics', () => {
139
+ it('returns E_SEMANTIC + E_UNKNOWN_CHART_TYPE for chart bar', async () => {
140
+ const r = await renderTool({ source: 'chart bar { data { "E" = 1 } }' });
141
+ expect(r.ok).toBe(false);
142
+ if (!r.ok) {
143
+ expect(r.code).toBe('E_SEMANTIC');
144
+ expect(r.errors[0].code).toBe('E_UNKNOWN_CHART_TYPE');
145
+ expect(r.errors[0].suggestion).toMatch(/^bar-/);
146
+ }
147
+ });
148
+ it('returns E_SEMANTIC + E_EMPTY_DATA when data is empty', async () => {
149
+ const r = await renderTool({ source: 'chart bar-vertical { title = "x" }' });
150
+ expect(r.ok).toBe(false);
151
+ if (!r.ok) {
152
+ expect(r.code).toBe('E_SEMANTIC');
153
+ expect(r.errors[0].code).toBe('E_EMPTY_DATA');
154
+ }
155
+ });
156
+ it('never returns the generic "produced no SVG output" message', async () => {
157
+ const r = await renderTool({ source: 'chart bar { data { "E" = 1 } }' });
158
+ expect(r.ok).toBe(false);
159
+ if (!r.ok) {
160
+ for (const e of r.errors) {
161
+ expect(e.message).not.toContain('produced no SVG output');
162
+ }
163
+ }
164
+ });
165
+ it('renders every sample to SVG and provides frame metadata', async () => {
166
+ const { samples } = await import('@blueprint-chart/lib');
167
+ for (const s of samples) {
168
+ const r = await renderTool({ source: s.dsl, format: 'svg' });
169
+ expect(r.ok, `sample ${s.id}`).toBe(true);
170
+ if (r.ok) {
171
+ expect(r.data.frame, `sample ${s.id} frame`).toBeDefined();
172
+ }
173
+ }
174
+ });
175
+ });
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ import { type ToolResult } from '../errors';
3
+ export declare const SearchExamplesInputSchema: z.ZodObject<{
4
+ query: z.ZodOptional<z.ZodString>;
5
+ chartType: z.ZodOptional<z.ZodString>;
6
+ limit: z.ZodOptional<z.ZodNumber>;
7
+ }, "strict", z.ZodTypeAny, {
8
+ chartType?: string | undefined;
9
+ query?: string | undefined;
10
+ limit?: number | undefined;
11
+ }, {
12
+ chartType?: string | undefined;
13
+ query?: string | undefined;
14
+ limit?: number | undefined;
15
+ }>;
16
+ export type SearchExamplesInput = z.infer<typeof SearchExamplesInputSchema>;
17
+ export interface SearchExampleHit {
18
+ id: string;
19
+ title: string;
20
+ description: string;
21
+ chartType: string;
22
+ /** Raw count of query terms matched in title+description; used only for ordering. */
23
+ score: number;
24
+ }
25
+ export interface SearchExamplesOutput {
26
+ results: SearchExampleHit[];
27
+ }
28
+ export declare function searchExamples(input: unknown): ToolResult<SearchExamplesOutput>;
@@ -0,0 +1,54 @@
1
+ import { z } from 'zod';
2
+ import { samples } from '@blueprint-chart/lib';
3
+ import { canonicalChartType } from '../dsl/chartTypes';
4
+ import { ErrorCode, toolError, toolOk } from '../errors';
5
+ export const SearchExamplesInputSchema = z.object({
6
+ query: z.string().optional(),
7
+ chartType: z.string().optional(),
8
+ limit: z.number().int().positive().max(20).optional(),
9
+ }).strict();
10
+ function scoreSample(s, terms) {
11
+ if (terms.length === 0) {
12
+ return 1;
13
+ }
14
+ const hay = `${s.title} ${s.description}`.toLowerCase();
15
+ let score = 0;
16
+ for (const t of terms) {
17
+ if (hay.includes(t)) {
18
+ score += 1;
19
+ }
20
+ }
21
+ return score;
22
+ }
23
+ export function searchExamples(input) {
24
+ const parsed = SearchExamplesInputSchema.safeParse(input);
25
+ if (!parsed.success) {
26
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
27
+ }
28
+ const { query, chartType, limit } = parsed.data;
29
+ if ((!query || query.trim() === '') && !chartType) {
30
+ return toolError(ErrorCode.E_INPUT, [{
31
+ path: 'query',
32
+ message: 'Provide a `query` (topic keywords) and/or a `chartType` to search examples.',
33
+ }]);
34
+ }
35
+ let canonical;
36
+ if (chartType) {
37
+ canonical = canonicalChartType(chartType);
38
+ if (!canonical) {
39
+ return toolError(ErrorCode.E_INPUT, [{
40
+ code: 'E_UNKNOWN_CHART_TYPE',
41
+ path: 'chartType',
42
+ message: `Unknown chart type "${chartType}".`,
43
+ }]);
44
+ }
45
+ }
46
+ const terms = (query ?? '').toLowerCase().split(/\s+/).filter(Boolean);
47
+ const results = samples
48
+ .filter(s => (canonical ? s.chartType === canonical : true))
49
+ .map(s => ({ id: s.id, title: s.title, description: s.description, chartType: s.chartType, score: scoreSample(s, terms) }))
50
+ .filter(h => h.score > 0)
51
+ .sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
52
+ .slice(0, limit ?? 10);
53
+ return toolOk({ results });
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { searchExamples } from './searchExamples';
3
+ describe('search_examples', () => {
4
+ it('finds samples by topic keyword in title/description', () => {
5
+ const r = searchExamples({ query: 'population' });
6
+ expect(r.ok).toBe(true);
7
+ if (r.ok) {
8
+ expect(r.data.results.length).toBeGreaterThan(0);
9
+ expect(r.data.results[0]).toHaveProperty('id');
10
+ expect(r.data.results[0]).toHaveProperty('chartType');
11
+ expect(r.data.results[0]).not.toHaveProperty('dsl');
12
+ }
13
+ });
14
+ it('filters by chartType', () => {
15
+ const r = searchExamples({ chartType: 'pie' });
16
+ expect(r.ok).toBe(true);
17
+ if (r.ok) {
18
+ expect(r.data.results.every(x => x.chartType === 'pie')).toBe(true);
19
+ }
20
+ });
21
+ it('rejects when neither query nor chartType is given', () => {
22
+ const r = searchExamples({});
23
+ expect(r.ok).toBe(false);
24
+ });
25
+ it('rejects an unknown chartType', () => {
26
+ const r = searchExamples({ chartType: 'notachart' });
27
+ expect(r.ok).toBe(false);
28
+ if (!r.ok) {
29
+ expect(r.code).toBe('E_INPUT');
30
+ }
31
+ });
32
+ });
@@ -1,4 +1,6 @@
1
1
  import { z } from 'zod';
2
+ import { type ValidationIssue } from '../dsl/validate';
3
+ import { type WarningIssue } from '../dsl/semanticWarnings';
2
4
  import { type ToolResult } from '../errors';
3
5
  export declare const ValidateInputSchema: z.ZodObject<{
4
6
  source: z.ZodString;
@@ -8,6 +10,10 @@ export declare const ValidateInputSchema: z.ZodObject<{
8
10
  source: string;
9
11
  }>;
10
12
  export type ValidateInput = z.infer<typeof ValidateInputSchema>;
11
- export declare function validateDsl(input: ValidateInput): ToolResult<{
12
- valid: true;
13
- }>;
13
+ export interface ValidateOutput {
14
+ valid: boolean;
15
+ errors: ValidationIssue[];
16
+ /** Non-fatal advisories (W_NO_EFFECT / W_NOT_IMPLEMENTED / W_MULTISERIES_SHAPE). `code` is a plain string until the warning catalogue stabilises; errors keep the strict ValidationCode union. */
17
+ warnings: WarningIssue[];
18
+ }
19
+ export declare function validateDsl(input: ValidateInput): ToolResult<ValidateOutput>;
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { parseDsl } from '../parse';
3
+ import { validateAst } from '../dsl/validate';
4
+ import { collectWarnings } from '../dsl/semanticWarnings';
3
5
  import { toolOk } from '../errors';
4
6
  export const ValidateInputSchema = z.object({
5
7
  source: z.string(),
@@ -9,5 +11,13 @@ export function validateDsl(input) {
9
11
  if (!parsed.ok) {
10
12
  return parsed;
11
13
  }
12
- return toolOk({ valid: true });
14
+ const errors = validateAst(parsed.data.ast);
15
+ // Warnings are non-fatal: they never change `valid`. Only emit them when the
16
+ // structure is sound, so authors fix hard errors before chasing advisories.
17
+ const warnings = errors.length === 0 ? collectWarnings(parsed.data.ast) : [];
18
+ return toolOk({
19
+ valid: errors.length === 0,
20
+ errors,
21
+ warnings,
22
+ });
13
23
  }
@@ -2,26 +2,48 @@ import { describe, expect, it } from 'vitest';
2
2
  import { samples } from '@blueprint-chart/lib';
3
3
  import { validateDsl } from './validate';
4
4
  describe('validate_dsl', () => {
5
- it('returns ok for every shipped sample', () => {
5
+ it('returns valid: true with empty errors/warnings for every sample', () => {
6
6
  for (const s of samples) {
7
7
  const r = validateDsl({ source: s.dsl });
8
- expect(r.ok, `sample "${s.id}" should validate`).toBe(true);
8
+ expect(r.ok, `sample ${s.id}`).toBe(true);
9
+ if (r.ok) {
10
+ expect(r.data.valid).toBe(true);
11
+ expect(r.data.errors).toEqual([]);
12
+ expect(r.data.warnings).toEqual([]);
13
+ }
9
14
  }
10
15
  });
11
- it('returns parse error with line/column', () => {
12
- const r = validateDsl({ source: 'chart not-a-real-thing\n@@@' });
16
+ it('returns valid: false with E_UNKNOWN_CHART_TYPE on chart bar', () => {
17
+ const r = validateDsl({ source: 'chart bar { data { "E" = 1 } }' });
18
+ expect(r.ok).toBe(true);
19
+ if (r.ok) {
20
+ expect(r.data.valid).toBe(false);
21
+ expect(r.data.errors[0].code).toBe('E_UNKNOWN_CHART_TYPE');
22
+ expect(r.data.errors[0].suggestion).toMatch(/^bar-/);
23
+ }
24
+ });
25
+ it('still surfaces PEG errors as E_PARSE (isError channel)', () => {
26
+ const r = validateDsl({ source: '@@@ not valid' });
13
27
  expect(r.ok).toBe(false);
14
28
  if (!r.ok) {
15
29
  expect(r.code).toBe('E_PARSE');
16
- expect(r.errors[0]?.line).toBeTypeOf('number');
17
30
  }
18
31
  });
19
- it('returns E_INPUT for non-string source', () => {
20
- // @ts-expect-error testing runtime validation
21
- const r = validateDsl({ source: null });
22
- expect(r.ok).toBe(false);
23
- if (!r.ok) {
24
- expect(r.code).toBe('E_INPUT');
32
+ });
33
+ describe('validate_dsl warnings', () => {
34
+ it('stays valid but surfaces a no-op warning for sort on donut', () => {
35
+ const r = validateDsl({ source: 'chart donut {\n sort = descending\n data { "A" = 1 }\n}' });
36
+ expect(r.ok).toBe(true);
37
+ if (r.ok) {
38
+ expect(r.data.valid).toBe(true);
39
+ expect(r.data.warnings.some(w => w.code === 'W_NO_EFFECT')).toBe(true);
40
+ }
41
+ });
42
+ it('emits no warnings for a clean chart', () => {
43
+ const r = validateDsl({ source: 'chart bar-vertical {\n data { "A" = 1 }\n}' });
44
+ expect(r.ok).toBe(true);
45
+ if (r.ok) {
46
+ expect(r.data.warnings).toEqual([]);
25
47
  }
26
48
  });
27
49
  });
@@ -1,18 +1,20 @@
1
1
  export interface StartHttpOptions {
2
2
  port: number;
3
3
  host?: string;
4
- /** If set, require `Authorization: Bearer <token>` on every /mcp request. */
4
+ /** If set, require `Authorization: Bearer <token>` on every request. */
5
5
  authToken?: string;
6
6
  /** Comma-resolved CORS allowlist. `'*'` allows any origin. Default: `'*'`. */
7
7
  allowedOrigins?: string[] | '*';
8
8
  /** Read `X-Forwarded-For` for client IP (enable when behind a reverse proxy). */
9
9
  trustProxy?: boolean;
10
- /** Cap on concurrent `POST /mcp` requests. Default: 16. */
10
+ /** Cap on concurrent tool calls. Default: 16. */
11
11
  maxConcurrentRequests?: number;
12
12
  /** Per-IP token-bucket rate limit. Disabled when undefined or 0. */
13
13
  rateLimitPerMinute?: number;
14
14
  /** Suppress JSON access logs. Default: false (logs to stderr). */
15
15
  silent?: boolean;
16
+ /** If set, redirect GET / to this URL. Otherwise returns 404. */
17
+ rootRedirectUrl?: string;
16
18
  }
17
19
  export interface HttpHandle {
18
20
  url: string;