graphein-mcp 0.17.0 → 0.19.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/README.md +49 -2
- package/dist/chunk-CODCFNDV.js +624 -0
- package/dist/chunk-CODCFNDV.js.map +1 -0
- package/dist/index.d.ts +67 -5
- package/dist/index.js +13 -1
- package/dist/server.js +1 -1
- package/package.json +3 -3
- package/resources/agent-guide.md +208 -3
- package/resources/chart-spec.schema.json +98 -129
- package/resources/spec-reference.md +248 -9
- package/dist/chunk-PHH2HU3O.js +0 -350
- package/dist/chunk-PHH2HU3O.js.map +0 -1
package/README.md
CHANGED
|
@@ -36,14 +36,61 @@ npm install -g graphein-mcp # then: graphein-mcp
|
|
|
36
36
|
| Tool | What it does |
|
|
37
37
|
| --- | --- |
|
|
38
38
|
| **`render_chart`** | The one-call loop. Validates a `ChartSpec`, auto-repairs safe mistakes, renders a PNG, and returns the **image** plus a **vision-free critique** (render report + lint warnings + repairs applied). If the spec can't be made valid, returns structured errors with JSON-Patch fixes instead of an image. |
|
|
39
|
+
| **`critique_chart`** | Same validation/rendering policy, but report-only: no image block. |
|
|
40
|
+
| **`recommend_chart`** | Profile tidy rows (or CSV/TSV) and return ranked specs with rationale and unresolved data decisions. |
|
|
41
|
+
| **`list_chart_types`** | Every registered chart/slicer family with purpose, requirements, capabilities, and a runnable starter. Dashboards compose these visuals. |
|
|
39
42
|
| **`validate_chart`** | Validate without rendering. Returns structural errors (each with a JSON-Patch `fix` when unambiguous, plus "did you mean" suggestions) and best-practice lint warnings. |
|
|
40
43
|
| **`repair_chart`** | Apply every safe, unambiguous fix and return the corrected spec, the patch ops applied, and whether it's now valid. |
|
|
41
44
|
| **`summarize_chart`** | Deterministic, plain-English description of what the data shows (doubles as alt-text; no LLM). |
|
|
42
45
|
|
|
43
|
-
`render_chart`
|
|
44
|
-
(default `true`)
|
|
46
|
+
`render_chart` and `critique_chart` accept `spec` plus optional `width`, `height`,
|
|
47
|
+
`dpr`, `repair` (default `true`), `repairLevel` (`'safe'` default; `'data'` explicitly
|
|
48
|
+
permits data-aware authoring inference), and `quality`. Every type rasterizes — kpi, table, matrix, slicers and dashboard render
|
|
45
49
|
a static canvas snapshot, so the whole catalog returns an image + report.
|
|
46
50
|
|
|
51
|
+
### Opt-in presentation improvement
|
|
52
|
+
|
|
53
|
+
The default remains **one render**. Set `"quality":true` (or
|
|
54
|
+
`"quality":{"maxPasses":3}`) to reuse core's bounded controller:
|
|
55
|
+
|
|
56
|
+
- At most **three total whole-visual render attempts**, including initial and
|
|
57
|
+
restoration passes. Never per diagnostic or dashboard child.
|
|
58
|
+
- Only absent, eligible presentation defaults can change. Explicit settings,
|
|
59
|
+
palettes/domains, encodings, and data are protected; semantic decisions stay with
|
|
60
|
+
the caller. `repair` / `repairLevel` remain separate authoring policies.
|
|
61
|
+
- Every candidate validates; no progress, regressions, repeated actions, or failures
|
|
62
|
+
stop the loop. Mutable targets reserve a rollback pass, so `maxPasses:1` or `2`
|
|
63
|
+
only evaluate the initial render.
|
|
64
|
+
- Opted-in results include **`effectiveSpec`** and **`quality`**
|
|
65
|
+
(`renderPasses`, `selectedPass`, `iterations`, `applied`, `rejected`, `stopReason`).
|
|
66
|
+
The PNG and full `report` match that selected spec. Remaining warnings and
|
|
67
|
+
incomplete evidence are not hidden.
|
|
68
|
+
- **`authoring`** includes the core input-row `profile` and selected-family
|
|
69
|
+
`capabilities` when available. Dashboard `authoring.views` identifies own-data profiles and
|
|
70
|
+
shared-data inheritance. This is supplied-input evidence before transforms,
|
|
71
|
+
filters, or selection effects, distinct from the final report's effective-row/draw
|
|
72
|
+
evidence. Differing counts or invalid-value findings are expected after explicit
|
|
73
|
+
preparation; profiling does not execute that pipeline or invent an encoding.
|
|
74
|
+
|
|
75
|
+
Rendering/critique responses expose the same JSON in the text block and MCP
|
|
76
|
+
`structuredContent`; existing top-level counts/diagnostics on image results remain
|
|
77
|
+
available. Invalid specs return structured validation errors and
|
|
78
|
+
`quality.stopReason:'invalid-spec'` with zero render passes, not an image.
|
|
79
|
+
|
|
80
|
+
### Data-driven authoring
|
|
81
|
+
|
|
82
|
+
`recommend_chart` preserves its `recommendations` array and adds the shared core
|
|
83
|
+
`profile`, `findings`, and `status` (`ready`, `needs-decision`, `no-candidate`).
|
|
84
|
+
Candidates include evidence and alternatives; a valid candidate may still require
|
|
85
|
+
a choice about duplicate positions, dates/identifiers, missing data, aggregation, or
|
|
86
|
+
geometry. Optional `targetSize:{width,height}` guides recommendations without
|
|
87
|
+
filtering/grouping rows. `list_chart_types` exposes each family's field roles,
|
|
88
|
+
requirements, cardinality guidance, supported features, and actual aggregation
|
|
89
|
+
semantics. Neither tool rewrites data to make a recommendation appear successful.
|
|
90
|
+
Validation/rendering/critique warning payloads preserve exact per-field
|
|
91
|
+
`evidence` counts and `requiresDecision`, so data-quality decisions are not lost
|
|
92
|
+
when the core findings are sent through MCP.
|
|
93
|
+
|
|
47
94
|
### Example result
|
|
48
95
|
|
|
49
96
|
Calling `render_chart` with a valid line spec returns an `image` block and a `text` block:
|
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
// src/tabular.ts
|
|
2
|
+
var TabularParseError = class extends Error {
|
|
3
|
+
/** Create a parse error with a human-readable message. */
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "TabularParseError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function detectDelimiter(text2) {
|
|
10
|
+
let comma = 0;
|
|
11
|
+
let tab = 0;
|
|
12
|
+
let quoted = false;
|
|
13
|
+
for (let i = 0; i < text2.length; i++) {
|
|
14
|
+
const ch = text2[i];
|
|
15
|
+
if (ch === '"') {
|
|
16
|
+
if (quoted && text2[i + 1] === '"') {
|
|
17
|
+
i++;
|
|
18
|
+
} else {
|
|
19
|
+
quoted = !quoted;
|
|
20
|
+
}
|
|
21
|
+
} else if (!quoted && (ch === "\n" || ch === "\r")) {
|
|
22
|
+
break;
|
|
23
|
+
} else if (!quoted && ch === ",") {
|
|
24
|
+
comma++;
|
|
25
|
+
} else if (!quoted && ch === " ") {
|
|
26
|
+
tab++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return tab > comma ? " " : ",";
|
|
30
|
+
}
|
|
31
|
+
function coerce(value) {
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
if (/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(trimmed)) {
|
|
34
|
+
const n = Number(trimmed);
|
|
35
|
+
if (Number.isFinite(n)) return n;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function parseRecords(text2, delimiter) {
|
|
40
|
+
const records = [];
|
|
41
|
+
let record = [];
|
|
42
|
+
let value = "";
|
|
43
|
+
let quoted = false;
|
|
44
|
+
let afterQuote = false;
|
|
45
|
+
const pushCell = () => {
|
|
46
|
+
record.push({ value });
|
|
47
|
+
value = "";
|
|
48
|
+
afterQuote = false;
|
|
49
|
+
};
|
|
50
|
+
const pushRecord = () => {
|
|
51
|
+
pushCell();
|
|
52
|
+
records.push(record);
|
|
53
|
+
record = [];
|
|
54
|
+
};
|
|
55
|
+
for (let i = 0; i < text2.length; i++) {
|
|
56
|
+
const ch = text2[i];
|
|
57
|
+
if (quoted) {
|
|
58
|
+
if (ch === '"') {
|
|
59
|
+
if (text2[i + 1] === '"') {
|
|
60
|
+
value += '"';
|
|
61
|
+
i++;
|
|
62
|
+
} else {
|
|
63
|
+
quoted = false;
|
|
64
|
+
afterQuote = true;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
value += ch;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (afterQuote && ch !== delimiter && ch !== "\n" && ch !== "\r") {
|
|
72
|
+
if (/\s/.test(ch)) continue;
|
|
73
|
+
throw new TabularParseError(`Unexpected character '${ch}' after closing quote.`);
|
|
74
|
+
}
|
|
75
|
+
if (ch === '"') {
|
|
76
|
+
if (value.length > 0) throw new TabularParseError("Unexpected quote in an unquoted field.");
|
|
77
|
+
quoted = true;
|
|
78
|
+
} else if (ch === delimiter) {
|
|
79
|
+
pushCell();
|
|
80
|
+
} else if (ch === "\n") {
|
|
81
|
+
pushRecord();
|
|
82
|
+
} else if (ch === "\r") {
|
|
83
|
+
pushRecord();
|
|
84
|
+
if (text2[i + 1] === "\n") i++;
|
|
85
|
+
} else {
|
|
86
|
+
value += ch;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (quoted) throw new TabularParseError("Unclosed quoted field.");
|
|
90
|
+
if (value.length > 0 || record.length > 0 || text2.endsWith(String(delimiter))) pushRecord();
|
|
91
|
+
return records.filter((r) => r.some((cell) => cell.value.length > 0));
|
|
92
|
+
}
|
|
93
|
+
function parseTabularText(text2) {
|
|
94
|
+
const trimmed = text2.replace(/^\uFEFF/, "");
|
|
95
|
+
const delimiter = detectDelimiter(trimmed);
|
|
96
|
+
const records = parseRecords(trimmed, delimiter);
|
|
97
|
+
if (records.length === 0) return [];
|
|
98
|
+
const headers = records[0].map((cell) => cell.value.trim());
|
|
99
|
+
if (headers.some((h) => h.length === 0)) {
|
|
100
|
+
throw new TabularParseError("CSV/TSV header row must not contain empty column names.");
|
|
101
|
+
}
|
|
102
|
+
if (new Set(headers).size !== headers.length) {
|
|
103
|
+
throw new TabularParseError("CSV/TSV header row must not contain duplicate column names.");
|
|
104
|
+
}
|
|
105
|
+
return records.slice(1).map((record, rowIndex) => {
|
|
106
|
+
if (record.length !== headers.length) {
|
|
107
|
+
throw new TabularParseError(
|
|
108
|
+
`Row ${rowIndex + 2} has ${record.length} cells, expected ${headers.length}.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const row = {};
|
|
112
|
+
for (let i = 0; i < headers.length; i++) row[headers[i]] = coerce(record[i].value);
|
|
113
|
+
return row;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function parseInlineData(value) {
|
|
117
|
+
if (Array.isArray(value)) return value.map(parseInlineData);
|
|
118
|
+
if (typeof value !== "object" || value === null) return value;
|
|
119
|
+
const out = {};
|
|
120
|
+
for (const [key, child] of Object.entries(value)) {
|
|
121
|
+
out[key] = key === "data" && typeof child === "string" ? parseTabularText(child) : parseInlineData(child);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/handlers.ts
|
|
127
|
+
import {
|
|
128
|
+
validateSpec,
|
|
129
|
+
draft,
|
|
130
|
+
repairSpec,
|
|
131
|
+
recommendChart,
|
|
132
|
+
profileData,
|
|
133
|
+
listChartTypes,
|
|
134
|
+
summarize
|
|
135
|
+
} from "graphein";
|
|
136
|
+
import { NodeRenderError, renderChart } from "@graphein/node";
|
|
137
|
+
function text(value) {
|
|
138
|
+
return { type: "text", text: value };
|
|
139
|
+
}
|
|
140
|
+
function json(value) {
|
|
141
|
+
return text(JSON.stringify(value, null, 2));
|
|
142
|
+
}
|
|
143
|
+
function specType(spec) {
|
|
144
|
+
return typeof spec === "object" && spec !== null && "type" in spec ? String(spec.type) : "(missing)";
|
|
145
|
+
}
|
|
146
|
+
function isRecommendIntent(value) {
|
|
147
|
+
return value === void 0 || value === "trend" || value === "comparison" || value === "distribution" || value === "relationship" || value === "composition";
|
|
148
|
+
}
|
|
149
|
+
function parseErrorResult(e, quality) {
|
|
150
|
+
const payload = {
|
|
151
|
+
ok: false,
|
|
152
|
+
stage: "parse-data",
|
|
153
|
+
message: e instanceof Error ? e.message : String(e),
|
|
154
|
+
...quality ? {
|
|
155
|
+
rendered: false,
|
|
156
|
+
quality: draft(void 0, { repair: false, quality }).quality
|
|
157
|
+
} : {}
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
isError: true,
|
|
161
|
+
content: [json(payload)],
|
|
162
|
+
structuredContent: payload
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function specWithParsedData(spec) {
|
|
166
|
+
return parseInlineData(spec);
|
|
167
|
+
}
|
|
168
|
+
function rowsWithParsedData(data) {
|
|
169
|
+
return typeof data === "string" ? parseTabularText(data) : data;
|
|
170
|
+
}
|
|
171
|
+
function tidyError(e) {
|
|
172
|
+
const out = { path: e.path, message: e.message };
|
|
173
|
+
if (e.rule) out.rule = e.rule;
|
|
174
|
+
if (e.severity) out.severity = e.severity;
|
|
175
|
+
if (e.fix) out.fix = e.fix;
|
|
176
|
+
if (e.suggestion) out.suggestion = e.suggestion;
|
|
177
|
+
if (e.evidence) out.evidence = e.evidence;
|
|
178
|
+
if (e.requiresDecision !== void 0) out.requiresDecision = e.requiresDecision;
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
function authoringMetadata(spec) {
|
|
182
|
+
const types = listChartTypes();
|
|
183
|
+
const capabilities = (type) => types.find((t) => t.type === type)?.capabilities;
|
|
184
|
+
if (spec.type !== "dashboard") {
|
|
185
|
+
return { profile: profileData(spec.data ?? []), capabilities: capabilities(spec.type) };
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
...spec.data ? { profile: profileData(spec.data) } : {},
|
|
189
|
+
capabilities: capabilities(spec.type),
|
|
190
|
+
views: spec.views.map((view) => ({
|
|
191
|
+
id: view.id,
|
|
192
|
+
capabilities: capabilities(view.spec.type),
|
|
193
|
+
...view.spec.data ? { profile: profileData(view.spec.data) } : { inheritsSharedData: true }
|
|
194
|
+
}))
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function renderChartHandler(args) {
|
|
198
|
+
return renderOrCritique(args, true);
|
|
199
|
+
}
|
|
200
|
+
function critiqueChartHandler(args) {
|
|
201
|
+
return renderOrCritique(args, false);
|
|
202
|
+
}
|
|
203
|
+
function renderOrCritique(args, includeImage) {
|
|
204
|
+
const { spec, width, height, dpr, repair = true, repairLevel, quality } = args;
|
|
205
|
+
let working;
|
|
206
|
+
try {
|
|
207
|
+
working = specWithParsedData(spec);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return parseErrorResult(e, quality);
|
|
210
|
+
}
|
|
211
|
+
const prepared = draft(working, { repair, repairLevel, quality });
|
|
212
|
+
const type = specType(prepared.spec);
|
|
213
|
+
if (!prepared.valid) {
|
|
214
|
+
const payload = {
|
|
215
|
+
ok: false,
|
|
216
|
+
rendered: false,
|
|
217
|
+
stage: "validate",
|
|
218
|
+
type,
|
|
219
|
+
errors: prepared.errors.map(tidyError),
|
|
220
|
+
lint: prepared.warnings.map(tidyError),
|
|
221
|
+
repairsApplied: prepared.patches,
|
|
222
|
+
repairDetails: prepared.repairs,
|
|
223
|
+
...quality ? { effectiveSpec: prepared.spec, quality: prepared.quality } : {},
|
|
224
|
+
hint: `Apply each error.fix JSON Patch (or the repair_chart tool), then call ${includeImage ? "render_chart" : "critique_chart"} again. See the graphein://agent-guide and graphein://schema resources.`
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
isError: true,
|
|
228
|
+
content: [json(payload)],
|
|
229
|
+
structuredContent: payload
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const rendered = renderChart(prepared.spec, { width, height, dpr, quality });
|
|
234
|
+
const { report } = rendered;
|
|
235
|
+
const payload = {
|
|
236
|
+
ok: report.ok,
|
|
237
|
+
rendered: true,
|
|
238
|
+
type,
|
|
239
|
+
pixelSize: { width: rendered.width, height: rendered.height },
|
|
240
|
+
summary: report.summary,
|
|
241
|
+
...includeImage ? {
|
|
242
|
+
marks: report.markCount,
|
|
243
|
+
series: report.seriesCount,
|
|
244
|
+
colors: report.colorCount,
|
|
245
|
+
diagnostics: report.diagnostics
|
|
246
|
+
} : {},
|
|
247
|
+
report,
|
|
248
|
+
lint: validateSpec(rendered.spec).warnings.map(tidyError),
|
|
249
|
+
repairsApplied: prepared.patches,
|
|
250
|
+
repairDetails: prepared.repairs,
|
|
251
|
+
...quality ? {
|
|
252
|
+
effectiveSpec: rendered.spec,
|
|
253
|
+
quality: rendered.quality,
|
|
254
|
+
authoring: authoringMetadata(rendered.spec)
|
|
255
|
+
} : {}
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
isError: false,
|
|
259
|
+
content: [
|
|
260
|
+
...includeImage ? [{ type: "image", data: rendered.png.toString("base64"), mimeType: "image/png" }] : [],
|
|
261
|
+
json(payload)
|
|
262
|
+
],
|
|
263
|
+
structuredContent: payload
|
|
264
|
+
};
|
|
265
|
+
} catch (e) {
|
|
266
|
+
const failed = e instanceof NodeRenderError ? e.result : void 0;
|
|
267
|
+
const payload = {
|
|
268
|
+
ok: false,
|
|
269
|
+
rendered: false,
|
|
270
|
+
stage: "render",
|
|
271
|
+
type,
|
|
272
|
+
message: e instanceof Error ? e.message : String(e),
|
|
273
|
+
summary: prepared.summary,
|
|
274
|
+
repairsApplied: prepared.patches,
|
|
275
|
+
repairDetails: prepared.repairs,
|
|
276
|
+
...quality ? {
|
|
277
|
+
effectiveSpec: failed?.spec ?? prepared.spec,
|
|
278
|
+
quality: failed?.quality ?? { ...prepared.quality, stopReason: "render-failed" }
|
|
279
|
+
} : {}
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
isError: true,
|
|
283
|
+
content: [json(payload)],
|
|
284
|
+
structuredContent: payload
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function validateChartHandler(args) {
|
|
289
|
+
let spec;
|
|
290
|
+
try {
|
|
291
|
+
spec = specWithParsedData(args.spec);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
return parseErrorResult(e);
|
|
294
|
+
}
|
|
295
|
+
const result = validateSpec(spec);
|
|
296
|
+
return {
|
|
297
|
+
isError: false,
|
|
298
|
+
content: [
|
|
299
|
+
json({
|
|
300
|
+
valid: result.valid,
|
|
301
|
+
type: specType(spec),
|
|
302
|
+
errors: result.errors.map(tidyError),
|
|
303
|
+
warnings: result.warnings.map(tidyError)
|
|
304
|
+
})
|
|
305
|
+
]
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function recommendChartHandler(args) {
|
|
309
|
+
if (!isRecommendIntent(args.intent)) {
|
|
310
|
+
return {
|
|
311
|
+
isError: true,
|
|
312
|
+
content: [
|
|
313
|
+
json({
|
|
314
|
+
ok: false,
|
|
315
|
+
message: "Unsupported intent. Expected trend, comparison, distribution, relationship, or composition."
|
|
316
|
+
})
|
|
317
|
+
]
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
let data;
|
|
321
|
+
try {
|
|
322
|
+
data = rowsWithParsedData(args.data);
|
|
323
|
+
} catch (e) {
|
|
324
|
+
return parseErrorResult(e);
|
|
325
|
+
}
|
|
326
|
+
const recommendations = recommendChart(data, {
|
|
327
|
+
intent: args.intent,
|
|
328
|
+
maxResults: args.maxResults,
|
|
329
|
+
targetSize: args.targetSize,
|
|
330
|
+
detailed: true
|
|
331
|
+
});
|
|
332
|
+
const payload = { ok: true, ...recommendations };
|
|
333
|
+
return { isError: false, content: [json(payload)], structuredContent: payload };
|
|
334
|
+
}
|
|
335
|
+
function listChartTypesHandler() {
|
|
336
|
+
const payload = { ok: true, chartTypes: listChartTypes() };
|
|
337
|
+
return { isError: false, content: [json(payload)], structuredContent: payload };
|
|
338
|
+
}
|
|
339
|
+
function repairChartHandler(args) {
|
|
340
|
+
let input;
|
|
341
|
+
try {
|
|
342
|
+
input = specWithParsedData(args.spec);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
return parseErrorResult(e);
|
|
345
|
+
}
|
|
346
|
+
const { spec, applied, appliedDetails, remaining } = repairSpec(input, { level: args.level ?? "safe" });
|
|
347
|
+
return {
|
|
348
|
+
isError: false,
|
|
349
|
+
content: [
|
|
350
|
+
json({
|
|
351
|
+
valid: remaining.length === 0,
|
|
352
|
+
level: args.level ?? "safe",
|
|
353
|
+
applied,
|
|
354
|
+
appliedDetails,
|
|
355
|
+
remaining: remaining.map(tidyError),
|
|
356
|
+
spec
|
|
357
|
+
})
|
|
358
|
+
]
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function summarizeChartHandler(args) {
|
|
362
|
+
let spec;
|
|
363
|
+
try {
|
|
364
|
+
spec = specWithParsedData(args.spec);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
return parseErrorResult(e);
|
|
367
|
+
}
|
|
368
|
+
const result = validateSpec(spec);
|
|
369
|
+
if (!result.valid) {
|
|
370
|
+
return {
|
|
371
|
+
isError: true,
|
|
372
|
+
content: [
|
|
373
|
+
json({
|
|
374
|
+
summary: null,
|
|
375
|
+
reason: "Spec is invalid; fix it first (validate_chart / repair_chart).",
|
|
376
|
+
errors: result.errors.map(tidyError)
|
|
377
|
+
})
|
|
378
|
+
]
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const validSpec = spec;
|
|
382
|
+
const summary = validSpec.type === "dashboard" ? void 0 : summarize(validSpec);
|
|
383
|
+
return {
|
|
384
|
+
isError: false,
|
|
385
|
+
content: [
|
|
386
|
+
text(summary || `(No narrative summary is available for a '${specType(args.spec)}' chart.)`)
|
|
387
|
+
]
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/resources.ts
|
|
392
|
+
import { readFileSync } from "fs";
|
|
393
|
+
import { fileURLToPath } from "url";
|
|
394
|
+
var RESOURCES = [
|
|
395
|
+
{
|
|
396
|
+
name: "schema",
|
|
397
|
+
uri: "graphein://schema",
|
|
398
|
+
title: "Graphein ChartSpec JSON Schema",
|
|
399
|
+
description: "The machine-readable JSON Schema for every Graphein ChartSpec and DashboardSpec field \u2014 chart types, channels, transforms, annotations, and required properties. Generate or check a spec against this.",
|
|
400
|
+
mimeType: "application/json",
|
|
401
|
+
file: "chart-spec.schema.json"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "agent-guide",
|
|
405
|
+
uri: "graphein://agent-guide",
|
|
406
|
+
title: "Graphein Agent Guide",
|
|
407
|
+
description: "A task-oriented guide for producing correct Graphein charts: the one-rule workflow, choosing a chart type, encodings, transforms, the validate \u2192 repair \u2192 render \u2192 critique loop, and worked recipes. Read this first.",
|
|
408
|
+
mimeType: "text/markdown",
|
|
409
|
+
file: "agent-guide.md"
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "spec-reference",
|
|
413
|
+
uri: "graphein://spec-reference",
|
|
414
|
+
title: "Graphein Spec Reference",
|
|
415
|
+
description: "The exhaustive field-by-field reference for every chart type, channel, transform, annotation, and modifier. Consult this for the precise shape of a specific field.",
|
|
416
|
+
mimeType: "text/markdown",
|
|
417
|
+
file: "spec-reference.md"
|
|
418
|
+
}
|
|
419
|
+
];
|
|
420
|
+
var cache = /* @__PURE__ */ new Map();
|
|
421
|
+
function readResourceFile(file) {
|
|
422
|
+
const hit = cache.get(file);
|
|
423
|
+
if (hit !== void 0) return hit;
|
|
424
|
+
const url = new URL(`../resources/${file}`, import.meta.url);
|
|
425
|
+
const text2 = readFileSync(fileURLToPath(url), "utf8");
|
|
426
|
+
cache.set(file, text2);
|
|
427
|
+
return text2;
|
|
428
|
+
}
|
|
429
|
+
function resourceByUri(uri) {
|
|
430
|
+
return RESOURCES.find((r) => r.uri === uri);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/create-server.ts
|
|
434
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
435
|
+
import { z } from "zod";
|
|
436
|
+
var VERSION = "0.3.0";
|
|
437
|
+
var SERVER_INSTRUCTIONS = `Graphein is an agent-first data-visualization library: you describe a chart as one JSON ChartSpec ({ type, data, encoding, ... }) and it renders. This server lets you build correct charts without prior knowledge of the API.
|
|
438
|
+
|
|
439
|
+
Workflow:
|
|
440
|
+
1. Call list_chart_types to see every supported chart type with required channels, field roles, aggregation behavior, cardinality caveats, and starter specs; read graphein://agent-guide (and graphein://schema for exact fields) only when you need deeper detail.
|
|
441
|
+
2. Shape data as tidy rows \u2014 one row per observation, one column per variable. Tools that accept rows also accept CSV/TSV text with a header row.
|
|
442
|
+
3. Use recommend_chart when you want ranked specs from data, or start from a list_chart_types starter. Inspect profile, findings, and status: needs-decision/no-candidate are not permission to guess aggregation, date roles, missing values, or geometry.
|
|
443
|
+
4. Emit a ChartSpec and call critique_chart for report-only diagnostics, or render_chart when you need the PNG plus critique. Both validate, auto-repair safe mistakes, render headlessly once by default, and return lint/report feedback. Opt in with quality:true for presentation-default improvements within three TOTAL whole-visual renders, including restoration. Read effectiveSpec, quality history and unresolved diagnostics; the loop never changes data or explicit settings.
|
|
444
|
+
5. If a spec is invalid, validate_chart, critique_chart, and render_chart return structured errors with JSON-Patch fixes. Apply them or call repair_chart; use repair_chart level "data" to infer missing encodings or close field-name typos from the spec's own data.
|
|
445
|
+
Use summarize_chart for deterministic alt-text. Every type rasterizes headlessly, including kpi, table, matrix, slicers and dashboard (static canvas snapshots).`;
|
|
446
|
+
var specSchema = z.record(z.string(), z.unknown()).describe(
|
|
447
|
+
'A Graphein ChartSpec or DashboardSpec object, e.g. { "type": "line", "data": [...], "encoding": {...} }. Any data property may be tidy rows or CSV/TSV text with a header row. See the graphein://schema and graphein://agent-guide resources.'
|
|
448
|
+
);
|
|
449
|
+
var dataRowsSchema = z.union([
|
|
450
|
+
z.array(z.record(z.string(), z.any())),
|
|
451
|
+
z.string().describe("CSV or TSV text with a header row; quoted delimiters, quotes, and newlines are supported.")
|
|
452
|
+
]);
|
|
453
|
+
var intentSchema = z.enum(["trend", "comparison", "distribution", "relationship", "composition"]).optional().describe(
|
|
454
|
+
"Optional analytical intent used to rank recommendations: trend, comparison, distribution, relationship, or composition."
|
|
455
|
+
);
|
|
456
|
+
var qualitySchema = z.union([
|
|
457
|
+
z.boolean(),
|
|
458
|
+
z.object({
|
|
459
|
+
maxPasses: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional().describe("Total whole-visual render ceiling including initial and rollback passes (default 3).")
|
|
460
|
+
}).strict()
|
|
461
|
+
]).optional().describe(
|
|
462
|
+
"Opt in to bounded presentation-default improvement. Default false: one render. Never alters data, encodings, explicit palettes/domains, or other caller settings. Returns effectiveSpec and quality history. Mutable targets reserve a rollback pass."
|
|
463
|
+
);
|
|
464
|
+
var repairLevelSchema = z.enum(["safe", "data"]).optional().describe("Independent repair policy: safe (default), or explicitly opt in to data-aware field/encoding inference.");
|
|
465
|
+
function createServer() {
|
|
466
|
+
const server = new McpServer(
|
|
467
|
+
{ name: "graphein-mcp", version: VERSION },
|
|
468
|
+
{ instructions: SERVER_INSTRUCTIONS }
|
|
469
|
+
);
|
|
470
|
+
server.registerTool(
|
|
471
|
+
"render_chart",
|
|
472
|
+
{
|
|
473
|
+
title: "Render a Graphein chart",
|
|
474
|
+
description: "The one-call loop: validate a ChartSpec, auto-repair safe mistakes, render it to a PNG, and return the image plus a vision-free critique (render report, lint warnings, repairs applied). If the spec cannot be made valid, returns structured errors with JSON-Patch fixes instead of an image. Every type rasterizes headlessly \u2014 kpi, table, matrix, slicers and dashboard render static canvas snapshots.",
|
|
475
|
+
inputSchema: {
|
|
476
|
+
spec: specSchema,
|
|
477
|
+
width: z.number().int().positive().optional().describe("Logical width in CSS px (default 800)."),
|
|
478
|
+
height: z.number().int().positive().optional().describe("Logical height in CSS px (default 500)."),
|
|
479
|
+
dpr: z.number().positive().optional().describe("Device pixel ratio for crisp output (default 2)."),
|
|
480
|
+
repair: z.boolean().optional().describe("Auto-apply safe repairs before rendering when the spec is invalid (default true)."),
|
|
481
|
+
repairLevel: repairLevelSchema,
|
|
482
|
+
quality: qualitySchema
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
async (args) => renderChartHandler(args)
|
|
486
|
+
);
|
|
487
|
+
server.registerTool(
|
|
488
|
+
"critique_chart",
|
|
489
|
+
{
|
|
490
|
+
title: "Critique a Graphein chart without an image",
|
|
491
|
+
description: "Validate a ChartSpec, auto-repair safe mistakes, render it headlessly, and return only the RenderReport, deterministic summary, lint warnings, and repairs applied \u2014 no base64 PNG. Use this when you need diagnostics but not an image.",
|
|
492
|
+
inputSchema: {
|
|
493
|
+
spec: specSchema,
|
|
494
|
+
width: z.number().int().positive().optional().describe("Logical width in CSS px (default 800)."),
|
|
495
|
+
height: z.number().int().positive().optional().describe("Logical height in CSS px (default 500)."),
|
|
496
|
+
dpr: z.number().positive().optional().describe("Device pixel ratio for layout parity (default 2)."),
|
|
497
|
+
repair: z.boolean().optional().describe("Auto-apply safe repairs before rendering when the spec is invalid (default true)."),
|
|
498
|
+
repairLevel: repairLevelSchema,
|
|
499
|
+
quality: qualitySchema
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
async (args) => critiqueChartHandler(args)
|
|
503
|
+
);
|
|
504
|
+
server.registerTool(
|
|
505
|
+
"validate_chart",
|
|
506
|
+
{
|
|
507
|
+
title: "Validate a Graphein chart spec",
|
|
508
|
+
description: 'Validate a ChartSpec without rendering. Returns structural errors (each with a JSON-Patch `fix` when unambiguous, plus "did you mean" suggestions) and best-practice lint warnings. Fast feedback before rendering.',
|
|
509
|
+
inputSchema: { spec: specSchema }
|
|
510
|
+
},
|
|
511
|
+
async (args) => validateChartHandler(args)
|
|
512
|
+
);
|
|
513
|
+
server.registerTool(
|
|
514
|
+
"recommend_chart",
|
|
515
|
+
{
|
|
516
|
+
title: "Recommend Graphein chart specs",
|
|
517
|
+
description: "Profile tidy rows and return ranked ChartSpecs with rationale, profile evidence, alternatives, and explicit needs-decision/no-candidate findings. Use this before guessing a chart type or encoding; no supplied data is changed.",
|
|
518
|
+
inputSchema: {
|
|
519
|
+
data: dataRowsSchema,
|
|
520
|
+
intent: intentSchema,
|
|
521
|
+
maxResults: z.number().int().positive().optional(),
|
|
522
|
+
targetSize: z.object({
|
|
523
|
+
width: z.number().positive(),
|
|
524
|
+
height: z.number().positive()
|
|
525
|
+
}).optional().describe("Optional target CSS-pixel size for layout guidance only; does not filter or group data.")
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
async (args) => recommendChartHandler(args)
|
|
529
|
+
);
|
|
530
|
+
server.registerTool(
|
|
531
|
+
"repair_chart",
|
|
532
|
+
{
|
|
533
|
+
title: "Repair a Graphein chart spec",
|
|
534
|
+
description: `Apply Graphein repairs and return the corrected spec, JSON Patch ops, per-patch rationales, and whether it is now valid. The default safe level applies validator-provided fixes; level "data" may infer missing encodings and field-name typo fixes from the spec's own data.`,
|
|
535
|
+
inputSchema: {
|
|
536
|
+
spec: specSchema,
|
|
537
|
+
level: z.enum(["safe", "data"]).optional().describe('Repair aggressiveness. "safe" is default; "data" also infers from the spec data.')
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
async (args) => repairChartHandler(args)
|
|
541
|
+
);
|
|
542
|
+
server.registerTool(
|
|
543
|
+
"list_chart_types",
|
|
544
|
+
{
|
|
545
|
+
title: "List Graphein chart types",
|
|
546
|
+
description: "Return every supported chart type with purpose, required channels/properties, field roles, aggregation semantics, cardinality guidance, supported features, and a minimal runnable starter. Use this before reading the full schema.",
|
|
547
|
+
inputSchema: {}
|
|
548
|
+
},
|
|
549
|
+
async () => listChartTypesHandler()
|
|
550
|
+
);
|
|
551
|
+
server.registerTool(
|
|
552
|
+
"summarize_chart",
|
|
553
|
+
{
|
|
554
|
+
title: "Summarize a Graphein chart",
|
|
555
|
+
description: `Return a deterministic, plain-English description of what the chart's data shows (e.g. "Users grew 46% over six months, peaking in June"). Doubles as alt-text; needs no LLM.`,
|
|
556
|
+
inputSchema: { spec: specSchema }
|
|
557
|
+
},
|
|
558
|
+
async (args) => summarizeChartHandler(args)
|
|
559
|
+
);
|
|
560
|
+
for (const r of RESOURCES) {
|
|
561
|
+
server.registerResource(
|
|
562
|
+
r.name,
|
|
563
|
+
r.uri,
|
|
564
|
+
{ title: r.title, description: r.description, mimeType: r.mimeType },
|
|
565
|
+
async (uri) => ({
|
|
566
|
+
contents: [{ uri: uri.href, mimeType: r.mimeType, text: readResourceFile(r.file) }]
|
|
567
|
+
})
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
server.registerPrompt(
|
|
571
|
+
"create_chart",
|
|
572
|
+
{
|
|
573
|
+
title: "Create a Graphein chart",
|
|
574
|
+
description: "Scaffold the workflow for building a validated Graphein chart from a goal (and optional data).",
|
|
575
|
+
argsSchema: {
|
|
576
|
+
goal: z.string().describe('What the chart should show, e.g. "monthly active users over the last year".'),
|
|
577
|
+
data: z.string().optional().describe("Optional: the data as a JSON array, or a description of the columns available.")
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
({ goal, data }) => ({
|
|
581
|
+
messages: [
|
|
582
|
+
{
|
|
583
|
+
role: "user",
|
|
584
|
+
content: {
|
|
585
|
+
type: "text",
|
|
586
|
+
text: `Build a Graphein chart for this goal:
|
|
587
|
+
|
|
588
|
+
${goal}
|
|
589
|
+
${data ? `
|
|
590
|
+
Data:
|
|
591
|
+
${data}
|
|
592
|
+
` : ""}
|
|
593
|
+
Steps:
|
|
594
|
+
1. Call list_chart_types for available chart families and starter specs; read graphein://agent-guide or graphein://schema only for deeper details.
|
|
595
|
+
2. Shape the data as tidy rows \u2014 one row per observation, one column per variable. CSV/TSV text with a header row is accepted anywhere data rows are accepted.
|
|
596
|
+
3. Choose a chart type (or call recommend_chart) and write a single ChartSpec ({ type, data, encoding, title }).
|
|
597
|
+
4. Call critique_chart for report-only feedback or render_chart when you need the PNG too. Read the returned render report + lint to confirm it looks right.
|
|
598
|
+
5. If it reports errors, apply each error.fix patch (or call repair_chart, using level "data" when field/encoding inference from data is useful) and render again \u2014 do not regenerate from scratch.`
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
]
|
|
602
|
+
})
|
|
603
|
+
);
|
|
604
|
+
return server;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export {
|
|
608
|
+
TabularParseError,
|
|
609
|
+
parseTabularText,
|
|
610
|
+
parseInlineData,
|
|
611
|
+
renderChartHandler,
|
|
612
|
+
critiqueChartHandler,
|
|
613
|
+
validateChartHandler,
|
|
614
|
+
recommendChartHandler,
|
|
615
|
+
listChartTypesHandler,
|
|
616
|
+
repairChartHandler,
|
|
617
|
+
summarizeChartHandler,
|
|
618
|
+
RESOURCES,
|
|
619
|
+
readResourceFile,
|
|
620
|
+
resourceByUri,
|
|
621
|
+
VERSION,
|
|
622
|
+
createServer
|
|
623
|
+
};
|
|
624
|
+
//# sourceMappingURL=chunk-CODCFNDV.js.map
|