semiotic 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CLAUDE.md +134 -216
  2. package/LICENSE +197 -10
  3. package/README.md +1 -0
  4. package/ai/dist/componentRegistry.js +6 -0
  5. package/ai/dist/mcp-server.js +232 -65
  6. package/ai/dist/renderHOCToSVG.js +5 -3
  7. package/ai/examples.md +93 -0
  8. package/ai/schema.json +3916 -878
  9. package/ai/system-prompt.md +27 -0
  10. package/dist/components/ThemeProvider.d.ts +5 -3
  11. package/dist/components/charts/geo/ChoroplethMap.d.ts +1 -1
  12. package/dist/components/charts/index.d.ts +8 -1
  13. package/dist/components/charts/ordinal/BarChart.d.ts +1 -0
  14. package/dist/components/charts/ordinal/BoxPlot.d.ts +1 -0
  15. package/dist/components/charts/ordinal/DonutChart.d.ts +1 -0
  16. package/dist/components/charts/ordinal/DotPlot.d.ts +1 -0
  17. package/dist/components/charts/ordinal/FunnelChart.d.ts +55 -0
  18. package/dist/components/charts/ordinal/GroupedBarChart.d.ts +1 -0
  19. package/dist/components/charts/ordinal/Histogram.d.ts +1 -0
  20. package/dist/components/charts/ordinal/PieChart.d.ts +1 -0
  21. package/dist/components/charts/ordinal/RidgelinePlot.d.ts +1 -0
  22. package/dist/components/charts/ordinal/StackedBarChart.d.ts +1 -0
  23. package/dist/components/charts/ordinal/SwarmPlot.d.ts +1 -0
  24. package/dist/components/charts/ordinal/ViolinPlot.d.ts +1 -0
  25. package/dist/components/charts/shared/colorManipulation.d.ts +15 -0
  26. package/dist/components/charts/shared/formatUtils.d.ts +28 -0
  27. package/dist/components/charts/shared/hatchPattern.d.ts +35 -0
  28. package/dist/components/charts/shared/hooks.d.ts +16 -1
  29. package/dist/components/charts/shared/legendUtils.d.ts +2 -1
  30. package/dist/components/charts/shared/selectionUtils.d.ts +11 -0
  31. package/dist/components/charts/shared/statisticalOverlays.d.ts +49 -5
  32. package/dist/components/charts/shared/types.d.ts +4 -1
  33. package/dist/components/charts/xy/Heatmap.d.ts +1 -1
  34. package/dist/components/charts/xy/MultiAxisLineChart.d.ts +71 -0
  35. package/dist/components/realtime/types.d.ts +2 -0
  36. package/dist/components/semiotic-ai.d.ts +3 -0
  37. package/dist/components/semiotic-ordinal.d.ts +3 -0
  38. package/dist/components/semiotic-themes.d.ts +64 -0
  39. package/dist/components/semiotic-xy.d.ts +1 -0
  40. package/dist/components/semiotic.d.ts +11 -5
  41. package/dist/components/store/ThemeStore.d.ts +22 -2
  42. package/dist/components/stream/OrdinalSVGOverlay.d.ts +1 -0
  43. package/dist/components/stream/PipelineStore.d.ts +2 -0
  44. package/dist/components/stream/SVGOverlay.d.ts +5 -3
  45. package/dist/components/stream/accessorUtils.d.ts +14 -0
  46. package/dist/components/stream/networkTypes.d.ts +2 -0
  47. package/dist/components/stream/ordinalSceneBuilders/barFunnelScene.d.ts +27 -0
  48. package/dist/components/stream/ordinalSceneBuilders/funnelScene.d.ts +26 -0
  49. package/dist/components/stream/ordinalTypes.d.ts +16 -2
  50. package/dist/components/stream/renderers/barFunnelCanvasRenderer.d.ts +12 -0
  51. package/dist/components/stream/renderers/trapezoidCanvasRenderer.d.ts +15 -0
  52. package/dist/components/stream/sceneUtils.d.ts +10 -0
  53. package/dist/components/stream/types.d.ts +10 -3
  54. package/dist/geo.min.js +1 -1
  55. package/dist/geo.module.min.js +1 -1
  56. package/dist/network.min.js +1 -1
  57. package/dist/network.module.min.js +1 -1
  58. package/dist/ordinal.min.js +1 -1
  59. package/dist/ordinal.module.min.js +1 -1
  60. package/dist/realtime.min.js +1 -1
  61. package/dist/realtime.module.min.js +1 -1
  62. package/dist/semiotic-ai-statisticalOverlays-C2PPlmXv.js +1 -0
  63. package/dist/semiotic-ai.d.ts +3 -0
  64. package/dist/semiotic-ai.min.js +1 -1
  65. package/dist/semiotic-ai.module.min.js +1 -1
  66. package/dist/semiotic-ordinal.d.ts +3 -0
  67. package/dist/semiotic-statisticalOverlays-D8LhSbQt.js +1 -0
  68. package/dist/semiotic-themes.d.ts +64 -0
  69. package/dist/semiotic-themes.min.js +1 -0
  70. package/dist/semiotic-themes.module.min.js +1 -0
  71. package/dist/semiotic-xy.d.ts +1 -0
  72. package/dist/semiotic.d.ts +11 -5
  73. package/dist/semiotic.min.js +1 -1
  74. package/dist/semiotic.module.min.js +1 -1
  75. package/dist/server.min.js +1 -1
  76. package/dist/server.module.min.js +1 -1
  77. package/dist/xy-statisticalOverlays-C2PPlmXv.js +1 -0
  78. package/dist/xy.min.js +1 -1
  79. package/dist/xy.module.min.js +1 -1
  80. package/package.json +24 -5
  81. package/dist/semiotic-ai-statisticalOverlays-C1f7TYyD.js +0 -1
  82. package/dist/semiotic-statisticalOverlays-C1f7TYyD.js +0 -1
  83. package/dist/xy-statisticalOverlays-C1f7TYyD.js +0 -1
@@ -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,34 +59,32 @@ 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
- // ── getSchema tool ──────────────────────────────────────────────────────
78
- // Returns the prop schema for a specific component, or lists all components.
79
- server.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.`, {}, async (args) => {
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();
83
84
  const renderable = new Set(Object.keys(componentRegistry_1.COMPONENT_REGISTRY));
84
85
  const list = all.map(name => renderable.has(name) ? `${name} [renderable]` : name);
85
86
  return {
86
- 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.` }],
87
+ content: [{ type: "text", text: `Available components (${all.length}):\n${list.join(", ")}\n\nComponents marked [renderable] can be rendered to SVG via renderChart (pass theme parameter for styled output). Others (Realtime*) require a browser environment.\n\nAll charts support CSS custom properties for theming (--semiotic-bg, --semiotic-text, --semiotic-grid, etc.) and <ThemeProvider>. Use COLOR_BLIND_SAFE_CATEGORICAL (import from semiotic) for accessible color palettes.\n\nPass { component: '<name>' } to get the prop schema for a specific component.` }],
87
88
  };
88
89
  }
89
90
  const entry = schemaByComponent[component];
@@ -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 renderable = 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
+ 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: `${renderable}\n\n${JSON.stringify(entry, null, 2)}` }],
100
+ content: [{ type: "text", text: `${renderableNote}\n\n${JSON.stringify(entry, null, 2)}` }],
100
101
  };
101
- });
102
- // ── suggestChart tool ───────────────────────────────────────────────────
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 (data.length >= 10 && (!intent || intent === "distribution")) {
249
+ if (!intent || intent === "distribution") {
256
250
  suggestions.push({
257
251
  component: "Histogram",
258
252
  confidence: "medium",
259
- reason: `${data.length}+ data points — histogram shows value distribution`,
253
+ reason: `Numeric distribution of ${valField} — histogram shows value spread`,
260
254
  props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"` },
261
255
  });
262
256
  }
@@ -311,25 +305,17 @@ server.tool("suggestChart", "Recommend Semiotic chart types for a given data sam
311
305
  const propsStr = Object.entries(s.props).map(([k, v]) => `${k}=${v}`).join(" ");
312
306
  return `${i + 1}. **${s.component}** (${s.confidence} confidence)\n ${s.reason}\n \`<${s.component} ${propsStr} />\``;
313
307
  });
308
+ // Theming guidance appended to every recommendation
309
+ const themingTip = `\n---\n**Styling**: All charts respond to CSS custom properties on any ancestor element:\n\`\`\`css\n.my-theme {\n --semiotic-bg: #fff; /* chart background */\n --semiotic-text: #333; /* primary text */\n --semiotic-text-secondary: #666; /* tick labels */\n --semiotic-grid: #e0e0e0; /* grid lines */\n --semiotic-border: #e0e0e0; /* axis lines, borders */\n --semiotic-font-family: sans-serif;\n --semiotic-tooltip-bg: rgba(0,0,0,0.85);\n --semiotic-tooltip-text: white;\n --semiotic-tooltip-radius: 6px;\n}\n\`\`\`\nOr use \`<ThemeProvider theme="dark">\` / \`<ThemeProvider theme={{ colors: {...}, typography: {...} }}>\`.\nFor accessibility, use \`colorScheme={COLOR_BLIND_SAFE_CATEGORICAL}\` (import from \`semiotic\`) — 8-color palette safe for all forms of color blindness.`;
314
310
  return {
315
- content: [{ type: "text", text: lines.join("\n\n") }],
311
+ content: [{ type: "text", text: lines.join("\n\n") + themingTip }],
316
312
  };
317
- });
318
- // ── renderChart tool ─────────────────────────────────────────────────────
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) => {
313
+ }
314
+ async function renderChartHandler(args) {
323
315
  const component = args.component;
324
- let props;
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
- }
316
+ const props = args.props ?? {};
317
+ const theme = args.theme;
318
+ const format = args.format || "svg";
333
319
  if (!component) {
334
320
  return {
335
321
  content: [{ type: "text", text: `Missing 'component' field. Provide { component: '<name>', props: { ... } }. Available: ${componentNames.join(", ")}` }],
@@ -349,23 +335,51 @@ server.tool("renderChart", `Render any Semiotic chart to static SVG. Pass { comp
349
335
  isError: true,
350
336
  };
351
337
  }
338
+ let svg = result.svg;
339
+ // Inject theme CSS custom properties into the SVG root element.
340
+ // We add a <style> block inside the SVG rather than wrapping in a <div>,
341
+ // because sharp requires pure SVG input for PNG rasterization.
342
+ if (theme && Object.keys(theme).length > 0) {
343
+ const validVars = Object.entries(theme)
344
+ .filter(([k]) => k.startsWith("--semiotic-"))
345
+ .map(([k, v]) => `${k}: ${v}`)
346
+ .join("; ");
347
+ if (validVars) {
348
+ svg = svg.replace(/<svg([^>]*)>/, `<svg$1><style>:root { ${validVars} }</style>`);
349
+ }
350
+ }
351
+ // PNG rasterization via sharp (optional dependency)
352
+ if (format === "png") {
353
+ try {
354
+ // Dynamic import — sharp is an optional dependency
355
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
356
+ const sharpMod = await Function('return import("sharp")')();
357
+ const sharpFn = sharpMod.default || sharpMod;
358
+ const pngBuffer = await sharpFn(Buffer.from(svg)).png().toBuffer();
359
+ const base64 = pngBuffer.toString("base64");
360
+ return {
361
+ content: [{ type: "text", text: `data:image/png;base64,${base64}` }],
362
+ };
363
+ }
364
+ catch (err) {
365
+ if (err.code === "MODULE_NOT_FOUND" || err.code === "ERR_MODULE_NOT_FOUND") {
366
+ return {
367
+ content: [{ type: "text", text: `PNG output requires the 'sharp' package. Install it with: npm install sharp\n\nFalling back to SVG output:\n\n${svg}` }],
368
+ };
369
+ }
370
+ return {
371
+ content: [{ type: "text", text: `PNG conversion failed: ${err.message}\n\nSVG output:\n\n${svg}` }],
372
+ isError: true,
373
+ };
374
+ }
375
+ }
352
376
  return {
353
- content: [{ type: "text", text: result.svg }],
377
+ content: [{ type: "text", text: svg }],
354
378
  };
355
- });
356
- // ── diagnoseConfig tool ──────────────────────────────────────────────────
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) => {
379
+ }
380
+ async function diagnoseConfigHandler(args) {
360
381
  const component = args.component;
361
- let props;
362
- if (args.props) {
363
- props = args.props;
364
- }
365
- else {
366
- const { component: _, ...rest } = args;
367
- props = rest;
368
- }
382
+ const props = args.props ?? {};
369
383
  if (!component) {
370
384
  return {
371
385
  content: [{ type: "text", text: "Missing 'component' field. Provide { component: 'LineChart', props: { ... } }." }],
@@ -374,13 +388,13 @@ server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for commo
374
388
  }
375
389
  const result = (0, ai_1.diagnoseConfig)(component, props);
376
390
  if (result.ok) {
377
- const warnings = result.diagnoses.filter(d => d.severity === "warning");
391
+ const warnings = result.diagnoses.filter((d) => d.severity === "warning");
378
392
  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")}`
393
+ ? `Configuration looks good with ${warnings.length} warning(s):\n${warnings.map((w) => `⚠ [${w.code}] ${w.message}\n Fix: ${w.fix}`).join("\n")}`
380
394
  : `✓ Configuration looks good — no issues detected.`;
381
395
  return { content: [{ type: "text", text: msg }] };
382
396
  }
383
- const lines = result.diagnoses.map(d => {
397
+ const lines = result.diagnoses.map((d) => {
384
398
  const icon = d.severity === "error" ? "✗" : "⚠";
385
399
  const fixLine = d.fix ? `\n Fix: ${d.fix}` : "";
386
400
  return `${icon} [${d.code}] ${d.message}${fixLine}`;
@@ -389,12 +403,8 @@ server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for commo
389
403
  content: [{ type: "text", text: lines.join("\n") }],
390
404
  isError: true,
391
405
  };
392
- });
393
- // ── reportIssue tool ─────────────────────────────────────────────────────
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) => {
406
+ }
407
+ async function reportIssueHandler(args) {
398
408
  const title = args.title;
399
409
  const body = args.body;
400
410
  const labels = args.labels;
@@ -416,11 +426,168 @@ server.tool("reportIssue", "Generate a GitHub issue URL for Semiotic bug reports
416
426
  return {
417
427
  content: [{ type: "text", text: `Open this URL to submit the issue:\n\n${url}` }],
418
428
  };
419
- });
420
- // Start the server
429
+ }
430
+ // Named theme presets (inlined to avoid runtime dependency on semiotic-themes bundle)
431
+ const THEME_PRESET_NAMES = [
432
+ "light", "dark", "high-contrast",
433
+ "pastels", "pastels-dark",
434
+ "bi-tool", "bi-tool-dark",
435
+ "italian", "italian-dark",
436
+ "tufte", "tufte-dark",
437
+ "journalist", "journalist-dark",
438
+ "playful", "playful-dark",
439
+ ];
440
+ async function applyThemeHandler(args) {
441
+ const name = args.name;
442
+ if (!name) {
443
+ return {
444
+ content: [{ type: "text", text: `Available theme presets:\n${THEME_PRESET_NAMES.join(", ")}\n\nPass { name: "tufte" } to get the CSS custom properties and ThemeProvider usage for that theme.\n\nLight-mode presets: ${THEME_PRESET_NAMES.filter(n => !n.includes("dark")).join(", ")}\nDark-mode presets: ${THEME_PRESET_NAMES.filter(n => n.includes("dark")).join(", ")}` }],
445
+ };
446
+ }
447
+ if (!THEME_PRESET_NAMES.includes(name)) {
448
+ return {
449
+ content: [{ type: "text", text: `Unknown theme "${name}". Available: ${THEME_PRESET_NAMES.join(", ")}` }],
450
+ isError: true,
451
+ };
452
+ }
453
+ const usage = [
454
+ `## Theme: "${name}"`,
455
+ "",
456
+ "### Option 1: ThemeProvider (recommended)",
457
+ "```jsx",
458
+ `import { ThemeProvider } from "semiotic"`,
459
+ `<ThemeProvider theme="${name}">`,
460
+ ` <LineChart ... />`,
461
+ `</ThemeProvider>`,
462
+ "```",
463
+ "",
464
+ "### Option 2: Import the theme object",
465
+ "```jsx",
466
+ `import { ${name.replace(/-./g, c => c[1].toUpperCase()).replace(/^./, c => c.toUpperCase()).replace(/Dark$/, '_DARK').replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()} } from "semiotic/themes"`,
467
+ `<ThemeProvider theme={themeObject}>`,
468
+ ` <BarChart ... />`,
469
+ `</ThemeProvider>`,
470
+ "```",
471
+ "",
472
+ "### Option 3: CSS custom properties (no React required)",
473
+ "```jsx",
474
+ `import { themeToCSS } from "semiotic/themes"`,
475
+ `import { ${name.replace(/-./g, c => c[1].toUpperCase()).replace(/^./, c => c.toUpperCase()).replace(/Dark$/, '_DARK').replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()} } from "semiotic/themes"`,
476
+ `const css = themeToCSS(themeObject, ".my-charts")`,
477
+ "// Outputs CSS custom properties string for embedding in a stylesheet",
478
+ "```",
479
+ "",
480
+ "### Option 4: Design tokens JSON",
481
+ "```jsx",
482
+ `import { themeToTokens } from "semiotic/themes"`,
483
+ `const tokens = themeToTokens(themeObject)`,
484
+ "// Style Dictionary / DTCG-compatible token format",
485
+ "```",
486
+ "",
487
+ "For accessibility, consider `\"high-contrast\"` which uses `COLOR_BLIND_SAFE_CATEGORICAL` (Wong 2011 palette).",
488
+ ];
489
+ return {
490
+ content: [{ type: "text", text: usage.join("\n") }],
491
+ };
492
+ }
493
+ // ── Server factory ───────────────────────────────────────────────────────
494
+ // Creates a fresh McpServer with all tools registered.
495
+ // HTTP mode needs one instance per session (McpServer can only connect to one transport).
496
+ // Stdio mode uses a single instance.
497
+ function createServer() {
498
+ const srv = new mcp_js_1.McpServer({
499
+ name: "semiotic",
500
+ version: schema.version || "3.0.0",
501
+ });
502
+ 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);
503
+ 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.", {
504
+ 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"),
505
+ intent: zod_1.z.enum(["comparison", "trend", "distribution", "relationship", "composition", "geographic", "network", "hierarchy"]).optional().describe("Visualization intent to narrow suggestions"),
506
+ }, suggestChartHandler);
507
+ srv.tool("renderChart", `Render a Semiotic chart to static SVG or PNG. Returns SVG string (default) or Base64-encoded PNG image. Optionally pass theme CSS custom properties (--semiotic-bg, --semiotic-text, etc.) to style the output. PNG requires the 'sharp' package to be installed. Available components: ${componentNames.join(", ")}.`, {
508
+ component: zod_1.z.string().describe("Chart component name, e.g. 'LineChart', 'BarChart'"),
509
+ props: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().describe("Chart props object, e.g. { data: [...], xAccessor: 'x' }."),
510
+ theme: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional().describe("CSS custom properties for theming, e.g. { '--semiotic-bg': '#1a1a2e', '--semiotic-text': '#ededed' }. Only --semiotic-* variables are applied."),
511
+ format: zod_1.z.enum(["svg", "png"]).optional().describe("Output format: 'svg' (default) returns SVG markup, 'png' returns a Base64-encoded PNG image. PNG requires the 'sharp' package."),
512
+ }, renderChartHandler);
513
+ srv.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for common problems (empty data, bad dimensions, missing accessors, wrong data shape, color contrast issues, etc). Checks WCAG color contrast ratios and suggests COLOR_BLIND_SAFE_CATEGORICAL for accessibility. Returns a human-readable diagnostic report with actionable fixes.", {
514
+ component: zod_1.z.string().describe("Chart component name, e.g. 'LineChart'"),
515
+ props: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().describe("Chart props object, e.g. { data: [...], xAccessor: 'x' }."),
516
+ }, diagnoseConfigHandler);
517
+ 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.", {
518
+ title: zod_1.z.string().describe("Issue title, e.g. 'Bug: BarChart tooltip shows undefined'"),
519
+ body: zod_1.z.string().optional().describe("Issue body with details, reproduction steps, diagnoseConfig output"),
520
+ 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'"),
521
+ }, reportIssueHandler);
522
+ srv.tool("applyTheme", `Get usage instructions for a named Semiotic theme preset. Returns ThemeProvider examples, CSS custom properties, and design token export patterns. Available themes: ${THEME_PRESET_NAMES.join(", ")}.`, {
523
+ name: zod_1.z.string().optional().describe("Theme preset name, e.g. 'tufte', 'pastels-dark', 'bi-tool'. Omit to list all available themes."),
524
+ }, applyThemeHandler);
525
+ return srv;
526
+ }
527
+ // ── Startup ──────────────────────────────────────────────────────────────
528
+ const cliArgs = process.argv.slice(2);
529
+ const httpMode = cliArgs.includes("--http");
530
+ const portFlagIndex = cliArgs.indexOf("--port");
531
+ const parsedPort = portFlagIndex !== -1 && cliArgs[portFlagIndex + 1] != null
532
+ ? parseInt(cliArgs[portFlagIndex + 1], 10)
533
+ : NaN;
534
+ const port = Number.isFinite(parsedPort) ? parsedPort : 3001;
421
535
  async function main() {
422
- const transport = new stdio_js_1.StdioServerTransport();
423
- await server.connect(transport);
536
+ if (httpMode) {
537
+ // HTTP mode — session-based, one server+transport per session
538
+ const sessions = new Map();
539
+ const httpServer = http.createServer(async (req, res) => {
540
+ // CORS headers for browser-based inspectors
541
+ res.setHeader("Access-Control-Allow-Origin", "*");
542
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
543
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
544
+ res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
545
+ if (req.method === "OPTIONS") {
546
+ res.writeHead(204);
547
+ res.end();
548
+ return;
549
+ }
550
+ const sessionId = req.headers["mcp-session-id"];
551
+ if (sessionId && sessions.has(sessionId)) {
552
+ // Existing session — route to its transport
553
+ const session = sessions.get(sessionId);
554
+ await session.transport.handleRequest(req, res);
555
+ }
556
+ else if (!sessionId) {
557
+ // New session — create server + transport
558
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
559
+ sessionIdGenerator: () => `semiotic-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
560
+ });
561
+ const srv = createServer();
562
+ await srv.connect(transport);
563
+ transport.onclose = () => {
564
+ const sid = transport.sessionId;
565
+ if (sid)
566
+ sessions.delete(sid);
567
+ };
568
+ await transport.handleRequest(req, res);
569
+ const sid = transport.sessionId;
570
+ if (sid) {
571
+ sessions.set(sid, { server: srv, transport });
572
+ }
573
+ }
574
+ else {
575
+ // Session ID provided but not found — stale session
576
+ res.writeHead(400, { "Content-Type": "application/json" });
577
+ 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 }));
578
+ }
579
+ });
580
+ httpServer.listen(port, () => {
581
+ console.error(`Semiotic MCP server (HTTP) listening on http://localhost:${port}`);
582
+ console.error("Tools: getSchema, suggestChart, renderChart, diagnoseConfig, reportIssue");
583
+ });
584
+ }
585
+ else {
586
+ // Default: stdio mode for Claude Desktop, Claude Code, Cursor, etc.
587
+ const srv = createServer();
588
+ const transport = new stdio_js_1.StdioServerTransport();
589
+ await srv.connect(transport);
590
+ }
424
591
  }
425
592
  main().catch((err) => {
426
593
  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
- if (!validation.valid) {
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${validation.errors.join("\n")}`,
63
+ error: `Validation errors:\n${errors.join("\n")}`,
62
64
  };
63
65
  }
64
66
  // Disable hover (not useful in static SVG)
package/ai/examples.md CHANGED
@@ -902,3 +902,96 @@ const containerRef = useRef<HTMLDivElement>(null)
902
902
  ```
903
903
 
904
904
  Key: `exportChart(wrapperDiv, { format, filename, scale, background })`. It queries the wrapper for canvas and SVG elements internally. Default format: PNG with 2x scale.
905
+
906
+ ---
907
+
908
+ ## Theming & Brand Styling
909
+
910
+ ### CSS Custom Properties (no React context needed)
911
+
912
+ ```jsx
913
+ // Dark theme via CSS custom properties on a wrapper div
914
+ <div style={{
915
+ "--semiotic-bg": "#1a1a2e",
916
+ "--semiotic-text": "#ededed",
917
+ "--semiotic-text-secondary": "#aaa",
918
+ "--semiotic-grid": "#333",
919
+ "--semiotic-border": "#555",
920
+ "--semiotic-font-family": "'Georgia', serif",
921
+ "--semiotic-tooltip-bg": "#1a1a2e",
922
+ "--semiotic-tooltip-text": "#ededed",
923
+ "--semiotic-tooltip-radius": "8px",
924
+ "--semiotic-tooltip-shadow": "0 4px 12px rgba(0,0,0,0.4)",
925
+ "--semiotic-primary": "#ff6b6b",
926
+ "--semiotic-focus": "#ff6b6b",
927
+ }}>
928
+ <LineChart
929
+ data={[{ x: 1, y: 10 }, { x: 2, y: 20 }, { x: 3, y: 15 }]}
930
+ xAccessor="x" yAccessor="y"
931
+ showGrid
932
+ annotations={[{ type: "y-threshold", value: 18, label: "Target" }]}
933
+ />
934
+ </div>
935
+ ```
936
+
937
+ ### ThemeProvider (React context)
938
+
939
+ ```jsx
940
+ import { ThemeProvider, DARK_THEME, COLOR_BLIND_SAFE_CATEGORICAL } from "semiotic"
941
+
942
+ // Preset dark theme
943
+ <ThemeProvider theme="dark">
944
+ <BarChart data={data} categoryAccessor="name" valueAccessor="value" />
945
+ </ThemeProvider>
946
+
947
+ // Custom brand theme (partial merge with defaults)
948
+ <ThemeProvider theme={{
949
+ mode: "light",
950
+ colors: {
951
+ primary: "#cc0000",
952
+ categorical: ["#cc0000", "#333333", "#c8a415", "#4682b4"],
953
+ background: "#fafafa",
954
+ text: "#1a1a1a",
955
+ textSecondary: "#666",
956
+ grid: "#e0e0e0",
957
+ border: "#e0e0e0",
958
+ },
959
+ typography: {
960
+ fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
961
+ titleSize: 16, labelSize: 12, tickSize: 10,
962
+ },
963
+ tooltip: {
964
+ background: "#fafafa",
965
+ text: "#1a1a1a",
966
+ borderRadius: "4px",
967
+ },
968
+ }}>
969
+ <GroupedBarChart data={data} categoryAccessor="quarter" valueAccessor="revenue"
970
+ groupBy="region" colorBy="region" showGrid showLegend />
971
+ </ThemeProvider>
972
+
973
+ // Color-blind safe palette (Wong 2011 — 8 colors)
974
+ <Scatterplot data={data} xAccessor="x" yAccessor="y"
975
+ colorBy="category" colorScheme={COLOR_BLIND_SAFE_CATEGORICAL} />
976
+ ```
977
+
978
+ ### Annotations with Theme Colors
979
+
980
+ ```jsx
981
+ // Annotations inherit --semiotic-primary and --semiotic-text-secondary from theme
982
+ <LineChart
983
+ data={salesData}
984
+ xAccessor="month" yAccessor="revenue"
985
+ annotations={[
986
+ // Threshold line — defaults to --semiotic-primary if no color set
987
+ { type: "y-threshold", value: 50000, label: "Q3 Target" },
988
+ // Widget annotation at specific data point
989
+ { type: "widget", month: "Jul", revenue: 72000, dy: -15, content: (
990
+ <span style={{ fontSize: 11, fontWeight: 700 }}>Record month</span>
991
+ )},
992
+ // Enclose a cluster of outliers
993
+ { type: "enclose", coordinates: outlierPoints, label: "Outlier cluster", padding: 15 },
994
+ ]}
995
+ showGrid
996
+ />
997
+ ```