semiotic 3.1.0 → 3.1.2
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 +104 -0
- package/ai/dist/componentRegistry.js +5 -0
- package/ai/dist/mcp-server.js +378 -46
- package/ai/dist/renderHOCToSVG.js +5 -3
- package/ai/schema.json +1 -1
- package/dist/components/charts/shared/ChartError.d.ts +3 -1
- package/dist/components/charts/shared/withChartWrapper.d.ts +4 -3
- package/dist/geo.min.js +1 -1
- package/dist/geo.module.min.js +1 -1
- package/dist/network.min.js +1 -1
- package/dist/network.module.min.js +1 -1
- package/dist/ordinal.min.js +1 -1
- package/dist/ordinal.module.min.js +1 -1
- package/dist/realtime.min.js +1 -1
- package/dist/realtime.module.min.js +1 -1
- package/dist/semiotic-ai-statisticalOverlays-C1f7TYyD.js +1 -0
- package/dist/semiotic-ai.min.js +1 -1
- package/dist/semiotic-ai.module.min.js +1 -1
- package/dist/semiotic-statisticalOverlays-C1f7TYyD.js +1 -0
- package/dist/semiotic.min.js +1 -1
- package/dist/semiotic.module.min.js +1 -1
- package/dist/xy-statisticalOverlays-C1f7TYyD.js +1 -0
- package/dist/xy.min.js +1 -1
- package/dist/xy.module.min.js +1 -1
- package/package.json +50 -4
package/README.md
CHANGED
|
@@ -333,6 +333,110 @@ const svg = renderToStaticSVG("xy", {
|
|
|
333
333
|
})
|
|
334
334
|
```
|
|
335
335
|
|
|
336
|
+
## MCP Server
|
|
337
|
+
|
|
338
|
+
Semiotic ships with an [MCP server](https://modelcontextprotocol.io) that lets AI coding assistants render charts, diagnose configuration problems, discover schemas, and get chart recommendations via tool calls.
|
|
339
|
+
|
|
340
|
+
### Setup
|
|
341
|
+
|
|
342
|
+
Add to your MCP client config (e.g. `claude_desktop_config.json` for Claude Desktop):
|
|
343
|
+
|
|
344
|
+
```json
|
|
345
|
+
{
|
|
346
|
+
"mcpServers": {
|
|
347
|
+
"semiotic": {
|
|
348
|
+
"command": "npx",
|
|
349
|
+
"args": ["semiotic-mcp"]
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
No API keys or authentication required. The server runs locally via stdio.
|
|
356
|
+
|
|
357
|
+
### Tools
|
|
358
|
+
|
|
359
|
+
| Tool | Description |
|
|
360
|
+
|------|-------------|
|
|
361
|
+
| **`renderChart`** | Render a Semiotic chart to static SVG. Supports the components returned by `getSchema` that are marked `[renderable]`. Pass `{ component: "LineChart", props: { data: [...], xAccessor: "x", yAccessor: "y" } }`. Returns SVG string or validation errors with fix suggestions. |
|
|
362
|
+
| **`getSchema`** | Return the prop schema for a specific component. Pass `{ component: "LineChart" }` to get its props, or omit `component` to list all 30 chart types. Use before `renderChart` to look up valid props. |
|
|
363
|
+
| **`suggestChart`** | Recommend chart types for a data sample. Pass `{ data: [{...}, ...] }` with 1–5 sample objects. Optionally include `intent` (`"comparison"`, `"trend"`, `"distribution"`, `"relationship"`, `"composition"`, `"geographic"`, `"network"`, `"hierarchy"`). Returns ranked suggestions with example props. |
|
|
364
|
+
| **`diagnoseConfig`** | Check a chart configuration for common problems — empty data, bad dimensions, missing accessors, wrong data shape, and more. Returns a human-readable diagnostic report with actionable fixes. |
|
|
365
|
+
| **`reportIssue`** | Generate a pre-filled GitHub issue URL for bug reports or feature requests. Pass `{ title: "...", body: "...", labels: ["bug"] }`. Returns a URL the user can open to submit. |
|
|
366
|
+
|
|
367
|
+
### Example: get schema for a component
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
Tool: getSchema
|
|
371
|
+
Args: { "component": "LineChart" }
|
|
372
|
+
→ Returns: { "name": "LineChart", "description": "...", "parameters": { "properties": { "data": ..., "xAccessor": ..., ... } } }
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Example: suggest a chart for your data
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
Tool: suggestChart
|
|
379
|
+
Args: {
|
|
380
|
+
"data": [
|
|
381
|
+
{ "month": "Jan", "revenue": 120, "region": "East" },
|
|
382
|
+
{ "month": "Feb", "revenue": 180, "region": "West" }
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
→ Returns:
|
|
386
|
+
1. BarChart (high confidence) — categorical field (region) with values (revenue)
|
|
387
|
+
2. StackedBarChart (medium confidence) — two categorical fields (month, region)
|
|
388
|
+
3. DonutChart (medium confidence) — 2 categories — proportional composition
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Example: render a chart
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
Tool: renderChart
|
|
395
|
+
Args: {
|
|
396
|
+
"component": "BarChart",
|
|
397
|
+
"props": {
|
|
398
|
+
"data": [
|
|
399
|
+
{ "category": "Q1", "revenue": 120 },
|
|
400
|
+
{ "category": "Q2", "revenue": 180 },
|
|
401
|
+
{ "category": "Q3", "revenue": 150 }
|
|
402
|
+
],
|
|
403
|
+
"categoryAccessor": "category",
|
|
404
|
+
"valueAccessor": "revenue"
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
→ Returns: <svg>...</svg>
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Example: diagnose a broken config
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
Tool: diagnoseConfig
|
|
414
|
+
Args: { "component": "LineChart", "props": { "data": [] } }
|
|
415
|
+
→ Returns: ✗ [EMPTY_DATA] data is an empty array — Fix: provide at least one data point
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Example: report an issue
|
|
419
|
+
|
|
420
|
+
```
|
|
421
|
+
Tool: reportIssue
|
|
422
|
+
Args: {
|
|
423
|
+
"title": "Bug: BarChart tooltip shows undefined for custom accessor",
|
|
424
|
+
"body": "When using valueAccessor='amount', tooltip displays 'undefined'.\n\ndiagnoseConfig output: ✓ no issues detected.",
|
|
425
|
+
"labels": ["bug"]
|
|
426
|
+
}
|
|
427
|
+
→ Returns: Open this URL to submit the issue: https://github.com/nteract/semiotic/issues/new?...
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### CLI alternative
|
|
431
|
+
|
|
432
|
+
For quick validation without an MCP client:
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
npx semiotic-ai --doctor # validate component + props JSON
|
|
436
|
+
npx semiotic-ai --schema # dump all chart schemas
|
|
437
|
+
npx semiotic-ai --compact # compact schema (fewer tokens)
|
|
438
|
+
```
|
|
439
|
+
|
|
336
440
|
## Documentation
|
|
337
441
|
|
|
338
442
|
[Interactive docs and examples](https://semiotic.nteract.io)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.COMPONENT_REGISTRY = void 0;
|
|
4
4
|
const ai_1 = require("semiotic/ai");
|
|
5
|
+
const geo_1 = require("semiotic/geo");
|
|
5
6
|
exports.COMPONENT_REGISTRY = {
|
|
6
7
|
LineChart: { component: ai_1.LineChart, category: "xy" },
|
|
7
8
|
AreaChart: { component: ai_1.AreaChart, category: "xy" },
|
|
@@ -25,4 +26,8 @@ exports.COMPONENT_REGISTRY = {
|
|
|
25
26
|
Treemap: { component: ai_1.Treemap, category: "network" },
|
|
26
27
|
CirclePack: { component: ai_1.CirclePack, category: "network" },
|
|
27
28
|
OrbitDiagram: { component: ai_1.OrbitDiagram, category: "network" },
|
|
29
|
+
ChoroplethMap: { component: geo_1.ChoroplethMap, category: "geo" },
|
|
30
|
+
ProportionalSymbolMap: { component: geo_1.ProportionalSymbolMap, category: "geo" },
|
|
31
|
+
FlowMap: { component: geo_1.FlowMap, category: "geo" },
|
|
32
|
+
DistanceCartogram: { component: geo_1.DistanceCartogram, category: "geo" },
|
|
28
33
|
};
|
package/ai/dist/mcp-server.js
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Semiotic MCP Server
|
|
5
5
|
*
|
|
6
|
-
* Exposes
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Exposes five tools:
|
|
7
|
+
* 1. renderChart — renders any HOC chart to static SVG
|
|
8
|
+
* 2. diagnoseConfig — anti-pattern detector for chart configurations
|
|
9
|
+
* 3. reportIssue — generates a pre-filled GitHub issue URL for bugs/features
|
|
10
|
+
* 4. getSchema — returns the prop schema for a specific component
|
|
11
|
+
* 5. suggestChart — recommends chart types for a given data shape
|
|
9
12
|
*
|
|
10
13
|
* Usage (Claude Desktop / claude_desktop_config.json):
|
|
11
14
|
* {
|
|
@@ -16,6 +19,9 @@
|
|
|
16
19
|
* }
|
|
17
20
|
* }
|
|
18
21
|
* }
|
|
22
|
+
*
|
|
23
|
+
* HTTP mode (for remote inspectors / web clients):
|
|
24
|
+
* npx semiotic-mcp --http --port 3001
|
|
19
25
|
*/
|
|
20
26
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
27
|
if (k2 === undefined) k2 = k;
|
|
@@ -53,51 +59,268 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
53
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
60
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
55
61
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
62
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
63
|
+
const zod_1 = require("zod");
|
|
56
64
|
const fs = __importStar(require("fs"));
|
|
57
65
|
const path = __importStar(require("path"));
|
|
66
|
+
const http = __importStar(require("http"));
|
|
58
67
|
const renderHOCToSVG_1 = require("./renderHOCToSVG");
|
|
59
68
|
const componentRegistry_1 = require("./componentRegistry");
|
|
60
69
|
const ai_1 = require("semiotic/ai");
|
|
61
|
-
// Load schema.json for
|
|
70
|
+
// Load schema.json for version info
|
|
62
71
|
const schemaPath = path.resolve(__dirname, "../schema.json");
|
|
63
72
|
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
|
|
64
|
-
// Build
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
73
|
+
// Build component name → schema lookup from schema.json
|
|
74
|
+
const schemaByComponent = {};
|
|
75
|
+
for (const tool of schema.tools) {
|
|
76
|
+
schemaByComponent[tool.function.name] = tool.function;
|
|
77
|
+
}
|
|
78
|
+
const componentNames = Object.keys(componentRegistry_1.COMPONENT_REGISTRY).sort();
|
|
79
|
+
const REPO = "nteract/semiotic";
|
|
80
|
+
async function getSchemaHandler(args) {
|
|
81
|
+
const component = args.component;
|
|
82
|
+
if (!component) {
|
|
83
|
+
const all = Object.keys(schemaByComponent).sort();
|
|
84
|
+
const renderable = new Set(Object.keys(componentRegistry_1.COMPONENT_REGISTRY));
|
|
85
|
+
const list = all.map(name => renderable.has(name) ? `${name} [renderable]` : name);
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: "text", text: `Available components (${all.length}):\n${list.join(", ")}\n\nComponents marked [renderable] can be rendered to SVG via renderChart. Others (Realtime*) require a browser environment.\n\nPass { component: '<name>' } to get the prop schema for a specific component.` }],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const entry = schemaByComponent[component];
|
|
91
|
+
if (!entry) {
|
|
92
|
+
const available = Object.keys(schemaByComponent).sort();
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text: `Unknown component "${component}". Available: ${available.join(", ")}` }],
|
|
95
|
+
isError: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const renderableNote = componentRegistry_1.COMPONENT_REGISTRY[component] ? "This component can be rendered to SVG via renderChart." : "This component requires a browser environment and cannot be rendered via renderChart.";
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: `${renderableNote}\n\n${JSON.stringify(entry, null, 2)}` }],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function suggestChartHandler(args) {
|
|
104
|
+
const data = args.data;
|
|
105
|
+
const intent = args.intent;
|
|
106
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: "Pass { data: [{ ... }, ...] } with 1-5 sample data objects. Optionally include intent: 'comparison' | 'trend' | 'distribution' | 'relationship' | 'composition' | 'geographic' | 'network' | 'hierarchy'." }],
|
|
109
|
+
isError: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const sample = data[0];
|
|
113
|
+
if (!sample || typeof sample !== "object") {
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: "Data items must be objects with key-value pairs." }],
|
|
116
|
+
isError: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const keys = Object.keys(sample);
|
|
120
|
+
const suggestions = [];
|
|
121
|
+
// Classify fields
|
|
122
|
+
const numericFields = [];
|
|
123
|
+
const stringFields = [];
|
|
124
|
+
const dateFields = [];
|
|
125
|
+
const geoFields = {};
|
|
126
|
+
const networkFields = {};
|
|
127
|
+
const hierarchyFields = {};
|
|
128
|
+
for (const key of keys) {
|
|
129
|
+
const values = data.map(d => d[key]).filter(v => v != null);
|
|
130
|
+
if (values.length === 0)
|
|
131
|
+
continue;
|
|
132
|
+
const first = values[0];
|
|
133
|
+
if (typeof first === "number") {
|
|
134
|
+
numericFields.push(key);
|
|
135
|
+
}
|
|
136
|
+
else if (typeof first === "string") {
|
|
137
|
+
if (/^\d{4}[-/]\d{2}/.test(first) && !isNaN(Date.parse(first))) {
|
|
138
|
+
dateFields.push(key);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
stringFields.push(key);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const kl = key.toLowerCase();
|
|
145
|
+
if (kl === "lat" || kl === "latitude")
|
|
146
|
+
geoFields.lat = key;
|
|
147
|
+
if (kl === "lon" || kl === "lng" || kl === "longitude")
|
|
148
|
+
geoFields.lon = key;
|
|
149
|
+
if (kl === "source" || kl === "from")
|
|
150
|
+
networkFields.source = key;
|
|
151
|
+
if (kl === "target" || kl === "to")
|
|
152
|
+
networkFields.target = key;
|
|
153
|
+
if (kl === "value" || kl === "weight" || kl === "amount")
|
|
154
|
+
networkFields.value = key;
|
|
155
|
+
if (kl === "children" || kl === "values")
|
|
156
|
+
hierarchyFields.children = key;
|
|
157
|
+
if (kl === "parent")
|
|
158
|
+
hierarchyFields.parent = key;
|
|
159
|
+
}
|
|
160
|
+
const hasTime = dateFields.length > 0;
|
|
161
|
+
const hasCat = stringFields.length > 0;
|
|
162
|
+
const hasNum = numericFields.length > 0;
|
|
163
|
+
const hasGeo = geoFields.lat && geoFields.lon;
|
|
164
|
+
const hasNetwork = networkFields.source && networkFields.target;
|
|
165
|
+
const hasHierarchy = hierarchyFields.children || hierarchyFields.parent;
|
|
166
|
+
// Network data
|
|
167
|
+
if (hasNetwork && (!intent || intent === "network")) {
|
|
168
|
+
const src = networkFields.source;
|
|
169
|
+
const tgt = networkFields.target;
|
|
170
|
+
if (networkFields.value) {
|
|
171
|
+
suggestions.push({
|
|
172
|
+
component: "SankeyDiagram",
|
|
173
|
+
confidence: "high",
|
|
174
|
+
reason: `Data has ${src}→${tgt} with ${networkFields.value} — ideal for flow visualization`,
|
|
175
|
+
props: { edges: "data", sourceAccessor: `"${src}"`, targetAccessor: `"${tgt}"`, valueAccessor: `"${networkFields.value}"` },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
suggestions.push({
|
|
179
|
+
component: "ForceDirectedGraph",
|
|
180
|
+
confidence: networkFields.value ? "medium" : "high",
|
|
181
|
+
reason: `Data has ${src}→${tgt} edges — force layout shows network structure. Nodes are auto-inferred from edges when not provided.`,
|
|
182
|
+
props: { edges: "data", sourceAccessor: `"${src}"`, targetAccessor: `"${tgt}"` },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// Hierarchy data
|
|
186
|
+
if (hasHierarchy && (!intent || intent === "hierarchy")) {
|
|
187
|
+
suggestions.push({
|
|
188
|
+
component: "Treemap",
|
|
189
|
+
confidence: "high",
|
|
190
|
+
reason: `Data has nested ${hierarchyFields.children || "parent"} structure — treemap shows hierarchical proportions`,
|
|
191
|
+
props: { data: "rootObject", childrenAccessor: `"${hierarchyFields.children || "children"}"`, ...(numericFields[0] ? { valueAccessor: `"${numericFields[0]}"` } : {}) },
|
|
192
|
+
});
|
|
193
|
+
suggestions.push({
|
|
194
|
+
component: "TreeDiagram",
|
|
195
|
+
confidence: "medium",
|
|
196
|
+
reason: "Tree layout shows hierarchical relationships",
|
|
197
|
+
props: { data: "rootObject", childrenAccessor: `"${hierarchyFields.children || "children"}"` },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// Geographic data
|
|
201
|
+
if (hasGeo && (!intent || intent === "geographic")) {
|
|
202
|
+
const sizeField = numericFields.find(f => f !== geoFields.lat && f !== geoFields.lon);
|
|
203
|
+
suggestions.push({
|
|
204
|
+
component: "ProportionalSymbolMap",
|
|
205
|
+
confidence: "high",
|
|
206
|
+
reason: `Data has ${geoFields.lat}/${geoFields.lon} coordinates — map shows spatial distribution`,
|
|
207
|
+
props: { points: "data", xAccessor: `"${geoFields.lon}"`, yAccessor: `"${geoFields.lat}"`, ...(sizeField ? { sizeBy: `"${sizeField}"` } : {}) },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// Time series
|
|
211
|
+
if (hasTime && hasNum && (!intent || intent === "trend")) {
|
|
212
|
+
const timeField = dateFields[0];
|
|
213
|
+
const valueField = numericFields[0];
|
|
214
|
+
suggestions.push({
|
|
215
|
+
component: "LineChart",
|
|
216
|
+
confidence: "high",
|
|
217
|
+
reason: `Data has dates (${timeField}) and numeric values (${valueField}) — line chart shows trends over time`,
|
|
218
|
+
props: { data: "data", xAccessor: `"${timeField}"`, yAccessor: `"${valueField}"`, ...(hasCat ? { lineBy: `"${stringFields[0]}"`, colorBy: `"${stringFields[0]}"` } : {}) },
|
|
219
|
+
});
|
|
220
|
+
if (hasCat) {
|
|
221
|
+
suggestions.push({
|
|
222
|
+
component: "StackedAreaChart",
|
|
223
|
+
confidence: "medium",
|
|
224
|
+
reason: `Multiple categories (${stringFields[0]}) over time — stacked area shows composition trends`,
|
|
225
|
+
props: { data: "data", xAccessor: `"${timeField}"`, yAccessor: `"${valueField}"`, areaBy: `"${stringFields[0]}"`, colorBy: `"${stringFields[0]}"` },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Categorical + numeric
|
|
230
|
+
if (hasCat && hasNum && (!intent || intent === "comparison" || intent === "composition" || intent === "distribution")) {
|
|
231
|
+
const catField = stringFields[0];
|
|
232
|
+
const valField = numericFields[0];
|
|
233
|
+
if (!intent || intent === "comparison") {
|
|
234
|
+
suggestions.push({
|
|
235
|
+
component: "BarChart",
|
|
236
|
+
confidence: hasTime ? "medium" : "high",
|
|
237
|
+
reason: `Categorical field (${catField}) with values (${valField}) — bar chart for comparison`,
|
|
238
|
+
props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"` },
|
|
239
|
+
});
|
|
86
240
|
}
|
|
241
|
+
if (stringFields.length >= 2 && (!intent || intent === "composition")) {
|
|
242
|
+
suggestions.push({
|
|
243
|
+
component: "StackedBarChart",
|
|
244
|
+
confidence: "medium",
|
|
245
|
+
reason: `Two categorical fields (${stringFields.join(", ")}) — stacked bar shows composition within categories`,
|
|
246
|
+
props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"`, stackBy: `"${stringFields[1]}"` },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (!intent || intent === "distribution") {
|
|
250
|
+
suggestions.push({
|
|
251
|
+
component: "Histogram",
|
|
252
|
+
confidence: "medium",
|
|
253
|
+
reason: `Numeric distribution of ${valField} — histogram shows value spread`,
|
|
254
|
+
props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"` },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (!intent || intent === "composition") {
|
|
258
|
+
const uniqueCats = new Set(data.map(d => d[catField])).size;
|
|
259
|
+
if (uniqueCats <= 8) {
|
|
260
|
+
suggestions.push({
|
|
261
|
+
component: "DonutChart",
|
|
262
|
+
confidence: "medium",
|
|
263
|
+
reason: `${uniqueCats} categories — donut chart shows proportional composition`,
|
|
264
|
+
props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"` },
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Two numeric fields → scatterplot
|
|
270
|
+
if (numericFields.length >= 2 && (!intent || intent === "relationship")) {
|
|
271
|
+
const xField = numericFields[0];
|
|
272
|
+
const yField = numericFields[1];
|
|
273
|
+
suggestions.push({
|
|
274
|
+
component: "Scatterplot",
|
|
275
|
+
confidence: "high",
|
|
276
|
+
reason: `Two numeric fields (${xField}, ${yField}) — scatterplot shows relationships`,
|
|
277
|
+
props: { data: "data", xAccessor: `"${xField}"`, yAccessor: `"${yField}"`, ...(hasCat ? { colorBy: `"${stringFields[0]}"` } : {}), ...(numericFields[2] ? { sizeBy: `"${numericFields[2]}"` } : {}) },
|
|
278
|
+
});
|
|
279
|
+
if (numericFields.length >= 3) {
|
|
280
|
+
suggestions.push({
|
|
281
|
+
component: "BubbleChart",
|
|
282
|
+
confidence: "medium",
|
|
283
|
+
reason: `Three numeric fields — bubble chart adds size dimension to scatter`,
|
|
284
|
+
props: { data: "data", xAccessor: `"${xField}"`, yAccessor: `"${yField}"`, sizeBy: `"${numericFields[2]}"` },
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (numericFields.length >= 2 && hasCat) {
|
|
288
|
+
suggestions.push({
|
|
289
|
+
component: "Heatmap",
|
|
290
|
+
confidence: "medium",
|
|
291
|
+
reason: `Numeric values across dimensions — heatmap shows density/intensity`,
|
|
292
|
+
props: { data: "data", xAccessor: `"${xField}"`, yAccessor: `"${hasCat ? stringFields[0] : yField}"`, valueAccessor: `"${hasCat ? numericFields[0] : numericFields[2] || yField}"` },
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Fallback
|
|
297
|
+
if (suggestions.length === 0) {
|
|
298
|
+
const fieldSummary = `Fields: ${keys.join(", ")} (${numericFields.length} numeric, ${stringFields.length} categorical, ${dateFields.length} date)`;
|
|
87
299
|
return {
|
|
88
|
-
content: [{ type: "text", text:
|
|
300
|
+
content: [{ type: "text", text: `Could not confidently recommend a chart type.\n\n${fieldSummary}\n\nTry providing intent ('comparison', 'trend', 'distribution', 'relationship', 'composition', 'geographic', 'network', 'hierarchy') to narrow recommendations, or use getSchema to browse available components.` }],
|
|
89
301
|
};
|
|
302
|
+
}
|
|
303
|
+
// Format output
|
|
304
|
+
const lines = suggestions.map((s, i) => {
|
|
305
|
+
const propsStr = Object.entries(s.props).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
306
|
+
return `${i + 1}. **${s.component}** (${s.confidence} confidence)\n ${s.reason}\n \`<${s.component} ${propsStr} />\``;
|
|
90
307
|
});
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text", text: lines.join("\n\n") }],
|
|
310
|
+
};
|
|
91
311
|
}
|
|
92
|
-
|
|
93
|
-
// Accepts { component, props } — closes the agent feedback loop by letting
|
|
94
|
-
// an LLM render any chart type in a single tool call.
|
|
95
|
-
server.tool("renderChart", "Render any Semiotic chart to static SVG. Pass { component: 'LineChart', props: { data: [...], ... } }. Returns SVG string or validation errors.", {}, async (args) => {
|
|
312
|
+
async function renderChartHandler(args) {
|
|
96
313
|
const component = args.component;
|
|
97
|
-
const props =
|
|
314
|
+
const props = args.props ?? {};
|
|
98
315
|
if (!component) {
|
|
99
316
|
return {
|
|
100
|
-
content: [{ type: "text", text:
|
|
317
|
+
content: [{ type: "text", text: `Missing 'component' field. Provide { component: '<name>', props: { ... } }. Available: ${componentNames.join(", ")}` }],
|
|
318
|
+
isError: true,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (!componentRegistry_1.COMPONENT_REGISTRY[component]) {
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: "text", text: `Unknown component "${component}". Available: ${componentNames.join(", ")}` }],
|
|
101
324
|
isError: true,
|
|
102
325
|
};
|
|
103
326
|
}
|
|
@@ -111,13 +334,10 @@ server.tool("renderChart", "Render any Semiotic chart to static SVG. Pass { comp
|
|
|
111
334
|
return {
|
|
112
335
|
content: [{ type: "text", text: result.svg }],
|
|
113
336
|
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Anti-pattern detector: checks for common failure modes and returns
|
|
117
|
-
// actionable fix instructions.
|
|
118
|
-
server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for common problems (empty data, bad dimensions, missing accessors, wrong data shape, etc). Pass { component: 'LineChart', props: { ... } }. Returns structured diagnoses with fix instructions.", {}, async (args) => {
|
|
337
|
+
}
|
|
338
|
+
async function diagnoseConfigHandler(args) {
|
|
119
339
|
const component = args.component;
|
|
120
|
-
const props =
|
|
340
|
+
const props = args.props ?? {};
|
|
121
341
|
if (!component) {
|
|
122
342
|
return {
|
|
123
343
|
content: [{ type: "text", text: "Missing 'component' field. Provide { component: 'LineChart', props: { ... } }." }],
|
|
@@ -126,13 +346,13 @@ server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for commo
|
|
|
126
346
|
}
|
|
127
347
|
const result = (0, ai_1.diagnoseConfig)(component, props);
|
|
128
348
|
if (result.ok) {
|
|
129
|
-
const warnings = result.diagnoses.filter(d => d.severity === "warning");
|
|
349
|
+
const warnings = result.diagnoses.filter((d) => d.severity === "warning");
|
|
130
350
|
const msg = warnings.length > 0
|
|
131
|
-
? `Configuration looks good with ${warnings.length} warning(s):\n${warnings.map(w => `⚠ [${w.code}] ${w.message}\n Fix: ${w.fix}`).join("\n")}`
|
|
351
|
+
? `Configuration looks good with ${warnings.length} warning(s):\n${warnings.map((w) => `⚠ [${w.code}] ${w.message}\n Fix: ${w.fix}`).join("\n")}`
|
|
132
352
|
: `✓ Configuration looks good — no issues detected.`;
|
|
133
353
|
return { content: [{ type: "text", text: msg }] };
|
|
134
354
|
}
|
|
135
|
-
const lines = result.diagnoses.map(d => {
|
|
355
|
+
const lines = result.diagnoses.map((d) => {
|
|
136
356
|
const icon = d.severity === "error" ? "✗" : "⚠";
|
|
137
357
|
const fixLine = d.fix ? `\n Fix: ${d.fix}` : "";
|
|
138
358
|
return `${icon} [${d.code}] ${d.message}${fixLine}`;
|
|
@@ -141,11 +361,123 @@ server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for commo
|
|
|
141
361
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
142
362
|
isError: true,
|
|
143
363
|
};
|
|
144
|
-
}
|
|
145
|
-
|
|
364
|
+
}
|
|
365
|
+
async function reportIssueHandler(args) {
|
|
366
|
+
const title = args.title;
|
|
367
|
+
const body = args.body;
|
|
368
|
+
const labels = args.labels;
|
|
369
|
+
if (!title) {
|
|
370
|
+
return {
|
|
371
|
+
content: [{ type: "text", text: "Missing 'title' field. Provide { title: 'Bug: ...', body: '...', labels?: ['bug'] }." }],
|
|
372
|
+
isError: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const params = new URLSearchParams();
|
|
376
|
+
params.set("title", title);
|
|
377
|
+
if (body)
|
|
378
|
+
params.set("body", body);
|
|
379
|
+
if (labels) {
|
|
380
|
+
const labelList = Array.isArray(labels) ? labels.join(",") : labels;
|
|
381
|
+
params.set("labels", labelList);
|
|
382
|
+
}
|
|
383
|
+
const url = `https://github.com/${REPO}/issues/new?${params.toString()}`;
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: "text", text: `Open this URL to submit the issue:\n\n${url}` }],
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
// ── Server factory ───────────────────────────────────────────────────────
|
|
389
|
+
// Creates a fresh McpServer with all tools registered.
|
|
390
|
+
// HTTP mode needs one instance per session (McpServer can only connect to one transport).
|
|
391
|
+
// Stdio mode uses a single instance.
|
|
392
|
+
function createServer() {
|
|
393
|
+
const srv = new mcp_js_1.McpServer({
|
|
394
|
+
name: "semiotic",
|
|
395
|
+
version: schema.version || "3.0.0",
|
|
396
|
+
});
|
|
397
|
+
srv.tool("getSchema", `Return the prop schema for a Semiotic chart component. Pass { component: '<name>' } to get its props, or omit component to list all available components. Components marked [renderable] can be passed to renderChart for static SVG output.`, { component: zod_1.z.string().optional().describe("Component name, e.g. 'LineChart'. Omit to list all.") }, getSchemaHandler);
|
|
398
|
+
srv.tool("suggestChart", "Recommend Semiotic chart types for a given data sample. Pass { data: [...] } with 1-5 sample objects. Optionally pass intent to narrow suggestions. Returns ranked recommendations with example props.", {
|
|
399
|
+
data: zod_1.z.array(zod_1.z.record(zod_1.z.string(), zod_1.z.unknown())).min(1).max(5).describe("1-5 sample data objects"),
|
|
400
|
+
intent: zod_1.z.enum(["comparison", "trend", "distribution", "relationship", "composition", "geographic", "network", "hierarchy"]).optional().describe("Visualization intent to narrow suggestions"),
|
|
401
|
+
}, suggestChartHandler);
|
|
402
|
+
srv.tool("renderChart", `Render a Semiotic chart to static SVG. Returns SVG string or validation errors. Available components: ${componentNames.join(", ")}.`, {
|
|
403
|
+
component: zod_1.z.string().describe("Chart component name, e.g. 'LineChart', 'BarChart'"),
|
|
404
|
+
props: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().describe("Chart props object, e.g. { data: [...], xAccessor: 'x' }."),
|
|
405
|
+
}, renderChartHandler);
|
|
406
|
+
srv.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for common problems (empty data, bad dimensions, missing accessors, wrong data shape, etc). Returns a human-readable diagnostic report with actionable fixes.", {
|
|
407
|
+
component: zod_1.z.string().describe("Chart component name, e.g. 'LineChart'"),
|
|
408
|
+
props: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().describe("Chart props object, e.g. { data: [...], xAccessor: 'x' }."),
|
|
409
|
+
}, diagnoseConfigHandler);
|
|
410
|
+
srv.tool("reportIssue", "Generate a GitHub issue URL for Semiotic bug reports or feature requests. Returns a URL the user can open to submit. For rendering bugs, include the component name, props summary, and any diagnoseConfig output in the body.", {
|
|
411
|
+
title: zod_1.z.string().describe("Issue title, e.g. 'Bug: BarChart tooltip shows undefined'"),
|
|
412
|
+
body: zod_1.z.string().optional().describe("Issue body with details, reproduction steps, diagnoseConfig output"),
|
|
413
|
+
labels: zod_1.z.union([zod_1.z.array(zod_1.z.string()), zod_1.z.string()]).optional().describe("GitHub labels, e.g. ['bug'] or 'bug'"),
|
|
414
|
+
}, reportIssueHandler);
|
|
415
|
+
return srv;
|
|
416
|
+
}
|
|
417
|
+
// ── Startup ──────────────────────────────────────────────────────────────
|
|
418
|
+
const cliArgs = process.argv.slice(2);
|
|
419
|
+
const httpMode = cliArgs.includes("--http");
|
|
420
|
+
const portFlagIndex = cliArgs.indexOf("--port");
|
|
421
|
+
const parsedPort = portFlagIndex !== -1 && cliArgs[portFlagIndex + 1] != null
|
|
422
|
+
? parseInt(cliArgs[portFlagIndex + 1], 10)
|
|
423
|
+
: NaN;
|
|
424
|
+
const port = Number.isFinite(parsedPort) ? parsedPort : 3001;
|
|
146
425
|
async function main() {
|
|
147
|
-
|
|
148
|
-
|
|
426
|
+
if (httpMode) {
|
|
427
|
+
// HTTP mode — session-based, one server+transport per session
|
|
428
|
+
const sessions = new Map();
|
|
429
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
430
|
+
// CORS headers for browser-based inspectors
|
|
431
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
432
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
433
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
|
|
434
|
+
res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
|
|
435
|
+
if (req.method === "OPTIONS") {
|
|
436
|
+
res.writeHead(204);
|
|
437
|
+
res.end();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
441
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
442
|
+
// Existing session — route to its transport
|
|
443
|
+
const session = sessions.get(sessionId);
|
|
444
|
+
await session.transport.handleRequest(req, res);
|
|
445
|
+
}
|
|
446
|
+
else if (!sessionId) {
|
|
447
|
+
// New session — create server + transport
|
|
448
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
449
|
+
sessionIdGenerator: () => `semiotic-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
450
|
+
});
|
|
451
|
+
const srv = createServer();
|
|
452
|
+
await srv.connect(transport);
|
|
453
|
+
transport.onclose = () => {
|
|
454
|
+
const sid = transport.sessionId;
|
|
455
|
+
if (sid)
|
|
456
|
+
sessions.delete(sid);
|
|
457
|
+
};
|
|
458
|
+
await transport.handleRequest(req, res);
|
|
459
|
+
const sid = transport.sessionId;
|
|
460
|
+
if (sid) {
|
|
461
|
+
sessions.set(sid, { server: srv, transport });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
// Session ID provided but not found — stale session
|
|
466
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
467
|
+
res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Unknown session. Send a request without mcp-session-id to start a new session." }, id: null }));
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
httpServer.listen(port, () => {
|
|
471
|
+
console.error(`Semiotic MCP server (HTTP) listening on http://localhost:${port}`);
|
|
472
|
+
console.error("Tools: getSchema, suggestChart, renderChart, diagnoseConfig, reportIssue");
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
// Default: stdio mode for Claude Desktop, Claude Code, Cursor, etc.
|
|
477
|
+
const srv = createServer();
|
|
478
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
479
|
+
await srv.connect(transport);
|
|
480
|
+
}
|
|
149
481
|
}
|
|
150
482
|
main().catch((err) => {
|
|
151
483
|
console.error("MCP server error:", err);
|
|
@@ -53,12 +53,14 @@ function renderHOCToSVG(componentName, props) {
|
|
|
53
53
|
error: `Unknown component "${componentName}". Available: ${Object.keys(componentRegistry_1.COMPONENT_REGISTRY).join(", ")}`,
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
-
// Validate props
|
|
56
|
+
// Validate props (skip for components not in the validation map, e.g. geo)
|
|
57
57
|
const validation = (0, ai_1.validateProps)(componentName, props);
|
|
58
|
-
|
|
58
|
+
const errors = validation.errors ?? [];
|
|
59
|
+
const isUnknownComponentOnly = errors.length === 1 && errors[0].startsWith("Unknown component");
|
|
60
|
+
if (!validation.valid && !isUnknownComponentOnly) {
|
|
59
61
|
return {
|
|
60
62
|
svg: null,
|
|
61
|
-
error: `Validation errors:\n${
|
|
63
|
+
error: `Validation errors:\n${errors.join("\n")}`,
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
// Disable hover (not useful in static SVG)
|
package/ai/schema.json
CHANGED
|
@@ -4,6 +4,8 @@ export interface ChartErrorProps {
|
|
|
4
4
|
componentName: string;
|
|
5
5
|
/** The error message to display */
|
|
6
6
|
message: string;
|
|
7
|
+
/** Optional diagnostic suggestions from diagnoseConfig */
|
|
8
|
+
diagnosticHint?: string;
|
|
7
9
|
/** Chart width */
|
|
8
10
|
width: number;
|
|
9
11
|
/** Chart height */
|
|
@@ -16,4 +18,4 @@ export interface ChartErrorProps {
|
|
|
16
18
|
* Designed to be obvious in development but not alarming in production —
|
|
17
19
|
* uses muted colors that adapt to light/dark backgrounds.
|
|
18
20
|
*/
|
|
19
|
-
export default function ChartError({ componentName, message, width, height, }: ChartErrorProps): React.JSX.Element;
|
|
21
|
+
export default function ChartError({ componentName, message, diagnosticHint, width, height, }: ChartErrorProps): React.JSX.Element;
|