semiotic 3.1.0 → 3.1.1

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 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
  };
@@ -3,9 +3,12 @@
3
3
  /**
4
4
  * Semiotic MCP Server
5
5
  *
6
- * Exposes every HOC chart component as an MCP tool.
7
- * Accepts component props as tool arguments, renders to static SVG,
8
- * and returns the SVG string.
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
  * {
@@ -58,7 +61,7 @@ const path = __importStar(require("path"));
58
61
  const renderHOCToSVG_1 = require("./renderHOCToSVG");
59
62
  const componentRegistry_1 = require("./componentRegistry");
60
63
  const ai_1 = require("semiotic/ai");
61
- // Load schema.json for tool definitions
64
+ // Load schema.json for version info
62
65
  const schemaPath = path.resolve(__dirname, "../schema.json");
63
66
  const schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
64
67
  // Build MCP server
@@ -66,38 +69,276 @@ const server = new mcp_js_1.McpServer({
66
69
  name: "semiotic",
67
70
  version: schema.version || "3.0.0",
68
71
  });
69
- // Register each chart component as a tool
70
- for (const toolDef of schema.tools) {
71
- const { name, description, parameters } = toolDef.function;
72
- // Skip realtime charts (ref-based, can't render to static SVG)
73
- if (name.startsWith("Realtime"))
74
- continue;
75
- // Skip components not in registry
76
- if (!componentRegistry_1.COMPONENT_REGISTRY[name])
77
- continue;
78
- // Register the tool — use raw z.any() style since we have our own validation
79
- server.tool(name, description, {}, async (args) => {
80
- const result = (0, renderHOCToSVG_1.renderHOCToSVG)(name, args);
81
- if (result.error) {
82
- return {
83
- content: [{ type: "text", text: result.error }],
84
- isError: true,
85
- };
72
+ // Build component name schema lookup from schema.json
73
+ const schemaByComponent = {};
74
+ for (const tool of schema.tools) {
75
+ schemaByComponent[tool.function.name] = tool.function;
76
+ }
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) => {
80
+ const component = args.component;
81
+ if (!component) {
82
+ const all = Object.keys(schemaByComponent).sort();
83
+ const renderable = new Set(Object.keys(componentRegistry_1.COMPONENT_REGISTRY));
84
+ const list = all.map(name => renderable.has(name) ? `${name} [renderable]` : name);
85
+ 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
+ };
88
+ }
89
+ const entry = schemaByComponent[component];
90
+ if (!entry) {
91
+ const available = Object.keys(schemaByComponent).sort();
92
+ return {
93
+ content: [{ type: "text", text: `Unknown component "${component}". Available: ${available.join(", ")}` }],
94
+ isError: true,
95
+ };
96
+ }
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
+ return {
99
+ content: [{ type: "text", text: `${renderable}\n\n${JSON.stringify(entry, null, 2)}` }],
100
+ };
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) => {
105
+ const data = args.data;
106
+ const intent = args.intent;
107
+ if (!data || !Array.isArray(data) || data.length === 0) {
108
+ return {
109
+ content: [{ type: "text", text: "Pass { data: [{ ... }, ...] } with 1-5 sample data objects. Optionally include intent: 'comparison' | 'trend' | 'distribution' | 'relationship' | 'composition' | 'geographic' | 'network' | 'hierarchy'." }],
110
+ isError: true,
111
+ };
112
+ }
113
+ const sample = data[0];
114
+ if (!sample || typeof sample !== "object") {
115
+ return {
116
+ content: [{ type: "text", text: "Data items must be objects with key-value pairs." }],
117
+ isError: true,
118
+ };
119
+ }
120
+ const keys = Object.keys(sample);
121
+ const suggestions = [];
122
+ // Classify fields
123
+ const numericFields = [];
124
+ const stringFields = [];
125
+ const dateFields = [];
126
+ const geoFields = {};
127
+ const networkFields = {};
128
+ const hierarchyFields = {};
129
+ for (const key of keys) {
130
+ const values = data.map(d => d[key]).filter(v => v != null);
131
+ if (values.length === 0)
132
+ continue;
133
+ const first = values[0];
134
+ if (typeof first === "number") {
135
+ numericFields.push(key);
136
+ }
137
+ 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
+ if (/^\d{4}[-/]\d{2}/.test(first) && !isNaN(Date.parse(first))) {
141
+ dateFields.push(key);
142
+ }
143
+ else {
144
+ stringFields.push(key);
145
+ }
146
+ }
147
+ // Detect geo fields
148
+ const kl = key.toLowerCase();
149
+ if (kl === "lat" || kl === "latitude")
150
+ geoFields.lat = key;
151
+ if (kl === "lon" || kl === "lng" || kl === "longitude")
152
+ geoFields.lon = key;
153
+ // Detect network fields
154
+ if (kl === "source" || kl === "from")
155
+ networkFields.source = key;
156
+ if (kl === "target" || kl === "to")
157
+ networkFields.target = key;
158
+ if (kl === "value" || kl === "weight" || kl === "amount")
159
+ networkFields.value = key;
160
+ // Detect hierarchy fields
161
+ if (kl === "children" || kl === "values")
162
+ hierarchyFields.children = key;
163
+ if (kl === "parent")
164
+ hierarchyFields.parent = key;
165
+ }
166
+ const hasTime = dateFields.length > 0;
167
+ const hasCat = stringFields.length > 0;
168
+ const hasNum = numericFields.length > 0;
169
+ const hasGeo = geoFields.lat && geoFields.lon;
170
+ const hasNetwork = networkFields.source && networkFields.target;
171
+ const hasHierarchy = hierarchyFields.children || hierarchyFields.parent;
172
+ // Network data
173
+ if (hasNetwork && (!intent || intent === "network")) {
174
+ const src = networkFields.source;
175
+ const tgt = networkFields.target;
176
+ if (networkFields.value) {
177
+ suggestions.push({
178
+ component: "SankeyDiagram",
179
+ confidence: "high",
180
+ reason: `Data has ${src}→${tgt} with ${networkFields.value} — ideal for flow visualization`,
181
+ props: { edges: "data", sourceAccessor: `"${src}"`, targetAccessor: `"${tgt}"`, valueAccessor: `"${networkFields.value}"` },
182
+ });
183
+ }
184
+ suggestions.push({
185
+ component: "ForceDirectedGraph",
186
+ confidence: networkFields.value ? "medium" : "high",
187
+ reason: `Data has ${src}→${tgt} edges — force layout shows network structure. Nodes are auto-inferred from edges when not provided.`,
188
+ props: { edges: "data", sourceAccessor: `"${src}"`, targetAccessor: `"${tgt}"` },
189
+ });
190
+ }
191
+ // Hierarchy data
192
+ if (hasHierarchy && (!intent || intent === "hierarchy")) {
193
+ suggestions.push({
194
+ component: "Treemap",
195
+ confidence: "high",
196
+ reason: `Data has nested ${hierarchyFields.children || "parent"} structure — treemap shows hierarchical proportions`,
197
+ props: { data: "rootObject", childrenAccessor: `"${hierarchyFields.children || "children"}"`, ...(numericFields[0] ? { valueAccessor: `"${numericFields[0]}"` } : {}) },
198
+ });
199
+ suggestions.push({
200
+ component: "TreeDiagram",
201
+ confidence: "medium",
202
+ reason: "Tree layout shows hierarchical relationships",
203
+ props: { data: "rootObject", childrenAccessor: `"${hierarchyFields.children || "children"}"` },
204
+ });
205
+ }
206
+ // Geographic data
207
+ if (hasGeo && (!intent || intent === "geographic")) {
208
+ const sizeField = numericFields.find(f => f !== geoFields.lat && f !== geoFields.lon);
209
+ suggestions.push({
210
+ component: "ProportionalSymbolMap",
211
+ confidence: "high",
212
+ reason: `Data has ${geoFields.lat}/${geoFields.lon} coordinates — map shows spatial distribution`,
213
+ props: { points: "data", xAccessor: `"${geoFields.lon}"`, yAccessor: `"${geoFields.lat}"`, ...(sizeField ? { sizeBy: `"${sizeField}"` } : {}) },
214
+ });
215
+ }
216
+ // Time series
217
+ if (hasTime && hasNum && (!intent || intent === "trend")) {
218
+ const timeField = dateFields[0];
219
+ const valueField = numericFields[0];
220
+ suggestions.push({
221
+ component: "LineChart",
222
+ confidence: "high",
223
+ reason: `Data has dates (${timeField}) and numeric values (${valueField}) — line chart shows trends over time`,
224
+ props: { data: "data", xAccessor: `"${timeField}"`, yAccessor: `"${valueField}"`, ...(hasCat ? { lineBy: `"${stringFields[0]}"`, colorBy: `"${stringFields[0]}"` } : {}) },
225
+ });
226
+ if (hasCat) {
227
+ suggestions.push({
228
+ component: "StackedAreaChart",
229
+ confidence: "medium",
230
+ reason: `Multiple categories (${stringFields[0]}) over time — stacked area shows composition trends`,
231
+ props: { data: "data", xAccessor: `"${timeField}"`, yAccessor: `"${valueField}"`, areaBy: `"${stringFields[0]}"`, colorBy: `"${stringFields[0]}"` },
232
+ });
233
+ }
234
+ }
235
+ // Categorical + numeric
236
+ if (hasCat && hasNum && (!intent || intent === "comparison" || intent === "composition" || intent === "distribution")) {
237
+ const catField = stringFields[0];
238
+ const valField = numericFields[0];
239
+ if (!intent || intent === "comparison") {
240
+ suggestions.push({
241
+ component: "BarChart",
242
+ confidence: hasTime ? "medium" : "high",
243
+ reason: `Categorical field (${catField}) with values (${valField}) — bar chart for comparison`,
244
+ props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"` },
245
+ });
86
246
  }
247
+ if (stringFields.length >= 2 && (!intent || intent === "composition")) {
248
+ suggestions.push({
249
+ component: "StackedBarChart",
250
+ confidence: "medium",
251
+ reason: `Two categorical fields (${stringFields.join(", ")}) — stacked bar shows composition within categories`,
252
+ props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"`, stackBy: `"${stringFields[1]}"` },
253
+ });
254
+ }
255
+ if (data.length >= 10 && (!intent || intent === "distribution")) {
256
+ suggestions.push({
257
+ component: "Histogram",
258
+ confidence: "medium",
259
+ reason: `${data.length}+ data points — histogram shows value distribution`,
260
+ props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"` },
261
+ });
262
+ }
263
+ if (!intent || intent === "composition") {
264
+ const uniqueCats = new Set(data.map(d => d[catField])).size;
265
+ if (uniqueCats <= 8) {
266
+ suggestions.push({
267
+ component: "DonutChart",
268
+ confidence: "medium",
269
+ reason: `${uniqueCats} categories — donut chart shows proportional composition`,
270
+ props: { data: "data", categoryAccessor: `"${catField}"`, valueAccessor: `"${valField}"` },
271
+ });
272
+ }
273
+ }
274
+ }
275
+ // Two numeric fields → scatterplot
276
+ if (numericFields.length >= 2 && (!intent || intent === "relationship")) {
277
+ const xField = numericFields[0];
278
+ const yField = numericFields[1];
279
+ suggestions.push({
280
+ component: "Scatterplot",
281
+ confidence: "high",
282
+ reason: `Two numeric fields (${xField}, ${yField}) — scatterplot shows relationships`,
283
+ props: { data: "data", xAccessor: `"${xField}"`, yAccessor: `"${yField}"`, ...(hasCat ? { colorBy: `"${stringFields[0]}"` } : {}), ...(numericFields[2] ? { sizeBy: `"${numericFields[2]}"` } : {}) },
284
+ });
285
+ if (numericFields.length >= 3) {
286
+ suggestions.push({
287
+ component: "BubbleChart",
288
+ confidence: "medium",
289
+ reason: `Three numeric fields — bubble chart adds size dimension to scatter`,
290
+ props: { data: "data", xAccessor: `"${xField}"`, yAccessor: `"${yField}"`, sizeBy: `"${numericFields[2]}"` },
291
+ });
292
+ }
293
+ if (numericFields.length >= 2 && hasCat) {
294
+ suggestions.push({
295
+ component: "Heatmap",
296
+ confidence: "medium",
297
+ reason: `Numeric values across dimensions — heatmap shows density/intensity`,
298
+ props: { data: "data", xAccessor: `"${xField}"`, yAccessor: `"${hasCat ? stringFields[0] : yField}"`, valueAccessor: `"${hasCat ? numericFields[0] : numericFields[2] || yField}"` },
299
+ });
300
+ }
301
+ }
302
+ // Fallback
303
+ if (suggestions.length === 0) {
304
+ const fieldSummary = `Fields: ${keys.join(", ")} (${numericFields.length} numeric, ${stringFields.length} categorical, ${dateFields.length} date)`;
87
305
  return {
88
- content: [{ type: "text", text: result.svg }],
306
+ 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
307
  };
308
+ }
309
+ // Format output
310
+ const lines = suggestions.map((s, i) => {
311
+ const propsStr = Object.entries(s.props).map(([k, v]) => `${k}=${v}`).join(" ");
312
+ return `${i + 1}. **${s.component}** (${s.confidence} confidence)\n ${s.reason}\n \`<${s.component} ${propsStr} />\``;
90
313
  });
91
- }
92
- // ── Generic renderChart tool ─────────────────────────────────────────────
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) => {
314
+ return {
315
+ content: [{ type: "text", text: lines.join("\n\n") }],
316
+ };
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) => {
96
323
  const component = args.component;
97
- const props = (args.props || args);
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
+ }
98
333
  if (!component) {
99
334
  return {
100
- content: [{ type: "text", text: "Missing 'component' field. Provide { component: 'LineChart', props: { ... } }." }],
335
+ content: [{ type: "text", text: `Missing 'component' field. Provide { component: '<name>', props: { ... } }. Available: ${componentNames.join(", ")}` }],
336
+ isError: true,
337
+ };
338
+ }
339
+ if (!componentRegistry_1.COMPONENT_REGISTRY[component]) {
340
+ return {
341
+ content: [{ type: "text", text: `Unknown component "${component}". Available: ${componentNames.join(", ")}` }],
101
342
  isError: true,
102
343
  };
103
344
  }
@@ -117,7 +358,14 @@ server.tool("renderChart", "Render any Semiotic chart to static SVG. Pass { comp
117
358
  // actionable fix instructions.
118
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) => {
119
360
  const component = args.component;
120
- const props = (args.props || args);
361
+ let props;
362
+ if (args.props) {
363
+ props = args.props;
364
+ }
365
+ else {
366
+ const { component: _, ...rest } = args;
367
+ props = rest;
368
+ }
121
369
  if (!component) {
122
370
  return {
123
371
  content: [{ type: "text", text: "Missing 'component' field. Provide { component: 'LineChart', props: { ... } }." }],
@@ -142,6 +390,33 @@ server.tool("diagnoseConfig", "Diagnose a Semiotic chart configuration for commo
142
390
  isError: true,
143
391
  };
144
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) => {
398
+ const title = args.title;
399
+ const body = args.body;
400
+ const labels = args.labels;
401
+ if (!title) {
402
+ return {
403
+ content: [{ type: "text", text: "Missing 'title' field. Provide { title: 'Bug: ...', body: '...', labels?: ['bug'] }." }],
404
+ isError: true,
405
+ };
406
+ }
407
+ const params = new URLSearchParams();
408
+ params.set("title", title);
409
+ if (body)
410
+ params.set("body", body);
411
+ if (labels) {
412
+ const labelList = Array.isArray(labels) ? labels.join(",") : labels;
413
+ params.set("labels", labelList);
414
+ }
415
+ const url = `https://github.com/${REPO}/issues/new?${params.toString()}`;
416
+ return {
417
+ content: [{ type: "text", text: `Open this URL to submit the issue:\n\n${url}` }],
418
+ };
419
+ });
145
420
  // Start the server
146
421
  async function main() {
147
422
  const transport = new stdio_js_1.StdioServerTransport();
package/ai/schema.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "name": "semiotic",
4
- "version": "3.1.0",
4
+ "version": "3.1.1",
5
5
  "description": "React data visualization library for charts, networks, and beyond",
6
6
  "tools": [
7
7
  {
@@ -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;
@@ -3,14 +3,15 @@ interface SafeRenderProps {
3
3
  componentName: string;
4
4
  width: number;
5
5
  height: number;
6
+ chartProps?: Record<string, unknown>;
6
7
  children: React.ReactNode;
7
8
  }
8
9
  /**
9
10
  * Wraps a chart's rendered output with an error boundary.
10
- * If the chart throws during render, shows a visible error box
11
- * with the component name and error message instead of crashing the page.
11
+ * If the chart throws during render, runs diagnoseConfig to produce
12
+ * actionable fix suggestions alongside the error message.
12
13
  */
13
- export declare function SafeRender({ componentName, width, height, children }: SafeRenderProps): React.JSX.Element;
14
+ export declare function SafeRender({ componentName, width, height, chartProps, children }: SafeRenderProps): React.JSX.Element;
14
15
  /**
15
16
  * Renders a "No data available" placeholder when data is empty.
16
17
  * Returns null when data is present or emptyContent is `false`.