semiotic 3.1.1 → 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/ai/dist/mcp-server.js +119 -62
- package/ai/dist/renderHOCToSVG.js +5 -3
- package/ai/schema.json +1 -1
- package/package.json +1 -1
package/ai/dist/mcp-server.js
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
* }
|
|
20
20
|
* }
|
|
21
21
|
* }
|
|
22
|
+
*
|
|
23
|
+
* HTTP mode (for remote inspectors / web clients):
|
|
24
|
+
* npx semiotic-mcp --http --port 3001
|
|
22
25
|
*/
|
|
23
26
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
27
|
if (k2 === undefined) k2 = k;
|
|
@@ -56,27 +59,25 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
56
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
60
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
58
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");
|
|
59
64
|
const fs = __importStar(require("fs"));
|
|
60
65
|
const path = __importStar(require("path"));
|
|
66
|
+
const http = __importStar(require("http"));
|
|
61
67
|
const renderHOCToSVG_1 = require("./renderHOCToSVG");
|
|
62
68
|
const componentRegistry_1 = require("./componentRegistry");
|
|
63
69
|
const ai_1 = require("semiotic/ai");
|
|
64
70
|
// Load schema.json for version info
|
|
65
71
|
const schemaPath = path.resolve(__dirname, "../schema.json");
|
|
66
72
|
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
|
|
67
|
-
// Build MCP server
|
|
68
|
-
const server = new mcp_js_1.McpServer({
|
|
69
|
-
name: "semiotic",
|
|
70
|
-
version: schema.version || "3.0.0",
|
|
71
|
-
});
|
|
72
73
|
// Build component name → schema lookup from schema.json
|
|
73
74
|
const schemaByComponent = {};
|
|
74
75
|
for (const tool of schema.tools) {
|
|
75
76
|
schemaByComponent[tool.function.name] = tool.function;
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const componentNames = Object.keys(componentRegistry_1.COMPONENT_REGISTRY).sort();
|
|
79
|
+
const REPO = "nteract/semiotic";
|
|
80
|
+
async function getSchemaHandler(args) {
|
|
80
81
|
const component = args.component;
|
|
81
82
|
if (!component) {
|
|
82
83
|
const all = Object.keys(schemaByComponent).sort();
|
|
@@ -94,14 +95,12 @@ server.tool("getSchema", `Return the prop schema for a Semiotic chart component.
|
|
|
94
95
|
isError: true,
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
|
-
const
|
|
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.";
|
|
98
99
|
return {
|
|
99
|
-
content: [{ type: "text", text: `${
|
|
100
|
+
content: [{ type: "text", text: `${renderableNote}\n\n${JSON.stringify(entry, null, 2)}` }],
|
|
100
101
|
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Analyzes a data sample and recommends appropriate chart types.
|
|
104
|
-
server.tool("suggestChart", "Recommend Semiotic chart types for a given data sample. Pass { data: [...] } with 1-5 sample objects. Optionally pass { intent: 'comparison' | 'trend' | 'distribution' | 'relationship' | 'composition' | 'geographic' | 'network' | 'hierarchy' } to narrow suggestions. Returns ranked recommendations with example props.", {}, async (args) => {
|
|
102
|
+
}
|
|
103
|
+
async function suggestChartHandler(args) {
|
|
105
104
|
const data = args.data;
|
|
106
105
|
const intent = args.intent;
|
|
107
106
|
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
@@ -135,8 +134,6 @@ server.tool("suggestChart", "Recommend Semiotic chart types for a given data sam
|
|
|
135
134
|
numericFields.push(key);
|
|
136
135
|
}
|
|
137
136
|
else if (typeof first === "string") {
|
|
138
|
-
// Check for dates — require ISO-like pattern (YYYY-MM or YYYY/MM or YYYY-MM-DD, etc.)
|
|
139
|
-
// to avoid false positives on 4-digit IDs like "1234"
|
|
140
137
|
if (/^\d{4}[-/]\d{2}/.test(first) && !isNaN(Date.parse(first))) {
|
|
141
138
|
dateFields.push(key);
|
|
142
139
|
}
|
|
@@ -144,20 +141,17 @@ server.tool("suggestChart", "Recommend Semiotic chart types for a given data sam
|
|
|
144
141
|
stringFields.push(key);
|
|
145
142
|
}
|
|
146
143
|
}
|
|
147
|
-
// Detect geo fields
|
|
148
144
|
const kl = key.toLowerCase();
|
|
149
145
|
if (kl === "lat" || kl === "latitude")
|
|
150
146
|
geoFields.lat = key;
|
|
151
147
|
if (kl === "lon" || kl === "lng" || kl === "longitude")
|
|
152
148
|
geoFields.lon = key;
|
|
153
|
-
// Detect network fields
|
|
154
149
|
if (kl === "source" || kl === "from")
|
|
155
150
|
networkFields.source = key;
|
|
156
151
|
if (kl === "target" || kl === "to")
|
|
157
152
|
networkFields.target = key;
|
|
158
153
|
if (kl === "value" || kl === "weight" || kl === "amount")
|
|
159
154
|
networkFields.value = key;
|
|
160
|
-
// Detect hierarchy fields
|
|
161
155
|
if (kl === "children" || kl === "values")
|
|
162
156
|
hierarchyFields.children = key;
|
|
163
157
|
if (kl === "parent")
|
|
@@ -252,11 +246,11 @@ server.tool("suggestChart", "Recommend Semiotic chart types for a given data sam
|
|
|
252
246
|
props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"`, stackBy: `"${stringFields[1]}"` },
|
|
253
247
|
});
|
|
254
248
|
}
|
|
255
|
-
if (
|
|
249
|
+
if (!intent || intent === "distribution") {
|
|
256
250
|
suggestions.push({
|
|
257
251
|
component: "Histogram",
|
|
258
252
|
confidence: "medium",
|
|
259
|
-
reason:
|
|
253
|
+
reason: `Numeric distribution of ${valField} — histogram shows value spread`,
|
|
260
254
|
props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"` },
|
|
261
255
|
});
|
|
262
256
|
}
|
|
@@ -314,22 +308,10 @@ server.tool("suggestChart", "Recommend Semiotic chart types for a given data sam
|
|
|
314
308
|
return {
|
|
315
309
|
content: [{ type: "text", text: lines.join("\n\n") }],
|
|
316
310
|
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Generic tool that renders any Semiotic HOC chart to static SVG.
|
|
320
|
-
// Accepts { component, props } — the single entry point for all chart rendering.
|
|
321
|
-
const componentNames = Object.keys(componentRegistry_1.COMPONENT_REGISTRY).sort();
|
|
322
|
-
server.tool("renderChart", `Render any Semiotic chart to static SVG. Pass { component: '<name>', props: { ... } }. Returns SVG string or validation errors. Available components: ${componentNames.join(", ")}.`, {}, async (args) => {
|
|
311
|
+
}
|
|
312
|
+
async function renderChartHandler(args) {
|
|
323
313
|
const component = args.component;
|
|
324
|
-
|
|
325
|
-
if (args.props) {
|
|
326
|
-
props = args.props;
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
// Flatten shape: { component, data, ... } — strip component before forwarding
|
|
330
|
-
const { component: _, ...rest } = args;
|
|
331
|
-
props = rest;
|
|
332
|
-
}
|
|
314
|
+
const props = args.props ?? {};
|
|
333
315
|
if (!component) {
|
|
334
316
|
return {
|
|
335
317
|
content: [{ type: "text", text: `Missing 'component' field. Provide { component: '<name>', props: { ... } }. Available: ${componentNames.join(", ")}` }],
|
|
@@ -352,20 +334,10 @@ server.tool("renderChart", `Render any Semiotic chart to static SVG. Pass { comp
|
|
|
352
334
|
return {
|
|
353
335
|
content: [{ type: "text", text: result.svg }],
|
|
354
336
|
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Anti-pattern detector: checks for common failure modes and returns
|
|
358
|
-
// actionable fix instructions.
|
|
359
|
-
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) {
|
|
360
339
|
const component = args.component;
|
|
361
|
-
|
|
362
|
-
if (args.props) {
|
|
363
|
-
props = args.props;
|
|
364
|
-
}
|
|
365
|
-
else {
|
|
366
|
-
const { component: _, ...rest } = args;
|
|
367
|
-
props = rest;
|
|
368
|
-
}
|
|
340
|
+
const props = args.props ?? {};
|
|
369
341
|
if (!component) {
|
|
370
342
|
return {
|
|
371
343
|
content: [{ type: "text", text: "Missing 'component' field. Provide { component: 'LineChart', props: { ... } }." }],
|
|
@@ -374,13 +346,13 @@ server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for commo
|
|
|
374
346
|
}
|
|
375
347
|
const result = (0, ai_1.diagnoseConfig)(component, props);
|
|
376
348
|
if (result.ok) {
|
|
377
|
-
const warnings = result.diagnoses.filter(d => d.severity === "warning");
|
|
349
|
+
const warnings = result.diagnoses.filter((d) => d.severity === "warning");
|
|
378
350
|
const msg = warnings.length > 0
|
|
379
|
-
? `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")}`
|
|
380
352
|
: `✓ Configuration looks good — no issues detected.`;
|
|
381
353
|
return { content: [{ type: "text", text: msg }] };
|
|
382
354
|
}
|
|
383
|
-
const lines = result.diagnoses.map(d => {
|
|
355
|
+
const lines = result.diagnoses.map((d) => {
|
|
384
356
|
const icon = d.severity === "error" ? "✗" : "⚠";
|
|
385
357
|
const fixLine = d.fix ? `\n Fix: ${d.fix}` : "";
|
|
386
358
|
return `${icon} [${d.code}] ${d.message}${fixLine}`;
|
|
@@ -389,12 +361,8 @@ server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for commo
|
|
|
389
361
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
390
362
|
isError: true,
|
|
391
363
|
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Generates a pre-filled GitHub issue URL for bug reports or feature requests.
|
|
395
|
-
// The user (or AI agent) can open the URL to submit — no auth needed.
|
|
396
|
-
const REPO = "nteract/semiotic";
|
|
397
|
-
server.tool("reportIssue", "Generate a GitHub issue URL for Semiotic bug reports or feature requests. Pass { title, body, labels? }. 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.", {}, async (args) => {
|
|
364
|
+
}
|
|
365
|
+
async function reportIssueHandler(args) {
|
|
398
366
|
const title = args.title;
|
|
399
367
|
const body = args.body;
|
|
400
368
|
const labels = args.labels;
|
|
@@ -416,11 +384,100 @@ server.tool("reportIssue", "Generate a GitHub issue URL for Semiotic bug reports
|
|
|
416
384
|
return {
|
|
417
385
|
content: [{ type: "text", text: `Open this URL to submit the issue:\n\n${url}` }],
|
|
418
386
|
};
|
|
419
|
-
}
|
|
420
|
-
//
|
|
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;
|
|
421
425
|
async function main() {
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
}
|
|
424
481
|
}
|
|
425
482
|
main().catch((err) => {
|
|
426
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
package/package.json
CHANGED