@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.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/bin/blueprint-chart-mcp.js +15 -0
- package/bin/loader.mjs +36 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +123 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +16 -0
- package/dist/errors.test.d.ts +1 -0
- package/dist/errors.test.js +23 -0
- package/dist/lib/zodToJsonSchema.d.ts +8 -0
- package/dist/lib/zodToJsonSchema.js +9 -0
- package/dist/parse.d.ts +5 -0
- package/dist/parse.js +25 -0
- package/dist/parse.test.d.ts +1 -0
- package/dist/parse.test.js +29 -0
- package/dist/prompts/authorChart.d.ts +12 -0
- package/dist/prompts/authorChart.js +35 -0
- package/dist/prompts/authorChart.test.d.ts +1 -0
- package/dist/prompts/authorChart.test.js +13 -0
- package/dist/render/jsdomEnv.d.ts +12 -0
- package/dist/render/jsdomEnv.js +28 -0
- package/dist/render/jsdomEnv.test.d.ts +1 -0
- package/dist/render/jsdomEnv.test.js +22 -0
- package/dist/render/rasterize.d.ts +5 -0
- package/dist/render/rasterize.js +14 -0
- package/dist/render/rasterize.test.d.ts +1 -0
- package/dist/render/rasterize.test.js +24 -0
- package/dist/render/renderSceneState.d.ts +21 -0
- package/dist/render/renderSceneState.js +71 -0
- package/dist/render/renderSceneState.test.d.ts +1 -0
- package/dist/render/renderSceneState.test.js +18 -0
- package/dist/render/textShim.d.ts +12 -0
- package/dist/render/textShim.js +78 -0
- package/dist/render/textShim.test.d.ts +1 -0
- package/dist/render/textShim.test.js +31 -0
- package/dist/resources/docsReader.d.ts +14 -0
- package/dist/resources/docsReader.js +50 -0
- package/dist/resources/docsReader.test.d.ts +1 -0
- package/dist/resources/docsReader.test.js +24 -0
- package/dist/resources/index.d.ts +6 -0
- package/dist/resources/index.js +11 -0
- package/dist/resources/samples.d.ts +13 -0
- package/dist/resources/samples.js +21 -0
- package/dist/resources/samples.test.d.ts +1 -0
- package/dist/resources/samples.test.js +18 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +86 -0
- package/dist/server.test.d.ts +1 -0
- package/dist/server.test.js +68 -0
- package/dist/smoke.test.d.ts +1 -0
- package/dist/smoke.test.js +11 -0
- package/dist/tools/inspect.d.ts +26 -0
- package/dist/tools/inspect.js +37 -0
- package/dist/tools/inspect.test.d.ts +1 -0
- package/dist/tools/inspect.test.js +29 -0
- package/dist/tools/recommend.d.ts +21 -0
- package/dist/tools/recommend.js +17 -0
- package/dist/tools/recommend.test.d.ts +1 -0
- package/dist/tools/recommend.test.js +33 -0
- package/dist/tools/render.d.ts +35 -0
- package/dist/tools/render.js +63 -0
- package/dist/tools/render.test.d.ts +1 -0
- package/dist/tools/render.test.js +39 -0
- package/dist/tools/validate.d.ts +13 -0
- package/dist/tools/validate.js +13 -0
- package/dist/tools/validate.test.d.ts +1 -0
- package/dist/tools/validate.test.js +27 -0
- package/dist/transports/http.d.ts +21 -0
- package/dist/transports/http.js +193 -0
- package/dist/transports/http.test.d.ts +1 -0
- package/dist/transports/http.test.js +85 -0
- package/dist/transports/stdio.d.ts +1 -0
- package/dist/transports/stdio.js +7 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pirhoo and Blueprint Chart contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://blueprintchart.com" align="center">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/blueprint-chart/blueprint-chart/main/packages/editor/src/assets/images/blueprint-chart-logo.svg" width="120" alt="blueprint-chart">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
<p align="center"><strong>Model Context Protocol server for authoring Blueprint Chart <code>.bpc</code> files with LLMs — grounded in real dataviz pedagogy with a tight parse + render feedback loop.</strong></p>
|
|
7
|
+
|
|
8
|
+
<div align="center">
|
|
9
|
+
|
|
10
|
+
| | Status |
|
|
11
|
+
| ---: | :--- |
|
|
12
|
+
| **CI checks** | [](https://github.com/blueprint-chart/mcp/actions/workflows/ci.yml) |
|
|
13
|
+
| **Latest version** | [](https://www.npmjs.com/package/@blueprint-chart/mcp) |
|
|
14
|
+
| **Release date** | [](https://github.com/blueprint-chart/mcp/releases/latest) |
|
|
15
|
+
| **Open issues** | [](https://github.com/blueprint-chart/mcp/issues/) |
|
|
16
|
+
| **Websites** | [](https://blueprintchart.com) [](https://docs.blueprintchart.com) |
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
The MCP exposes Blueprint Chart's dataviz handbook, DSL grammar reference, chart-type docs, and canonical samples as MCP resources, plus four deterministic tools: `validate_dsl`, `inspect_dsl`, `recommend_chart_type`, and `render`. Your LLM writes the `.bpc`; the MCP grounds it in real dataviz pedagogy and gives it a tight feedback loop.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @blueprint-chart/mcp # stdio (for Claude Desktop, Claude Code, Cursor)
|
|
26
|
+
npx @blueprint-chart/mcp --http # HTTP/SSE on 127.0.0.1:4321
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Use with Claude Desktop
|
|
30
|
+
|
|
31
|
+
Add to `claude_desktop_config.json`:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"blueprint-chart": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "@blueprint-chart/mcp"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Use with Claude Code
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
claude mcp add blueprint-chart -- npx -y @blueprint-chart/mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Tools
|
|
51
|
+
|
|
52
|
+
| Tool | Purpose |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `validate_dsl` | Parse `.bpc`; precise errors with line/column |
|
|
55
|
+
| `inspect_dsl` | Parse and summarize: chart type, scenes, series, annotations |
|
|
56
|
+
| `recommend_chart_type` | Rank chart types for a given column shape |
|
|
57
|
+
| `render` | Render to SVG (default) or PNG |
|
|
58
|
+
|
|
59
|
+
## Resources
|
|
60
|
+
|
|
61
|
+
- `bpc://grammar` — full DSL syntax reference
|
|
62
|
+
- `bpc://handbook/<slug>` — dataviz pedagogy (choosing, design-principles, color, typography, annotations, accessibility, ...)
|
|
63
|
+
- `bpc://guide/<slug>` — usage guides (scenes, palettes, data-transforms, ...)
|
|
64
|
+
- `bpc://chart-types/<slug>` — per-chart-type docs
|
|
65
|
+
- `bpc://samples/<id>` — canonical `.bpc` examples
|
|
66
|
+
- `bpc://reference/dsl/<slug>`, `bpc://reference/api/<slug>` — full reference
|
|
67
|
+
|
|
68
|
+
## Prompts
|
|
69
|
+
|
|
70
|
+
- `author_chart` — primes the LLM end-to-end (read → write → validate → render → iterate)
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
### Quickstart with Claude
|
|
75
|
+
|
|
76
|
+
Once the MCP is connected, ask Claude to make a chart:
|
|
77
|
+
|
|
78
|
+
> **You:** Make a horizontal bar chart of English letter frequencies — top 10, highlight E.
|
|
79
|
+
>
|
|
80
|
+
> **Claude:** *(reads `bpc://grammar`, `bpc://handbook/choosing`, `bpc://samples/letter-frequency`, writes the `.bpc`, calls `validate_dsl` to confirm it parses, calls `render` with `format: 'png'` and shows you the image and the source)*
|
|
81
|
+
>
|
|
82
|
+
> Here's the chart:
|
|
83
|
+
>
|
|
84
|
+
> ![image]
|
|
85
|
+
>
|
|
86
|
+
> ```
|
|
87
|
+
> chart bar-horizontal {
|
|
88
|
+
> title = "E is the most frequent letter in English"
|
|
89
|
+
> sort = descending
|
|
90
|
+
> valueLabels = true
|
|
91
|
+
> highlight "E"
|
|
92
|
+
> data { "E" = 12.70; "T" = 9.06; "A" = 8.17; ... }
|
|
93
|
+
> }
|
|
94
|
+
> ```
|
|
95
|
+
|
|
96
|
+
The MCP grounds Claude in real dataviz pedagogy (the handbook) before it writes a single line of DSL, then closes the loop with deterministic parse + render feedback.
|
|
97
|
+
|
|
98
|
+
### What `.bpc` looks like
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
chart bar-vertical {
|
|
102
|
+
title = "E is the most frequent letter in English"
|
|
103
|
+
description = "How often each letter appears in typical English text"
|
|
104
|
+
source = "Lewand, Cryptological Mathematics"
|
|
105
|
+
colorPalette = "London"
|
|
106
|
+
sort = descending
|
|
107
|
+
valueLabels = true
|
|
108
|
+
highlight "E"
|
|
109
|
+
|
|
110
|
+
data {
|
|
111
|
+
"E" = 12.70
|
|
112
|
+
"T" = 9.06
|
|
113
|
+
"A" = 8.17
|
|
114
|
+
"O" = 7.51
|
|
115
|
+
...
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Full grammar at `bpc://grammar`; 17 canonical samples at `bpc://samples/<id>` (`letter-frequency`, `co2-emissions`, `quarterly-revenue`, `browser-market`, `temperature-anomaly`, `population-stacked-bar`, ...).
|
|
121
|
+
|
|
122
|
+
### `validate_dsl` — parse with precise errors
|
|
123
|
+
|
|
124
|
+
Request:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"name": "validate_dsl",
|
|
129
|
+
"arguments": { "source": "chart bar-vertical {\n title = \"oops\n}" }
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Response (note the line + column):
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"ok": false,
|
|
138
|
+
"code": "E_PARSE",
|
|
139
|
+
"errors": [
|
|
140
|
+
{ "line": 2, "column": 19, "message": "Expected \"\\\"\" but end of input found." }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `inspect_dsl` — structured summary
|
|
146
|
+
|
|
147
|
+
Request:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{ "name": "inspect_dsl", "arguments": { "source": "<.bpc source>" } }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Response:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"ok": true,
|
|
158
|
+
"data": {
|
|
159
|
+
"chartType": "bar-vertical",
|
|
160
|
+
"scenes": [{ "index": 0, "hasTransition": false }],
|
|
161
|
+
"hasAnnotations": false,
|
|
162
|
+
"hasColorizes": false,
|
|
163
|
+
"hasHighlights": true,
|
|
164
|
+
"hasAreaFills": false,
|
|
165
|
+
"seriesCount": 0,
|
|
166
|
+
"rowCount": 26
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `recommend_chart_type` — ranked suggestions
|
|
172
|
+
|
|
173
|
+
Request:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"name": "recommend_chart_type",
|
|
178
|
+
"arguments": { "columnTypes": ["date", "number", "number", "number"], "rowCount": 24 }
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Response:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"ok": true,
|
|
187
|
+
"data": {
|
|
188
|
+
"recommendations": [
|
|
189
|
+
{ "chartType": "line-multi", "label": "Multi-Line Chart", "fitness": "best",
|
|
190
|
+
"reason": "1 date + 3 numeric columns — compare trends" },
|
|
191
|
+
{ "chartType": "bar-multi", "label": "Grouped Bar Chart", "fitness": "alternative",
|
|
192
|
+
"reason": "Can also show as grouped bars" }
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### `render` — SVG (default) or PNG
|
|
199
|
+
|
|
200
|
+
Request:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"name": "render",
|
|
205
|
+
"arguments": { "source": "<.bpc source>", "format": "png", "width": 800, "height": 500 }
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Response:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"ok": true,
|
|
214
|
+
"data": {
|
|
215
|
+
"svg": "<svg ...>...</svg>",
|
|
216
|
+
"png": "<base64-encoded image>",
|
|
217
|
+
"mimeType": "image/png"
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
If rasterization fails (rare), the response is `{ ok: false, code: "E_RENDER", … }` **and still includes** the SVG that was successfully produced — partial success is preserved.
|
|
223
|
+
|
|
224
|
+
### Reading a resource
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{ "uri": "bpc://handbook/choosing" }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Returns the full Markdown of the "Choosing the Right Chart" handbook page (same content as `docs.blueprintchart.com`).
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{ "uri": "bpc://samples/letter-frequency" }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Returns the raw `.bpc` source for the letter-frequency sample as `text/plain` — exactly what the LLM should imitate.
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/blueprint-chart-mcp.js
|
|
3
|
+
//
|
|
4
|
+
// Entry point for the @blueprint-chart/mcp CLI. Registers a resolver hook
|
|
5
|
+
// that fills in `.js` extensions for the extensionless relative imports
|
|
6
|
+
// emitted by `tsc` (the project sources target `moduleResolution: "Bundler"`),
|
|
7
|
+
// then loads the compiled `dist/cli.js`.
|
|
8
|
+
import { register } from 'node:module'
|
|
9
|
+
|
|
10
|
+
register(new URL('./loader.mjs', import.meta.url))
|
|
11
|
+
|
|
12
|
+
import('../dist/cli.js').catch((err) => {
|
|
13
|
+
console.error(err)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
})
|
package/bin/loader.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// bin/loader.mjs
|
|
2
|
+
//
|
|
3
|
+
// Node ESM resolver hook that maps extensionless relative imports emitted by
|
|
4
|
+
// `tsc` (because the project source targets `moduleResolution: "Bundler"`) to
|
|
5
|
+
// concrete `.js` / `index.js` files in `dist/`.
|
|
6
|
+
//
|
|
7
|
+
// Without this hook, `node dist/cli.js` would fail with ERR_MODULE_NOT_FOUND
|
|
8
|
+
// on every relative import. Registered by `bin/blueprint-chart-mcp.js`.
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
const CANDIDATES = ['.js', '.mjs', '/index.js', '/index.mjs']
|
|
13
|
+
|
|
14
|
+
function fileExists(url) {
|
|
15
|
+
try {
|
|
16
|
+
return existsSync(fileURLToPath(url))
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolve(specifier, context, nextResolve) {
|
|
24
|
+
if ((specifier.startsWith('./') || specifier.startsWith('../')) && !/\.[a-zA-Z0-9]+$/.test(specifier)) {
|
|
25
|
+
const baseUrl = context.parentURL ? new URL(context.parentURL) : null
|
|
26
|
+
if (baseUrl) {
|
|
27
|
+
for (const ext of CANDIDATES) {
|
|
28
|
+
const candidate = new URL(specifier + ext, baseUrl)
|
|
29
|
+
if (fileExists(candidate)) {
|
|
30
|
+
return nextResolve(specifier + ext, context)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return nextResolve(specifier, context)
|
|
36
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { startStdio } from './transports/stdio.js';
|
|
2
|
+
import { startHttp } from './transports/http.js';
|
|
3
|
+
function parseBool(value) {
|
|
4
|
+
if (!value) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return value === '1' || value.toLowerCase() === 'true';
|
|
8
|
+
}
|
|
9
|
+
function parseList(value) {
|
|
10
|
+
if (!value) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return value.split(',').map(s => s.trim()).filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
function parseConfig(argv) {
|
|
16
|
+
const env = process.env;
|
|
17
|
+
const port = Number(env.PORT) || 4321;
|
|
18
|
+
const host = env.MCP_HOST || '127.0.0.1';
|
|
19
|
+
const allowedOriginsList = parseList(env.MCP_ALLOWED_ORIGINS);
|
|
20
|
+
const allowedOrigins = allowedOriginsList
|
|
21
|
+
&& allowedOriginsList.length === 1 && allowedOriginsList[0] === '*'
|
|
22
|
+
? '*'
|
|
23
|
+
: allowedOriginsList;
|
|
24
|
+
const config = {
|
|
25
|
+
http: parseBool(env.MCP_HTTP),
|
|
26
|
+
port,
|
|
27
|
+
host,
|
|
28
|
+
httpOpts: {
|
|
29
|
+
port,
|
|
30
|
+
host,
|
|
31
|
+
authToken: env.MCP_AUTH_TOKEN || undefined,
|
|
32
|
+
allowedOrigins,
|
|
33
|
+
trustProxy: parseBool(env.MCP_TRUST_PROXY),
|
|
34
|
+
maxConcurrentRequests: env.MCP_MAX_CONCURRENT_REQUESTS
|
|
35
|
+
? Number(env.MCP_MAX_CONCURRENT_REQUESTS)
|
|
36
|
+
: undefined,
|
|
37
|
+
rateLimitPerMinute: env.MCP_RATE_LIMIT_PER_MINUTE
|
|
38
|
+
? Number(env.MCP_RATE_LIMIT_PER_MINUTE)
|
|
39
|
+
: undefined,
|
|
40
|
+
silent: parseBool(env.MCP_SILENT),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
for (let i = 0; i < argv.length; i++) {
|
|
44
|
+
const a = argv[i];
|
|
45
|
+
if (a === '--http') {
|
|
46
|
+
config.http = true;
|
|
47
|
+
}
|
|
48
|
+
else if (a === '--stdio') {
|
|
49
|
+
config.http = false;
|
|
50
|
+
}
|
|
51
|
+
else if (a === '--port' && argv[i + 1]) {
|
|
52
|
+
const p = Number(argv[++i]);
|
|
53
|
+
config.port = p;
|
|
54
|
+
config.httpOpts.port = p;
|
|
55
|
+
}
|
|
56
|
+
else if (a === '--host' && argv[i + 1]) {
|
|
57
|
+
const next = argv[++i];
|
|
58
|
+
if (next) {
|
|
59
|
+
config.host = next;
|
|
60
|
+
config.httpOpts.host = next;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (a === '--help' || a === '-h') {
|
|
64
|
+
process.stderr.write(`Usage: blueprint-chart-mcp [--http|--stdio] [--port N] [--host HOST]
|
|
65
|
+
|
|
66
|
+
Stdio mode (default): for Claude Desktop, Claude Code, Cursor, etc.
|
|
67
|
+
HTTP mode (--http): for hosted use (e.g. Railway, behind a reverse proxy).
|
|
68
|
+
|
|
69
|
+
Environment variables (HTTP mode):
|
|
70
|
+
PORT HTTP port (default 4321). Railway sets this.
|
|
71
|
+
MCP_HTTP=1 Default to HTTP mode (same as --http).
|
|
72
|
+
MCP_HOST Bind host (default 127.0.0.1; use 0.0.0.0 in containers).
|
|
73
|
+
MCP_AUTH_TOKEN If set, require Authorization: Bearer <token>.
|
|
74
|
+
MCP_ALLOWED_ORIGINS Comma-separated CORS allowlist, or "*" (default).
|
|
75
|
+
MCP_TRUST_PROXY=1 Read X-Forwarded-For (enable behind a proxy / Railway).
|
|
76
|
+
MCP_MAX_CONCURRENT_REQUESTS Cap on concurrent POSTs (default 16).
|
|
77
|
+
MCP_RATE_LIMIT_PER_MINUTE Per-IP rate limit (default off; e.g. 60).
|
|
78
|
+
MCP_SILENT=1 Suppress JSON access logs to stderr.
|
|
79
|
+
`);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return config;
|
|
84
|
+
}
|
|
85
|
+
function installSignalHandlers(close) {
|
|
86
|
+
let shuttingDown = false;
|
|
87
|
+
const handler = (signal) => {
|
|
88
|
+
if (shuttingDown) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
shuttingDown = true;
|
|
92
|
+
close()
|
|
93
|
+
.catch((err) => {
|
|
94
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
95
|
+
process.stderr.write(`Error during shutdown (${signal}): ${message}\n`);
|
|
96
|
+
})
|
|
97
|
+
.finally(() => {
|
|
98
|
+
process.exit(0);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
process.on('SIGINT', handler);
|
|
102
|
+
process.on('SIGTERM', handler);
|
|
103
|
+
}
|
|
104
|
+
const config = parseConfig(process.argv.slice(2));
|
|
105
|
+
if (config.http) {
|
|
106
|
+
startHttp(config.httpOpts)
|
|
107
|
+
.then((handle) => {
|
|
108
|
+
process.stderr.write(`MCP HTTP server listening at ${handle.url}/mcp\n`);
|
|
109
|
+
installSignalHandlers(handle.close);
|
|
110
|
+
})
|
|
111
|
+
.catch((err) => {
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
process.stderr.write(`Failed to start HTTP: ${message}\n`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
startStdio().catch((err) => {
|
|
119
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
120
|
+
process.stderr.write(`Failed to start stdio: ${message}\n`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const ErrorCode: {
|
|
2
|
+
readonly E_INPUT: "E_INPUT";
|
|
3
|
+
readonly E_PARSE: "E_PARSE";
|
|
4
|
+
readonly E_SEMANTIC: "E_SEMANTIC";
|
|
5
|
+
readonly E_RENDER: "E_RENDER";
|
|
6
|
+
readonly E_INTERNAL: "E_INTERNAL";
|
|
7
|
+
};
|
|
8
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
9
|
+
export interface ToolErrorEntry {
|
|
10
|
+
path?: string;
|
|
11
|
+
line?: number;
|
|
12
|
+
column?: number;
|
|
13
|
+
message: string;
|
|
14
|
+
snippet?: string;
|
|
15
|
+
}
|
|
16
|
+
export type ToolResult<T> = {
|
|
17
|
+
ok: true;
|
|
18
|
+
data: T;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
code: ErrorCode;
|
|
22
|
+
errors: ToolErrorEntry[];
|
|
23
|
+
};
|
|
24
|
+
export declare function toolOk<T>(data: T): ToolResult<T>;
|
|
25
|
+
export declare function toolError<T = never>(code: ErrorCode, errors: ToolErrorEntry[]): ToolResult<T>;
|
|
26
|
+
export declare function isToolError<T>(r: ToolResult<T>): r is Extract<ToolResult<T>, {
|
|
27
|
+
ok: false;
|
|
28
|
+
}>;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const ErrorCode = {
|
|
2
|
+
E_INPUT: 'E_INPUT',
|
|
3
|
+
E_PARSE: 'E_PARSE',
|
|
4
|
+
E_SEMANTIC: 'E_SEMANTIC',
|
|
5
|
+
E_RENDER: 'E_RENDER',
|
|
6
|
+
E_INTERNAL: 'E_INTERNAL',
|
|
7
|
+
};
|
|
8
|
+
export function toolOk(data) {
|
|
9
|
+
return { ok: true, data };
|
|
10
|
+
}
|
|
11
|
+
export function toolError(code, errors) {
|
|
12
|
+
return { ok: false, code, errors };
|
|
13
|
+
}
|
|
14
|
+
export function isToolError(r) {
|
|
15
|
+
return r.ok === false;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ErrorCode, toolError, toolOk, isToolError } from './errors';
|
|
3
|
+
describe('errors', () => {
|
|
4
|
+
it('toolOk wraps data', () => {
|
|
5
|
+
const r = toolOk({ x: 1 });
|
|
6
|
+
expect(r.ok).toBe(true);
|
|
7
|
+
if (r.ok) {
|
|
8
|
+
expect(r.data).toEqual({ x: 1 });
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
it('toolError wraps code + errors[]', () => {
|
|
12
|
+
const r = toolError(ErrorCode.E_PARSE, [{ line: 2, column: 3, message: 'oops' }]);
|
|
13
|
+
expect(r.ok).toBe(false);
|
|
14
|
+
if (!r.ok) {
|
|
15
|
+
expect(r.code).toBe('E_PARSE');
|
|
16
|
+
expect(r.errors[0]).toMatchObject({ line: 2, column: 3 });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
it('isToolError narrows the union', () => {
|
|
20
|
+
const r = toolError(ErrorCode.E_INTERNAL, [{ message: 'x' }]);
|
|
21
|
+
expect(isToolError(r)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ZodTypeAny } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Permissive stub: returns a generic object schema that allows any properties.
|
|
4
|
+
* MCP tool handlers do the precise runtime validation via Zod. If MCP clients
|
|
5
|
+
* begin requiring precise JSON Schema for `listTools`, replace this body with
|
|
6
|
+
* the `zod-to-json-schema` package.
|
|
7
|
+
*/
|
|
8
|
+
export declare function zodToJsonSchema(_schema: ZodTypeAny): Record<string, unknown>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissive stub: returns a generic object schema that allows any properties.
|
|
3
|
+
* MCP tool handlers do the precise runtime validation via Zod. If MCP clients
|
|
4
|
+
* begin requiring precise JSON Schema for `listTools`, replace this body with
|
|
5
|
+
* the `zod-to-json-schema` package.
|
|
6
|
+
*/
|
|
7
|
+
export function zodToJsonSchema(_schema) {
|
|
8
|
+
return { type: 'object', additionalProperties: true };
|
|
9
|
+
}
|
package/dist/parse.d.ts
ADDED
package/dist/parse.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parse as libParse } from '@blueprint-chart/lib';
|
|
2
|
+
import { ErrorCode, toolError, toolOk } from './errors';
|
|
3
|
+
export function parseDsl(source) {
|
|
4
|
+
if (typeof source !== 'string') {
|
|
5
|
+
return toolError(ErrorCode.E_INPUT, [{ path: 'source', message: 'expected string' }]);
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
const ast = libParse(source);
|
|
9
|
+
return toolOk({ ast });
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
if (err instanceof Error) {
|
|
13
|
+
// lib's parser wraps SyntaxError with " at L:C" suffix in the message
|
|
14
|
+
const match = err.message.match(/^(.*) at (\d+):(\d+)$/);
|
|
15
|
+
if (match) {
|
|
16
|
+
const [, message, line, column] = match;
|
|
17
|
+
return toolError(ErrorCode.E_PARSE, [
|
|
18
|
+
{ line: Number(line), column: Number(column), message: message.trim() },
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
return toolError(ErrorCode.E_PARSE, [{ message: err.message }]);
|
|
22
|
+
}
|
|
23
|
+
return toolError(ErrorCode.E_INTERNAL, [{ message: String(err) }]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -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 { parseDsl } from './parse';
|
|
4
|
+
describe('parseDsl', () => {
|
|
5
|
+
it('parses a real lib sample', () => {
|
|
6
|
+
const r = parseDsl(samples[0].dsl);
|
|
7
|
+
expect(r.ok).toBe(true);
|
|
8
|
+
if (r.ok) {
|
|
9
|
+
expect(r.data.ast).toBeDefined();
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
it('returns E_PARSE with line + column for a syntax error', () => {
|
|
13
|
+
const r = parseDsl('chart bogus\n garbage at line 2');
|
|
14
|
+
expect(r.ok).toBe(false);
|
|
15
|
+
if (!r.ok) {
|
|
16
|
+
expect(r.code).toBe('E_PARSE');
|
|
17
|
+
expect(r.errors[0]).toHaveProperty('line');
|
|
18
|
+
expect(r.errors[0]).toHaveProperty('column');
|
|
19
|
+
expect(typeof r.errors[0].message).toBe('string');
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
it('returns E_INPUT for non-string input', () => {
|
|
23
|
+
const r = parseDsl(123);
|
|
24
|
+
expect(r.ok).toBe(false);
|
|
25
|
+
if (!r.ok) {
|
|
26
|
+
expect(r.code).toBe('E_INPUT');
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const BODY = `You are authoring a Blueprint Chart (\`.bpc\`) file for a user.
|
|
2
|
+
|
|
3
|
+
Workflow:
|
|
4
|
+
1. Read \`bpc://grammar\` for the DSL syntax.
|
|
5
|
+
2. Read \`bpc://handbook/choosing\` and \`bpc://handbook/design-principles\` for dataviz judgment.
|
|
6
|
+
3. If unsure about the chart type, call \`recommend_chart_type\` with the user's column types and row count.
|
|
7
|
+
4. Read \`bpc://chart-types/<type>\` for the specific chart you'll use.
|
|
8
|
+
5. Look at \`bpc://samples/<id>\` for a working example in the same family.
|
|
9
|
+
6. Write the \`.bpc\` source.
|
|
10
|
+
7. Call \`validate_dsl\` on your draft. If errors, read the line/column from the response and fix them.
|
|
11
|
+
8. Call \`inspect_dsl\` to sanity-check the parsed structure.
|
|
12
|
+
9. Call \`render\` with format="png" to get a visual. If the chart looks wrong, iterate on the \`.bpc\` and re-render.
|
|
13
|
+
10. Return the final \`.bpc\` source AND the rendered chart to the user.
|
|
14
|
+
|
|
15
|
+
Resources you can read:
|
|
16
|
+
- \`bpc://grammar\` — DSL syntax reference (aggregate)
|
|
17
|
+
- \`bpc://handbook/{slug}\` — dataviz pedagogy (choosing, design-principles, color, typography, annotations, accessibility, ...)
|
|
18
|
+
- \`bpc://guide/{slug}\` — Blueprint Chart guides (scenes, palettes, data-transforms, ...)
|
|
19
|
+
- \`bpc://chart-types/{slug}\` — per-chart-type docs
|
|
20
|
+
- \`bpc://samples/{id}\` — canonical \`.bpc\` examples
|
|
21
|
+
- \`bpc://reference/dsl/{slug}\`, \`bpc://reference/api/{slug}\` — full reference
|
|
22
|
+
|
|
23
|
+
Tools:
|
|
24
|
+
- \`validate_dsl({source})\` — parse, return errors with line/column
|
|
25
|
+
- \`inspect_dsl({source})\` — parsed summary (chart type, scenes, series, annotations)
|
|
26
|
+
- \`recommend_chart_type({columnTypes, rowCount, goal?})\` — ranked suggestions
|
|
27
|
+
- \`render({source, format, scene?, width?, height?})\` — SVG (default) or PNG
|
|
28
|
+
|
|
29
|
+
Be patient with errors — the feedback loop is the point.`;
|
|
30
|
+
export function authorChartPrompt() {
|
|
31
|
+
return {
|
|
32
|
+
description: 'Author a Blueprint Chart .bpc file with the LLM as the writer; the MCP as validator + renderer.',
|
|
33
|
+
messages: [{ role: 'user', content: { type: 'text', text: BODY } }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { authorChartPrompt } from './authorChart';
|
|
3
|
+
describe('authorChartPrompt', () => {
|
|
4
|
+
it('returns a non-empty prompt body with resource URIs', () => {
|
|
5
|
+
const p = authorChartPrompt();
|
|
6
|
+
expect(p.messages.length).toBeGreaterThan(0);
|
|
7
|
+
const first = p.messages[0];
|
|
8
|
+
expect(first.role).toBe('user');
|
|
9
|
+
expect(first.content.type).toBe('text');
|
|
10
|
+
expect(first.content.text).toMatch(/bpc:\/\/grammar/);
|
|
11
|
+
expect(first.content.text).toMatch(/validate_dsl/);
|
|
12
|
+
});
|
|
13
|
+
});
|