semiotic 3.7.1 → 3.7.3

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
@@ -12,6 +12,17 @@ Simple charts in 5 lines. Network graphs, streaming data, and coordinated
12
12
  dashboards when you need them. Structured schemas and an MCP server so
13
13
  AI coding assistants generate correct chart code on the first try.
14
14
 
15
+ ## What's New in 3.7.3
16
+
17
+ 3.7.3 is a hosted ChatGPT Apps patch release:
18
+
19
+ - `semiotic-mcp --http` can now serve the OpenAI Apps domain verification challenge from
20
+ `/.well-known/openai-apps-challenge` when `OPENAI_APPS_CHALLENGE_TOKEN` is configured.
21
+ - The Cloud Run wrapper docs now include the exact Challenge Base URL, token environment variable,
22
+ and `curl` check for ChatGPT Apps verification.
23
+ - MCP HTTP tests now cover the Apps challenge route while preserving the unauthenticated OAuth
24
+ discovery 404 behavior.
25
+
15
26
  ```jsx
16
27
  import { LineChart } from "semiotic/xy"
17
28
 
@@ -411,10 +422,16 @@ Add to your MCP client config (e.g. `claude_desktop_config.json` for Claude Desk
411
422
  }
412
423
  ```
413
424
 
414
- No API keys or authentication required. The server runs locally via stdio. HTTP mode is also available for inspectors, web clients, and ChatGPT Apps SDK experiments: `npx semiotic-mcp --http --port 3001`.
425
+ No API keys or authentication required. The server runs locally via stdio. HTTP mode is also available for inspectors, web clients, and ChatGPT Apps SDK experiments: `npx semiotic-mcp --http --port 3001`. Since 3.7.2, HTTP mode is stateless: each request gets a fresh read-only MCP server + transport, so it can autoscale on serverless hosts without sticky sessions.
415
426
 
416
427
  For ChatGPT developer mode, expose the HTTP endpoint over HTTPS with a tunnel and create a connector that points at `https://<your-tunnel>/mcp`. The experimental Apps SDK surface is `renderInteractiveChart`, which returns a `text/html;profile=mcp-app` widget template plus a hidden SVG payload rendered by Semiotic on the MCP server.
417
428
 
429
+ For a hosted deployment, see `deploy/cloud-run`. The wrapper runs the published `semiotic-mcp`
430
+ binary, exposes `/mcp` plus health endpoints, and supports `MCP_ALLOWED_HOSTS` for production
431
+ host-header allowlisting. For ChatGPT Apps domain verification, set
432
+ `OPENAI_APPS_CHALLENGE_TOKEN` so HTTP mode serves the raw token from
433
+ `/.well-known/openai-apps-challenge`.
434
+
418
435
  ### Tools
419
436
 
420
437
  | Tool | Description |
@@ -15,13 +15,17 @@ const DOC_MARKER_START = "<!-- semiotic-behavior-contracts:start -->"
15
15
  const DOC_MARKER_END = "<!-- semiotic-behavior-contracts:end -->"
16
16
 
17
17
  // Components whose static config requires `data` are derived from
18
- // `ai/schema.json` rather than maintained as a hand-curated list. The schema
19
- // already declares which components require data — duplicating that here led
20
- // to drift (Heatmap, FunnelChart, MinimapChart, ScatterplotMatrix, and the
21
- // hierarchy charts were schema-required but missing from the local list,
22
- // which made `dataRequiredForUsageMode` incorrectly return `false` and
23
- // suppressed the "data is required" error in --doctor / MCP diagnoseConfig
24
- // even when usageMode wasn't `push`).
18
+ // `ai/schema.json` rather than maintained as a hand-curated list.
19
+ //
20
+ // A component needs data in STATIC usage if its schema declares a `data` input
21
+ // prop NOT merely if `data` is in the `required` array. `required` lists the
22
+ // semantic accessors a chart needs (highAccessor, subcategoryAccessor, series,
23
+ // …), and several data-driven charts don't put `data` itself there:
24
+ // CandlestickChart, MultiAxisLineChart, QuadrantChart, DifferenceChart,
25
+ // LikertChart, and SwimlaneChart. Keying off `required.includes("data")` missed
26
+ // exactly those — they'd render blank with no data yet passed --doctor / MCP
27
+ // diagnoseConfig as "OK" in static mode. Keying off the presence of a `data`
28
+ // property catches them (and still includes the charts that DO list `data`).
25
29
  //
26
30
  // `STATIC_DATA_COMPONENTS` stays exported as a Set for test/legacy callers
27
31
  // that probe the surface, and is rebuilt from disk at module load time.
@@ -41,8 +45,10 @@ function loadStaticDataComponentsFromSchema() {
41
45
  const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"))
42
46
  const out = new Set()
43
47
  for (const tool of schema.tools || []) {
44
- const required = tool.function?.parameters?.required || []
45
- if (required.includes("data")) out.add(tool.function.name)
48
+ const properties = tool.function?.parameters?.properties || {}
49
+ // Data-driven if the schema declares a `data` input prop, regardless of
50
+ // whether `data` appears in `required` (see note above).
51
+ if ("data" in properties) out.add(tool.function.name)
46
52
  }
47
53
  if (out.size > 0) return out
48
54
  } catch {
@@ -114,6 +120,10 @@ const PUSH_MODE_COMPONENTS = [
114
120
  "Scatterplot",
115
121
  "BubbleChart",
116
122
  "ConnectedScatterplot",
123
+ "CandlestickChart",
124
+ "MultiAxisLineChart",
125
+ "QuadrantChart",
126
+ "DifferenceChart",
117
127
  "BarChart",
118
128
  "StackedBarChart",
119
129
  "GroupedBarChart",
package/ai/cli.js CHANGED
@@ -163,6 +163,21 @@ function validatePropsWithSchema(componentName, props, usageMode = "static") {
163
163
  }
164
164
  }
165
165
 
166
+ // Array-shape charts that declare a `data` schema prop need it in static
167
+ // usage even when "data" isn't in `required` (those lists hold semantic
168
+ // accessors). Without this, --doctor passed dataless static CandlestickChart /
169
+ // MultiAxisLineChart / QuadrantChart / DifferenceChart / SwimlaneChart /
170
+ // LikertChart configs that render blank. dataRequiredForUsageMode is true for
171
+ // them in static and false in push, mirroring the MCP diagnoseConfig path.
172
+ if (
173
+ "data" in properties &&
174
+ !required.includes("data") &&
175
+ dataRequiredForUsageMode(component.name, usageMode) &&
176
+ (props.data === undefined || props.data === null)
177
+ ) {
178
+ errors.push(`"data" is required for ${component.name}.`)
179
+ }
180
+
166
181
  for (const [propName, value] of Object.entries(props)) {
167
182
  if (value === undefined || value === null) continue
168
183
  const propSchema = properties[propName]
@@ -7,7 +7,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
9
  var __commonJS = (cb, mod) => function __require() {
10
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ try {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ } catch (e) {
13
+ throw mod = 0, e;
14
+ }
11
15
  };
12
16
  var __export = (target, all) => {
13
17
  for (var name in all)
@@ -7030,7 +7034,7 @@ var require_chartSuggestions = __commonJS({
7030
7034
  "ai/chartSuggestions.cjs"(exports2, module2) {
7031
7035
  "use strict";
7032
7036
  var path2 = require("path");
7033
- var VALID_INTENTS = [
7037
+ var VALID_INTENTS2 = [
7034
7038
  "comparison",
7035
7039
  "trend",
7036
7040
  "distribution",
@@ -7163,10 +7167,10 @@ var require_chartSuggestions = __commonJS({
7163
7167
  const data = args.data;
7164
7168
  const intent = args.intent;
7165
7169
  const capabilities = args.capabilities;
7166
- if (intent && !VALID_INTENTS.includes(intent)) {
7170
+ if (intent && !VALID_INTENTS2.includes(intent)) {
7167
7171
  return {
7168
7172
  ok: false,
7169
- error: `Unknown intent "${intent}". Expected one of: ${VALID_INTENTS.join(", ")}.`
7173
+ error: `Unknown intent "${intent}". Expected one of: ${VALID_INTENTS2.join(", ")}.`
7170
7174
  };
7171
7175
  }
7172
7176
  if (capabilities) {
@@ -7384,7 +7388,7 @@ ${result.filteredOut.map((s) => `- ${s.component}: ${s.reason}`).join("\n")}
7384
7388
 
7385
7389
  Relax the capability constraints, or use getSchema to browse alternatives.` : `
7386
7390
 
7387
- Try providing intent ('${VALID_INTENTS.join("', '")}') to narrow recommendations, or use getSchema to browse available components.`;
7391
+ Try providing intent ('${VALID_INTENTS2.join("', '")}') to narrow recommendations, or use getSchema to browse available components.`;
7388
7392
  return `Could not confidently recommend a chart type.
7389
7393
 
7390
7394
  ${result.fieldSummary}${tail}`;
@@ -7423,7 +7427,7 @@ For accessibility, use \`colorScheme={COLOR_BLIND_SAFE_CATEGORICAL}\` (import fr
7423
7427
  return Object.entries(capabilities).filter(([, v]) => v != null).map(([k, v]) => `${k}=${v}`).join(", ");
7424
7428
  }
7425
7429
  module2.exports = {
7426
- VALID_INTENTS,
7430
+ VALID_INTENTS: VALID_INTENTS2,
7427
7431
  VALID_CAPABILITY_KEYS,
7428
7432
  formatSuggestionReport: formatSuggestionReport2,
7429
7433
  suggestCharts: suggestCharts2,
@@ -7453,8 +7457,8 @@ var require_behaviorContracts = __commonJS({
7453
7457
  const schema2 = JSON.parse(fs2.readFileSync(schemaPath2, "utf8"));
7454
7458
  const out = /* @__PURE__ */ new Set();
7455
7459
  for (const tool of schema2.tools || []) {
7456
- const required2 = tool.function?.parameters?.required || [];
7457
- if (required2.includes("data")) out.add(tool.function.name);
7460
+ const properties = tool.function?.parameters?.properties || {};
7461
+ if ("data" in properties) out.add(tool.function.name);
7458
7462
  }
7459
7463
  if (out.size > 0) return out;
7460
7464
  } catch {
@@ -7520,6 +7524,10 @@ var require_behaviorContracts = __commonJS({
7520
7524
  "Scatterplot",
7521
7525
  "BubbleChart",
7522
7526
  "ConnectedScatterplot",
7527
+ "CandlestickChart",
7528
+ "MultiAxisLineChart",
7529
+ "QuadrantChart",
7530
+ "DifferenceChart",
7523
7531
  "BarChart",
7524
7532
  "StackedBarChart",
7525
7533
  "GroupedBarChart",
@@ -32435,21 +32443,25 @@ ${errors.join("\n")}`
32435
32443
  // ai/mcp-server.ts
32436
32444
  var import_server2 = require("semiotic/server");
32437
32445
  var import_ai3 = require("semiotic/ai");
32446
+ var import_componentMetadata = __toESM(require_componentMetadata());
32447
+ var import_chartSuggestions = __toESM(require_chartSuggestions());
32448
+ var import_behaviorContracts = __toESM(require_behaviorContracts());
32438
32449
  var {
32439
32450
  componentIndexFromSchema,
32440
32451
  metadataForComponent
32441
- } = require_componentMetadata();
32452
+ } = import_componentMetadata.default;
32442
32453
  var {
32443
32454
  formatSuggestionReport,
32444
- suggestCharts
32445
- } = require_chartSuggestions();
32455
+ suggestCharts,
32456
+ VALID_INTENTS
32457
+ } = import_chartSuggestions.default;
32446
32458
  var {
32447
32459
  BEHAVIOR_CONTRACTS,
32448
32460
  behaviorContractsFor,
32449
32461
  dataRequiredForUsageMode,
32450
32462
  formatDoctorBehaviorContracts,
32451
32463
  normalizeUsageMode
32452
- } = require_behaviorContracts();
32464
+ } = import_behaviorContracts.default;
32453
32465
  var schemaPath = path.resolve(__dirname, "../schema.json");
32454
32466
  var schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
32455
32467
  var schemaByComponent = {};
@@ -32824,8 +32836,23 @@ ${JSON.stringify(contracts, null, 2)}` : "";
32824
32836
  ${JSON.stringify(entry, null, 2)}${contractText}` }]
32825
32837
  };
32826
32838
  }
32839
+ var SUGGEST_INTENT_ALIASES = {
32840
+ "compare-series": "comparison",
32841
+ "compare-categories": "comparison",
32842
+ "rank": "comparison",
32843
+ "part-to-whole": "composition",
32844
+ "composition-over-time": "composition",
32845
+ "correlation": "relationship",
32846
+ "flow": "network",
32847
+ "geo": "geographic",
32848
+ "outlier-detection": "distribution",
32849
+ "change-detection": "trend"
32850
+ };
32827
32851
  async function suggestChartHandler(args) {
32828
- const result = suggestCharts(args);
32852
+ let intent = args.intent;
32853
+ if (intent && SUGGEST_INTENT_ALIASES[intent]) intent = SUGGEST_INTENT_ALIASES[intent];
32854
+ if (intent && !VALID_INTENTS.includes(intent)) intent = void 0;
32855
+ const result = suggestCharts({ ...args, intent });
32829
32856
  const content = [{ type: "text", text: formatSuggestionReport(result) }];
32830
32857
  if (!result.ok) {
32831
32858
  return { content, isError: true, structuredContent: result };
@@ -32873,6 +32900,10 @@ async function renderChartHandler(args) {
32873
32900
  ${JSON.stringify(evidence, null, 2)}`
32874
32901
  };
32875
32902
  } catch {
32903
+ evidenceBlock = {
32904
+ type: "text",
32905
+ text: `Render evidence: unavailable for ${component} (no server render config). The SVG above is the validated React render; mark-count / domain evidence is only produced for components with a server render path.`
32906
+ };
32876
32907
  }
32877
32908
  if (theme && Object.keys(theme).length > 0) {
32878
32909
  const validVars = Object.entries(theme).filter(([k]) => k.startsWith("--semiotic-")).map(([k, v]) => `${k}: ${v}`).join("; ");
@@ -33046,23 +33077,26 @@ async function reportIssueHandler(args) {
33046
33077
  ${url2}` }]
33047
33078
  };
33048
33079
  }
33049
- var THEME_PRESET_NAMES = [
33050
- "light",
33051
- "dark",
33052
- "high-contrast",
33053
- "pastels",
33054
- "pastels-dark",
33055
- "bi-tool",
33056
- "bi-tool-dark",
33057
- "italian",
33058
- "italian-dark",
33059
- "tufte",
33060
- "tufte-dark",
33061
- "journalist",
33062
- "journalist-dark",
33063
- "playful",
33064
- "playful-dark"
33065
- ];
33080
+ var THEME_PRESETS = {
33081
+ "light": "LIGHT_THEME",
33082
+ "dark": "DARK_THEME",
33083
+ "high-contrast": "HIGH_CONTRAST_THEME",
33084
+ "pastels": "PASTELS_LIGHT",
33085
+ "pastels-dark": "PASTELS_DARK",
33086
+ "bi-tool": "BI_TOOL_LIGHT",
33087
+ "bi-tool-dark": "BI_TOOL_DARK",
33088
+ "italian": "ITALIAN_LIGHT",
33089
+ "italian-dark": "ITALIAN_DARK",
33090
+ "tufte": "TUFTE_LIGHT",
33091
+ "tufte-dark": "TUFTE_DARK",
33092
+ "journalist": "JOURNALIST_LIGHT",
33093
+ "journalist-dark": "JOURNALIST_DARK",
33094
+ "playful": "PLAYFUL_LIGHT",
33095
+ "playful-dark": "PLAYFUL_DARK",
33096
+ "carbon": "CARBON_LIGHT",
33097
+ "carbon-dark": "CARBON_DARK"
33098
+ };
33099
+ var THEME_PRESET_NAMES = Object.keys(THEME_PRESETS);
33066
33100
  async function applyThemeHandler(args) {
33067
33101
  const name = args.name;
33068
33102
  if (!name) {
@@ -33082,6 +33116,7 @@ Dark-mode presets: ${THEME_PRESET_NAMES.filter((n) => n.includes("dark")).join("
33082
33116
  isError: true
33083
33117
  };
33084
33118
  }
33119
+ const exportName = THEME_PRESETS[name];
33085
33120
  const usage = [
33086
33121
  `## Theme: "${name}"`,
33087
33122
  "",
@@ -33095,24 +33130,23 @@ Dark-mode presets: ${THEME_PRESET_NAMES.filter((n) => n.includes("dark")).join("
33095
33130
  "",
33096
33131
  "### Option 2: Import the theme object",
33097
33132
  "```jsx",
33098
- `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"`,
33099
- `<ThemeProvider theme={themeObject}>`,
33133
+ `import { ${exportName} } from "semiotic/themes"`,
33134
+ `<ThemeProvider theme={${exportName}}>`,
33100
33135
  ` <BarChart ... />`,
33101
33136
  `</ThemeProvider>`,
33102
33137
  "```",
33103
33138
  "",
33104
33139
  "### Option 3: CSS custom properties (no React required)",
33105
33140
  "```jsx",
33106
- `import { themeToCSS } from "semiotic/themes"`,
33107
- `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"`,
33108
- `const css = themeToCSS(themeObject, ".my-charts")`,
33141
+ `import { themeToCSS, ${exportName} } from "semiotic/themes"`,
33142
+ `const css = themeToCSS(${exportName}, ".my-charts")`,
33109
33143
  "// Outputs CSS custom properties string for embedding in a stylesheet",
33110
33144
  "```",
33111
33145
  "",
33112
33146
  "### Option 4: Design tokens JSON",
33113
33147
  "```jsx",
33114
- `import { themeToTokens } from "semiotic/themes"`,
33115
- `const tokens = themeToTokens(themeObject)`,
33148
+ `import { themeToTokens, ${exportName} } from "semiotic/themes"`,
33149
+ `const tokens = themeToTokens(${exportName})`,
33116
33150
  "// Style Dictionary / DTCG-compatible token format",
33117
33151
  "```",
33118
33152
  "",
@@ -33403,6 +33437,12 @@ async function groundChartHandler(args) {
33403
33437
  structuredContent: grounding
33404
33438
  };
33405
33439
  }
33440
+ var READ_ONLY_TOOL_ANNOTATIONS = {
33441
+ readOnlyHint: true,
33442
+ destructiveHint: false,
33443
+ idempotentHint: true,
33444
+ openWorldHint: false
33445
+ };
33406
33446
  function createServer2() {
33407
33447
  const srv = new McpServer({
33408
33448
  name: "semiotic",
@@ -33547,14 +33587,15 @@ function createServer2() {
33547
33587
  "getSchema",
33548
33588
  `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.`,
33549
33589
  { component: external_exports3.string().optional().describe("Component name, e.g. 'LineChart'. Omit to list all.") },
33590
+ READ_ONLY_TOOL_ANNOTATIONS,
33550
33591
  getSchemaHandler
33551
33592
  );
33552
33593
  srv.tool(
33553
33594
  "suggestChart",
33554
- "Recommend Semiotic chart types for a given data sample. Pass { data: [...] } with 1-5 sample objects. Optionally pass intent to narrow suggestions, or capabilities to require/forbid features (push API, linked hover, SSR, selection, legend). Returns ranked recommendations with example props; charts that don't satisfy the capability constraints are dropped.",
33595
+ "Lightweight heuristic chart recommender for a small data sample (1-5 rows) with capability filtering (push API, linked hover, SSR, selection, legend). Returns ranked recommendations with example props. For richer capability-descriptor ranking (scores, reasons, caveats) and the full 13-intent taxonomy, prefer `suggestCharts` (plural).",
33555
33596
  {
33556
33597
  data: external_exports3.array(external_exports3.record(external_exports3.string(), external_exports3.unknown())).min(1).max(5).describe("1-5 sample data objects"),
33557
- intent: external_exports3.enum(["comparison", "trend", "distribution", "relationship", "composition", "geographic", "network", "hierarchy"]).optional().describe("Visualization intent to narrow suggestions"),
33598
+ intent: external_exports3.string().optional().describe("Visualization intent. Accepts this engine's intents (comparison, trend, distribution, relationship, composition, geographic, network, hierarchy) AND the richer suggestCharts taxonomy (compare-categories, part-to-whole, correlation, flow, geo, rank, \u2026), which is translated automatically; an unrecognized intent is ignored rather than rejected."),
33558
33599
  capabilities: external_exports3.object({
33559
33600
  push: external_exports3.boolean().optional().describe("Require ref-based push API (live streaming via ref.current.push())"),
33560
33601
  linkedHover: external_exports3.boolean().optional().describe("Require cross-chart linked hover support"),
@@ -33567,6 +33608,7 @@ function createServer2() {
33567
33608
  // validation from being unreachable from MCP callers.
33568
33609
  }).strict().optional().describe("Capability constraints \u2014 set a key to true to require, false to forbid. Unset keys are ignored.")
33569
33610
  },
33611
+ READ_ONLY_TOOL_ANNOTATIONS,
33570
33612
  suggestChartHandler
33571
33613
  );
33572
33614
  srv.tool(
@@ -33578,6 +33620,7 @@ function createServer2() {
33578
33620
  theme: external_exports3.record(external_exports3.string(), external_exports3.string()).optional().describe("CSS custom properties for theming, e.g. { '--semiotic-bg': '#1a1a2e', '--semiotic-text': '#ededed' }. Only --semiotic-* variables are applied."),
33579
33621
  format: external_exports3.enum(["svg", "png"]).optional().describe("Output format: 'svg' (default) returns SVG markup, 'png' returns a Base64-encoded PNG image. PNG requires the 'sharp' package.")
33580
33622
  },
33623
+ READ_ONLY_TOOL_ANNOTATIONS,
33581
33624
  renderChartHandler
33582
33625
  );
33583
33626
  srv.registerTool(
@@ -33620,6 +33663,7 @@ function createServer2() {
33620
33663
  props: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional().describe("Chart props object, e.g. { data: [...], xAccessor: 'x' }."),
33621
33664
  usageMode: external_exports3.enum(["static", "push", "renderChart", "server"]).optional().describe("Validation mode. Use 'push' for ref-based React HOCs that omit data; use 'static' or omit for renderChart/MCP/static data configs.")
33622
33665
  },
33666
+ READ_ONLY_TOOL_ANNOTATIONS,
33623
33667
  diagnoseConfigHandler
33624
33668
  );
33625
33669
  srv.tool(
@@ -33632,6 +33676,7 @@ function createServer2() {
33632
33676
  describe: external_exports3.boolean().optional().describe("True if ChartContainer's describe option (auto-generated L1\u2013L3 description via describeChart) is enabled \u2014 passes the 'features described' heuristic."),
33633
33677
  navigable: external_exports3.boolean().optional().describe("True if ChartContainer's navigable option (structured navigation tree via buildNavigationTree) is enabled \u2014 passes the 'navigable structure' heuristic.")
33634
33678
  },
33679
+ READ_ONLY_TOOL_ANNOTATIONS,
33635
33680
  auditAccessibilityHandler
33636
33681
  );
33637
33682
  srv.tool(
@@ -33642,6 +33687,7 @@ function createServer2() {
33642
33687
  body: external_exports3.string().optional().describe("Issue body with details, reproduction steps, diagnoseConfig output"),
33643
33688
  labels: external_exports3.union([external_exports3.array(external_exports3.string()), external_exports3.string()]).optional().describe("GitHub labels, e.g. ['bug'] or 'bug'")
33644
33689
  },
33690
+ READ_ONLY_TOOL_ANNOTATIONS,
33645
33691
  reportIssueHandler
33646
33692
  );
33647
33693
  srv.tool(
@@ -33650,6 +33696,7 @@ function createServer2() {
33650
33696
  {
33651
33697
  name: external_exports3.string().optional().describe("Theme preset name, e.g. 'tufte', 'pastels-dark', 'bi-tool'. Omit to list all available themes.")
33652
33698
  },
33699
+ READ_ONLY_TOOL_ANNOTATIONS,
33653
33700
  applyThemeHandler
33654
33701
  );
33655
33702
  srv.tool(
@@ -33660,6 +33707,7 @@ function createServer2() {
33660
33707
  props: external_exports3.record(external_exports3.string(), external_exports3.unknown()).describe("The full chart props including data"),
33661
33708
  query: external_exports3.string().optional().describe("A natural language question about the chart data")
33662
33709
  },
33710
+ READ_ONLY_TOOL_ANNOTATIONS,
33663
33711
  interrogateChartHandler
33664
33712
  );
33665
33713
  srv.tool(
@@ -33669,6 +33717,7 @@ function createServer2() {
33669
33717
  component: external_exports3.string().describe("Chart component name, e.g. 'LineChart'"),
33670
33718
  props: external_exports3.record(external_exports3.string(), external_exports3.unknown()).describe("The full chart props including data")
33671
33719
  },
33720
+ READ_ONLY_TOOL_ANNOTATIONS,
33672
33721
  groundChartHandler
33673
33722
  );
33674
33723
  srv.tool(
@@ -33689,6 +33738,7 @@ function createServer2() {
33689
33738
  intent: external_exports3.union([external_exports3.string(), external_exports3.array(external_exports3.string())]).optional().describe("Ranking intent."),
33690
33739
  maxResults: external_exports3.number().int().min(1).max(20).optional()
33691
33740
  },
33741
+ READ_ONLY_TOOL_ANNOTATIONS,
33692
33742
  suggestStreamChartsHandler
33693
33743
  );
33694
33744
  srv.tool(
@@ -33700,6 +33750,7 @@ function createServer2() {
33700
33750
  maxPanels: external_exports3.number().int().min(1).max(12).optional().describe("Maximum panels (default 6)."),
33701
33751
  diversifyByFamily: external_exports3.boolean().optional().describe("Prefer not to repeat chart families across panels (default true).")
33702
33752
  },
33753
+ READ_ONLY_TOOL_ANNOTATIONS,
33703
33754
  suggestDashboardHandler
33704
33755
  );
33705
33756
  srv.tool(
@@ -33724,6 +33775,7 @@ function createServer2() {
33724
33775
  intent: external_exports3.union([external_exports3.string(), external_exports3.array(external_exports3.string())]).optional(),
33725
33776
  maxResults: external_exports3.number().int().min(1).max(20).optional()
33726
33777
  },
33778
+ READ_ONLY_TOOL_ANNOTATIONS,
33727
33779
  suggestStretchChartsHandler
33728
33780
  );
33729
33781
  srv.tool(
@@ -33735,6 +33787,7 @@ function createServer2() {
33735
33787
  intent: external_exports3.union([external_exports3.string(), external_exports3.array(external_exports3.string())]).optional().describe("User intent \u2014 informs ranking of alternatives when the chart doesn't fit."),
33736
33788
  maxAlternatives: external_exports3.number().int().min(1).max(10).optional().describe("Cap on alternatives returned (default 3).")
33737
33789
  },
33790
+ READ_ONLY_TOOL_ANNOTATIONS,
33738
33791
  repairChartConfigHandler
33739
33792
  );
33740
33793
  srv.tool(
@@ -33761,6 +33814,7 @@ function createServer2() {
33761
33814
  receptionModality: external_exports3.enum(["visual", "screen-reader", "sonified", "agent"]).optional().describe("Reception channel \u2014 see suggestCharts.")
33762
33815
  }).optional().describe("Audience profile \u2014 familiarity, adoption targets, exposure level, and reception modality.")
33763
33816
  },
33817
+ READ_ONLY_TOOL_ANNOTATIONS,
33764
33818
  proposeChartVariantsHandler
33765
33819
  );
33766
33820
  srv.tool(
@@ -33787,6 +33841,7 @@ function createServer2() {
33787
33841
  receptionModality: external_exports3.enum(["visual", "screen-reader", "sonified", "agent"]).optional().describe("Reception channel. A non-visual value down-ranks charts the audience can't receive in that channel (e.g. a many-slice pie for a screen reader) and adds receivability caveats.")
33788
33842
  }).optional().describe("Audience profile \u2014 familiarity, adoption targets, exposure level, and reception modality.")
33789
33843
  },
33844
+ READ_ONLY_TOOL_ANNOTATIONS,
33790
33845
  suggestChartsHandler
33791
33846
  );
33792
33847
  return srv;
@@ -33798,39 +33853,98 @@ var parsedPort = portFlagIndex !== -1 && cliArgs[portFlagIndex + 1] != null ? pa
33798
33853
  var port = Number.isFinite(parsedPort) ? parsedPort : 3001;
33799
33854
  async function main() {
33800
33855
  if (httpMode) {
33801
- const sessions = /* @__PURE__ */ new Map();
33856
+ const allowedHosts = (process.env.MCP_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
33857
+ const openaiAppsChallengeToken = (process.env.OPENAI_APPS_CHALLENGE_TOKEN || "").trim();
33858
+ const healthBody = () => JSON.stringify({
33859
+ status: "ok",
33860
+ name: "semiotic-mcp",
33861
+ version: schema.version || "3.0.0",
33862
+ transport: "streamable-http",
33863
+ mode: "stateless"
33864
+ });
33802
33865
  const httpServer = http.createServer(async (req, res) => {
33803
33866
  res.setHeader("Access-Control-Allow-Origin", "*");
33804
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
33805
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
33806
- res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
33867
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
33868
+ res.setHeader(
33869
+ "Access-Control-Allow-Headers",
33870
+ "Content-Type, Accept, Authorization, mcp-session-id, MCP-Protocol-Version, Last-Event-ID"
33871
+ );
33872
+ res.setHeader("Access-Control-Expose-Headers", "MCP-Protocol-Version");
33807
33873
  if (req.method === "OPTIONS") {
33808
33874
  res.writeHead(204);
33809
33875
  res.end();
33810
33876
  return;
33811
33877
  }
33812
- const sessionId = req.headers["mcp-session-id"];
33813
- if (sessionId && sessions.has(sessionId)) {
33814
- const session = sessions.get(sessionId);
33815
- await session.transport.handleRequest(req, res);
33816
- } else if (!sessionId) {
33817
- const transport = new StreamableHTTPServerTransport({
33818
- sessionIdGenerator: () => `semiotic-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
33878
+ const pathname = (() => {
33879
+ try {
33880
+ return new URL(req.url || "/", "http://localhost").pathname;
33881
+ } catch {
33882
+ return "/";
33883
+ }
33884
+ })();
33885
+ if (allowedHosts.length > 0) {
33886
+ const rawHost = String(req.headers.host || "").trim().toLowerCase();
33887
+ const normalizedHost = rawHost.startsWith("[") ? rawHost.replace(/^\[([^\]]+)\](?::\d+)?$/, "$1") : rawHost.split(":")[0];
33888
+ if (!allowedHosts.includes(rawHost) && !allowedHosts.includes(normalizedHost)) {
33889
+ res.writeHead(403, { "Content-Type": "application/json" });
33890
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32e3, message: "Forbidden host" }, id: null }));
33891
+ return;
33892
+ }
33893
+ }
33894
+ if (req.method === "GET" && (pathname === "/healthz" || pathname === "/health")) {
33895
+ res.writeHead(200, { "Content-Type": "application/json" });
33896
+ res.end(healthBody());
33897
+ return;
33898
+ }
33899
+ if (req.method === "GET" && pathname === "/.well-known/openai-apps-challenge" && openaiAppsChallengeToken) {
33900
+ res.writeHead(200, {
33901
+ "Content-Type": "text/plain; charset=utf-8",
33902
+ "Cache-Control": "no-store"
33819
33903
  });
33820
- const srv = createServer2();
33904
+ res.end(openaiAppsChallengeToken);
33905
+ return;
33906
+ }
33907
+ if (pathname !== "/" && pathname !== "/mcp") {
33908
+ res.writeHead(404, { "Content-Type": "application/json" });
33909
+ res.end(JSON.stringify({ error: "Not found" }));
33910
+ return;
33911
+ }
33912
+ if (req.method === "GET") {
33913
+ res.writeHead(200, { "Content-Type": "application/json" });
33914
+ res.end(healthBody());
33915
+ return;
33916
+ }
33917
+ if (req.method !== "POST") {
33918
+ res.writeHead(405, { "Content-Type": "application/json" });
33919
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32e3, message: "Method not allowed" }, id: null }));
33920
+ return;
33921
+ }
33922
+ const srv = createServer2();
33923
+ const transport = new StreamableHTTPServerTransport({
33924
+ sessionIdGenerator: void 0,
33925
+ enableJsonResponse: true
33926
+ });
33927
+ let torndown = false;
33928
+ const teardown = () => {
33929
+ if (torndown) return;
33930
+ torndown = true;
33931
+ Promise.resolve(transport.close()).catch(() => {
33932
+ });
33933
+ Promise.resolve(srv.close()).catch(() => {
33934
+ });
33935
+ };
33936
+ res.on("close", teardown);
33937
+ try {
33821
33938
  await srv.connect(transport);
33822
- transport.onclose = () => {
33823
- const sid2 = transport.sessionId;
33824
- if (sid2) sessions.delete(sid2);
33825
- };
33826
33939
  await transport.handleRequest(req, res);
33827
- const sid = transport.sessionId;
33828
- if (sid) {
33829
- sessions.set(sid, { server: srv, transport });
33940
+ } catch (err) {
33941
+ console.error("Request handling error:", err);
33942
+ if (!res.headersSent) {
33943
+ res.writeHead(500, { "Content-Type": "application/json" });
33944
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }));
33830
33945
  }
33831
- } else {
33832
- res.writeHead(400, { "Content-Type": "application/json" });
33833
- res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32e3, message: "Unknown session. Send a request without mcp-session-id to start a new session." }, id: null }));
33946
+ } finally {
33947
+ teardown();
33834
33948
  }
33835
33949
  });
33836
33950
  httpServer.listen(port, () => {
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.7.1",
4
+ "version": "3.7.3",
5
5
  "description": "React data visualization library for charts, networks, and beyond",
6
6
  "tools": [
7
7
  {
@@ -1,4 +1,5 @@
1
- import type { NetworkSceneNode, NetworkSceneEdge } from "./networkTypes";
1
+ import type { NetworkSceneNode, NetworkSceneEdge, NetworkCircleNode } from "./networkTypes";
2
+ import type { Quadtree } from "d3-quadtree";
2
3
  export interface NetworkHitResult {
3
4
  type: "node" | "edge";
4
5
  datum: any;
@@ -11,4 +12,4 @@ export interface NetworkHitResult {
11
12
  *
12
13
  * Checks nodes first (they're on top), then edges.
13
14
  */
14
- export declare function findNearestNetworkNode(sceneNodes: NetworkSceneNode[], sceneEdges: NetworkSceneEdge[], px: number, py: number, maxDistance?: number): NetworkHitResult | null;
15
+ export declare function findNearestNetworkNode(sceneNodes: NetworkSceneNode[], sceneEdges: NetworkSceneEdge[], px: number, py: number, maxDistance?: number, nodeQuadtree?: Quadtree<NetworkCircleNode> | null, maxNodeRadius?: number): NetworkHitResult | null;