@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,68 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
4
+ import { samples } from '@blueprint-chart/lib';
5
+ import { createServer } from './server';
6
+ async function connectInMemory() {
7
+ const server = createServer();
8
+ const [clientT, serverT] = InMemoryTransport.createLinkedPair();
9
+ await server.connect(serverT);
10
+ const client = new Client({ name: 'test', version: '0' }, { capabilities: {} });
11
+ await client.connect(clientT);
12
+ return { client, server };
13
+ }
14
+ describe('server', () => {
15
+ it('lists 4 tools', async () => {
16
+ const { client } = await connectInMemory();
17
+ const r = await client.listTools();
18
+ const names = r.tools.map(t => t.name).sort();
19
+ expect(names).toEqual(['inspect_dsl', 'recommend_chart_type', 'render', 'validate_dsl']);
20
+ });
21
+ it('calls validate_dsl successfully for a sample', async () => {
22
+ const { client } = await connectInMemory();
23
+ const r = await client.callTool({
24
+ name: 'validate_dsl',
25
+ arguments: { source: samples[0].dsl },
26
+ });
27
+ expect(r.isError).toBeFalsy();
28
+ });
29
+ it('calls validate_dsl with parse error', async () => {
30
+ const { client } = await connectInMemory();
31
+ const r = await client.callTool({ name: 'validate_dsl', arguments: { source: '@@@' } });
32
+ const content = r.content;
33
+ const text = content?.[0]?.text ?? '';
34
+ expect(text).toMatch(/E_PARSE/);
35
+ });
36
+ it('lists at least 30 resources across 5 URI families', async () => {
37
+ const { client } = await connectInMemory();
38
+ const r = await client.listResources();
39
+ expect(r.resources.length).toBeGreaterThanOrEqual(30);
40
+ const prefixes = new Set(r.resources.map(rs => rs.uri.split('/').slice(0, 3).join('/')));
41
+ expect(prefixes.has('bpc://handbook')).toBe(true);
42
+ expect(prefixes.has('bpc://guide')).toBe(true);
43
+ expect(prefixes.has('bpc://chart-types')).toBe(true);
44
+ });
45
+ it('reads a handbook resource', async () => {
46
+ const { client } = await connectInMemory();
47
+ const list = await client.listResources();
48
+ const handbook = list.resources.find(r => r.uri.startsWith('bpc://handbook/'));
49
+ const r = await client.readResource({ uri: handbook.uri });
50
+ const first = r.contents[0];
51
+ expect(first.text).toMatch(/.{50,}/);
52
+ });
53
+ it('exposes bpc://samples/<id> with .bpc content', async () => {
54
+ const { client } = await connectInMemory();
55
+ const list = await client.listResources();
56
+ const sample = list.resources.find(r => r.uri.startsWith('bpc://samples/'));
57
+ const r = await client.readResource({ uri: sample.uri });
58
+ const first = r.contents[0];
59
+ expect(first.text).toMatch(/chart\s+\w/);
60
+ });
61
+ it('exposes author_chart prompt', async () => {
62
+ const { client } = await connectInMemory();
63
+ const prompts = await client.listPrompts();
64
+ expect(prompts.prompts.some(p => p.name === 'author_chart')).toBe(true);
65
+ const got = await client.getPrompt({ name: 'author_chart' });
66
+ expect(got.messages.length).toBeGreaterThan(0);
67
+ });
68
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parse } from '@blueprint-chart/lib';
3
+ import { listDocs } from '@blueprint-chart/docs';
4
+ describe('smoke', () => {
5
+ it('@blueprint-chart/lib parse() is importable', () => {
6
+ expect(typeof parse).toBe('function');
7
+ });
8
+ it('@blueprint-chart/docs listDocs() returns entries', () => {
9
+ expect(listDocs('handbook').length).toBeGreaterThan(0);
10
+ });
11
+ });
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ import { type ToolResult } from '../errors';
3
+ export declare const InspectInputSchema: z.ZodObject<{
4
+ source: z.ZodString;
5
+ }, "strip", z.ZodTypeAny, {
6
+ source: string;
7
+ }, {
8
+ source: string;
9
+ }>;
10
+ export type InspectInput = z.infer<typeof InspectInputSchema>;
11
+ export interface SceneSummary {
12
+ index: number;
13
+ name?: string;
14
+ hasTransition: boolean;
15
+ }
16
+ export interface InspectOutput {
17
+ chartType: string;
18
+ scenes: SceneSummary[];
19
+ hasAnnotations: boolean;
20
+ hasColorizes: boolean;
21
+ hasHighlights: boolean;
22
+ hasAreaFills: boolean;
23
+ seriesCount: number;
24
+ rowCount: number;
25
+ }
26
+ export declare function inspectDsl(input: InspectInput): ToolResult<InspectOutput>;
@@ -0,0 +1,37 @@
1
+ import { z } from 'zod';
2
+ import { astToDefinition } from '@blueprint-chart/lib';
3
+ import { parseDsl } from '../parse';
4
+ import { toolOk } from '../errors';
5
+ export const InspectInputSchema = z.object({
6
+ source: z.string(),
7
+ });
8
+ function summarizeScenes(ast) {
9
+ const scenes = (ast.scenes ?? []);
10
+ if (scenes.length === 0) {
11
+ return [{ index: 0, hasTransition: false }];
12
+ }
13
+ return scenes.map((scene, i) => ({
14
+ index: i,
15
+ name: scene.name ?? undefined,
16
+ // SceneNode has no explicit `transition` field; transforms power animated
17
+ // transitions in the lib, so non-empty transforms imply a transition.
18
+ hasTransition: (scene.transforms?.length ?? 0) > 0,
19
+ }));
20
+ }
21
+ export function inspectDsl(input) {
22
+ const parsed = parseDsl(input.source);
23
+ if (!parsed.ok) {
24
+ return parsed;
25
+ }
26
+ const def = astToDefinition(parsed.data.ast);
27
+ return toolOk({
28
+ chartType: def.chartType,
29
+ scenes: summarizeScenes(parsed.data.ast),
30
+ hasAnnotations: (def.annotations?.length ?? 0) > 0,
31
+ hasColorizes: (def.colorizes?.length ?? 0) > 0,
32
+ hasHighlights: (def.highlights?.length ?? 0) > 0,
33
+ hasAreaFills: (def.areaFills?.length ?? 0) > 0,
34
+ seriesCount: def.data.series?.length ?? 0,
35
+ rowCount: def.data.labels?.length ?? 0,
36
+ });
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { samples } from '@blueprint-chart/lib';
3
+ import { inspectDsl } from './inspect';
4
+ describe('inspect_dsl', () => {
5
+ it('returns chartType + scene summary for a sample', () => {
6
+ const sample = samples[0];
7
+ const r = inspectDsl({ source: sample.dsl });
8
+ expect(r.ok).toBe(true);
9
+ if (r.ok) {
10
+ expect(r.data.chartType).toBe(sample.chartType);
11
+ expect(Array.isArray(r.data.scenes)).toBe(true);
12
+ expect(r.data.seriesCount).toBeGreaterThanOrEqual(0);
13
+ expect(r.data.rowCount).toBeGreaterThanOrEqual(0);
14
+ }
15
+ });
16
+ it('returns at least one scene for every shipped sample', () => {
17
+ for (const s of samples) {
18
+ const r = inspectDsl({ source: s.dsl });
19
+ expect(r.ok, `sample ${s.id}`).toBe(true);
20
+ if (r.ok) {
21
+ expect(r.data.scenes.length).toBeGreaterThanOrEqual(1);
22
+ }
23
+ }
24
+ });
25
+ it('forwards parse errors', () => {
26
+ const r = inspectDsl({ source: '@@@ not valid' });
27
+ expect(r.ok).toBe(false);
28
+ });
29
+ });
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import { type ChartRecommendation } from '@blueprint-chart/lib';
3
+ import { type ToolResult } from '../errors';
4
+ export declare const RecommendInputSchema: z.ZodObject<{
5
+ columnTypes: z.ZodArray<z.ZodEnum<["string", "number", "date"]>, "many">;
6
+ rowCount: z.ZodNumber;
7
+ goal: z.ZodOptional<z.ZodString>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ columnTypes: ("string" | "number" | "date")[];
10
+ rowCount: number;
11
+ goal?: string | undefined;
12
+ }, {
13
+ columnTypes: ("string" | "number" | "date")[];
14
+ rowCount: number;
15
+ goal?: string | undefined;
16
+ }>;
17
+ export type RecommendInput = z.infer<typeof RecommendInputSchema>;
18
+ export interface RecommendOutput {
19
+ recommendations: ChartRecommendation[];
20
+ }
21
+ export declare function recommendChartType(input: unknown): ToolResult<RecommendOutput>;
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import { recommendCharts } from '@blueprint-chart/lib';
3
+ import { ErrorCode, toolError, toolOk } from '../errors';
4
+ const ColumnTypeSchema = z.enum(['string', 'number', 'date']);
5
+ export const RecommendInputSchema = z.object({
6
+ columnTypes: z.array(ColumnTypeSchema),
7
+ rowCount: z.number().int().nonnegative(),
8
+ goal: z.string().optional(),
9
+ });
10
+ export function recommendChartType(input) {
11
+ const parsed = RecommendInputSchema.safeParse(input);
12
+ if (!parsed.success) {
13
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
14
+ }
15
+ const recommendations = recommendCharts(parsed.data.columnTypes, parsed.data.rowCount);
16
+ return toolOk({ recommendations });
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { recommendChartType } from './recommend';
3
+ describe('recommend_chart_type', () => {
4
+ it('recommends a line chart for 1 date + 1 number', () => {
5
+ const r = recommendChartType({ columnTypes: ['date', 'number'], rowCount: 12 });
6
+ expect(r.ok).toBe(true);
7
+ if (r.ok) {
8
+ expect(r.data.recommendations[0]?.chartType).toBe('line');
9
+ expect(r.data.recommendations[0]?.fitness).toBe('best');
10
+ }
11
+ });
12
+ it('returns empty list for empty columns', () => {
13
+ const r = recommendChartType({ columnTypes: [], rowCount: 0 });
14
+ expect(r.ok).toBe(true);
15
+ if (r.ok) {
16
+ expect(r.data.recommendations).toEqual([]);
17
+ }
18
+ });
19
+ it('rejects unknown column types', () => {
20
+ const r = recommendChartType({ columnTypes: ['unknown'], rowCount: 1 });
21
+ expect(r.ok).toBe(false);
22
+ if (!r.ok) {
23
+ expect(r.code).toBe('E_INPUT');
24
+ }
25
+ });
26
+ it('rejects negative row counts', () => {
27
+ const r = recommendChartType({ columnTypes: ['string', 'number'], rowCount: -1 });
28
+ expect(r.ok).toBe(false);
29
+ if (!r.ok) {
30
+ expect(r.code).toBe('E_INPUT');
31
+ }
32
+ });
33
+ });
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { type ToolResult } from '../errors';
3
+ export declare const RenderInputSchema: z.ZodObject<{
4
+ source: z.ZodString;
5
+ format: z.ZodDefault<z.ZodEnum<["svg", "png"]>>;
6
+ scene: z.ZodOptional<z.ZodNumber>;
7
+ width: z.ZodDefault<z.ZodNumber>;
8
+ height: z.ZodDefault<z.ZodNumber>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ source: string;
11
+ width: number;
12
+ height: number;
13
+ format: "svg" | "png";
14
+ scene?: number | undefined;
15
+ }, {
16
+ source: string;
17
+ width?: number | undefined;
18
+ height?: number | undefined;
19
+ format?: "svg" | "png" | undefined;
20
+ scene?: number | undefined;
21
+ }>;
22
+ export type RenderInput = z.infer<typeof RenderInputSchema>;
23
+ export interface RenderOutput {
24
+ svg: string;
25
+ png?: string;
26
+ mimeType: 'image/svg+xml' | 'image/png';
27
+ }
28
+ /**
29
+ * Composes `parseDsl`, `renderSceneState`, and `rasterizeToPng` into the
30
+ * `render` MCP tool. Always returns SVG; when `format=png`, also includes a
31
+ * base64-encoded PNG. If rasterisation fails we surface `E_RENDER` — the SVG
32
+ * is discarded in that branch to keep the union shape (ToolResult is either
33
+ * ok-with-data or err-with-errors, no partial-success carrier).
34
+ */
35
+ export declare function renderTool(input: unknown): Promise<ToolResult<RenderOutput>>;
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod';
2
+ import { parseDsl } from '../parse';
3
+ import { renderSceneState } from '../render/renderSceneState';
4
+ import { rasterizeToPng } from '../render/rasterize';
5
+ import { ErrorCode, toolError, toolOk } from '../errors';
6
+ export const RenderInputSchema = z.object({
7
+ source: z.string(),
8
+ format: z.enum(['svg', 'png']).default('svg'),
9
+ scene: z.number().int().nonnegative().optional(),
10
+ width: z.number().int().positive().default(800),
11
+ height: z.number().int().positive().default(500),
12
+ });
13
+ /**
14
+ * `renderSceneState` returns the SVG fragment as produced by jsdom's
15
+ * `outerHTML`, which omits the default SVG namespace. resvg's strict parser
16
+ * rejects that as "the document does not have a root node". Inject the
17
+ * namespace on the way to the rasterizer so the PNG path succeeds.
18
+ */
19
+ function ensureSvgNamespace(svg) {
20
+ if (svg.includes('xmlns="http://www.w3.org/2000/svg"')) {
21
+ return svg;
22
+ }
23
+ return svg.replace(/^<svg(?=\s|>)/, '<svg xmlns="http://www.w3.org/2000/svg"');
24
+ }
25
+ /**
26
+ * Composes `parseDsl`, `renderSceneState`, and `rasterizeToPng` into the
27
+ * `render` MCP tool. Always returns SVG; when `format=png`, also includes a
28
+ * base64-encoded PNG. If rasterisation fails we surface `E_RENDER` — the SVG
29
+ * is discarded in that branch to keep the union shape (ToolResult is either
30
+ * ok-with-data or err-with-errors, no partial-success carrier).
31
+ */
32
+ export async function renderTool(input) {
33
+ const parsed = RenderInputSchema.safeParse(input);
34
+ if (!parsed.success) {
35
+ return toolError(ErrorCode.E_INPUT, parsed.error.issues.map(i => ({ path: i.path.join('.'), message: i.message })));
36
+ }
37
+ const { source, format, scene, width, height } = parsed.data;
38
+ const parseResult = parseDsl(source);
39
+ if (!parseResult.ok) {
40
+ return parseResult;
41
+ }
42
+ let svg;
43
+ try {
44
+ svg = renderSceneState(source, { sceneIndex: scene, width, height });
45
+ }
46
+ catch (err) {
47
+ return toolError(ErrorCode.E_RENDER, [
48
+ { path: 'render', message: err instanceof Error ? err.message : String(err) },
49
+ ]);
50
+ }
51
+ if (format === 'svg') {
52
+ return toolOk({ svg, mimeType: 'image/svg+xml' });
53
+ }
54
+ try {
55
+ const png = await rasterizeToPng(ensureSvgNamespace(svg), { width });
56
+ return toolOk({ svg, png: png.toString('base64'), mimeType: 'image/png' });
57
+ }
58
+ catch (err) {
59
+ return toolError(ErrorCode.E_RENDER, [
60
+ { path: 'rasterize', message: err instanceof Error ? err.message : String(err) },
61
+ ]);
62
+ }
63
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { samples } from '@blueprint-chart/lib';
3
+ import { renderTool } from './render';
4
+ describe('render', () => {
5
+ it('returns SVG by default', async () => {
6
+ const r = await renderTool({ source: samples[0].dsl });
7
+ expect(r.ok).toBe(true);
8
+ if (r.ok) {
9
+ expect(r.data.mimeType).toBe('image/svg+xml');
10
+ expect(r.data.svg).toMatch(/^<svg/);
11
+ expect(r.data.png).toBeUndefined();
12
+ }
13
+ });
14
+ it('returns both SVG and PNG when format=png', async () => {
15
+ const r = await renderTool({ source: samples[0].dsl, format: 'png', width: 600, height: 400 });
16
+ expect(r.ok).toBe(true);
17
+ if (r.ok) {
18
+ expect(r.data.mimeType).toBe('image/png');
19
+ expect(r.data.svg).toMatch(/^<svg/);
20
+ expect(r.data.png).toBeTypeOf('string'); // base64
21
+ expect(r.data.png.length).toBeGreaterThan(100);
22
+ }
23
+ });
24
+ it('forwards parse errors', async () => {
25
+ const r = await renderTool({ source: '@@@' });
26
+ expect(r.ok).toBe(false);
27
+ if (!r.ok) {
28
+ expect(r.code).toBe('E_PARSE');
29
+ }
30
+ });
31
+ it('returns E_INPUT for invalid zod input', async () => {
32
+ const r = await renderTool({ source: samples[0].dsl, width: -10 });
33
+ expect(r.ok).toBe(false);
34
+ if (!r.ok) {
35
+ expect(r.code).toBe('E_INPUT');
36
+ expect(r.errors.length).toBeGreaterThan(0);
37
+ }
38
+ });
39
+ });
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ import { type ToolResult } from '../errors';
3
+ export declare const ValidateInputSchema: z.ZodObject<{
4
+ source: z.ZodString;
5
+ }, "strip", z.ZodTypeAny, {
6
+ source: string;
7
+ }, {
8
+ source: string;
9
+ }>;
10
+ export type ValidateInput = z.infer<typeof ValidateInputSchema>;
11
+ export declare function validateDsl(input: ValidateInput): ToolResult<{
12
+ valid: true;
13
+ }>;
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+ import { parseDsl } from '../parse';
3
+ import { toolOk } from '../errors';
4
+ export const ValidateInputSchema = z.object({
5
+ source: z.string(),
6
+ });
7
+ export function validateDsl(input) {
8
+ const parsed = parseDsl(input.source);
9
+ if (!parsed.ok) {
10
+ return parsed;
11
+ }
12
+ return toolOk({ valid: true });
13
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { samples } from '@blueprint-chart/lib';
3
+ import { validateDsl } from './validate';
4
+ describe('validate_dsl', () => {
5
+ it('returns ok for every shipped sample', () => {
6
+ for (const s of samples) {
7
+ const r = validateDsl({ source: s.dsl });
8
+ expect(r.ok, `sample "${s.id}" should validate`).toBe(true);
9
+ }
10
+ });
11
+ it('returns parse error with line/column', () => {
12
+ const r = validateDsl({ source: 'chart not-a-real-thing\n@@@' });
13
+ expect(r.ok).toBe(false);
14
+ if (!r.ok) {
15
+ expect(r.code).toBe('E_PARSE');
16
+ expect(r.errors[0]?.line).toBeTypeOf('number');
17
+ }
18
+ });
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');
25
+ }
26
+ });
27
+ });
@@ -0,0 +1,21 @@
1
+ export interface StartHttpOptions {
2
+ port: number;
3
+ host?: string;
4
+ /** If set, require `Authorization: Bearer <token>` on every /mcp request. */
5
+ authToken?: string;
6
+ /** Comma-resolved CORS allowlist. `'*'` allows any origin. Default: `'*'`. */
7
+ allowedOrigins?: string[] | '*';
8
+ /** Read `X-Forwarded-For` for client IP (enable when behind a reverse proxy). */
9
+ trustProxy?: boolean;
10
+ /** Cap on concurrent `POST /mcp` requests. Default: 16. */
11
+ maxConcurrentRequests?: number;
12
+ /** Per-IP token-bucket rate limit. Disabled when undefined or 0. */
13
+ rateLimitPerMinute?: number;
14
+ /** Suppress JSON access logs. Default: false (logs to stderr). */
15
+ silent?: boolean;
16
+ }
17
+ export interface HttpHandle {
18
+ url: string;
19
+ close: () => Promise<void>;
20
+ }
21
+ export declare function startHttp(opts: StartHttpOptions): Promise<HttpHandle>;