@barivia/barsom-mcp 0.23.9 → 0.23.11

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
@@ -1,8 +1,8 @@
1
1
  # @barivia/barsom-mcp
2
2
 
3
- MCP proxy for the Barivia Analytics Engine — connects any stdio MCP client (Cursor, Claude Desktop, etc.) to the barSOM cloud API. The npm package is **`@barivia/barsom-mcp`**; many configs label the server **`analytics-engine`** (the MCP server name in the client JSON). **`guide_barsom_workflow`** is the canonical bootstrap: it loads plan-scoped workflow text from the Barivia API when online (tool map, async rules, training modes, SOP). If the API is unreachable, it returns a short offline stub. Core tools are `datasets`, `train` (`map`, `siom_map`, `impute`, `floop_siom` where entitled), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/export/download/recolor), and `inference` (predict/batch_predict/impute_column/compare/project_columns/transition_flow/report). **Optional MCP App tools** (`training_monitor`, `results_explorer`) add embedded or localhost viz; they are **not required** to complete upload → train → poll → results, though `results_explorer` is the preferred way to browse figures.
3
+ MCP proxy for the Barivia Analytics Engine — connects any stdio MCP client (Cursor, Claude Desktop, etc.) to the barSOM cloud API. The npm package is **`@barivia/barsom-mcp`**; many configs label the server **`analytics-engine`** (the MCP server name in the client JSON). **`guide_barsom_workflow`** is the canonical bootstrap: it loads plan-scoped workflow text from the Barivia API when online (tool map, async rules, training modes, SOP). If the API is unreachable, it returns a short **connectivity-only** offline stub. Core tools are `datasets`, `train` (`map`, `siom_map`, `impute`, `floop_siom` where entitled), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/export/download/recolor), and `inference` (predict/batch_predict/impute_column/compare/project_columns/transition_flow/report). **Optional MCP App tools** (`training_monitor`, `results_explorer`) add embedded or localhost viz; they are **not required** to complete upload → train → poll → results, though `results_explorer` is the preferred way to browse figures.
4
4
 
5
- **Pre-training help (pick one):** **`prepare_training`** prompt = narrative checklist (tier-scoped from the API when online); **`training_guidance`** tool = structured presets and parameter hints (including the resolved per-column normalization). Review either, then submit with **`train`**.
5
+ **Pre-training help (pick one):** **`prepare_training`** prompt = narrative checklist (tier-scoped from the API when online); **`training_guidance`** tool = structured presets, quality/timing heuristics, and parameter hints (including resolved per-column normalization). Review either, then submit with **`train`**. Parameter encyclopedias and quality targets are **API-gated** (valid key + plan); the published proxy catalog keeps short routing text only.
6
6
 
7
7
  ## Installation
8
8
 
@@ -56,7 +56,8 @@ MCP clients typically run it with **`npx`** (downloads on first use):
56
56
  | `BARIVIA_WORKSPACE_EXTRA_ROOTS` | No | (empty) | Comma- or colon-separated absolute directories whose **realpath** targets are also allowed for uploads when the sandbox is on. Use for shared fixture trees reached via symlinks under the case workspace (e.g. `…/barivia-testing/data`). Does not weaken the default sandbox for paths outside these roots. |
57
57
  | `BARIVIA_FETCH_TIMEOUT_MS` | No | `60000` | Per-request HTTP timeout (ms) to the API. Increase (e.g. `120000`) on slow networks or when the API does long-running work. Large dataset uploads use a separate longer timeout internally, and `datasets(analyze)` runs asynchronously (auto-polled) so it is not bound by this. A timeout is not auto-retried (re-firing slow work would only multiply load / risk duplicates). |
58
58
  | `BARIVIA_VIZ_PORT` | No | OS-assigned | Local viz server on `127.0.0.1`. Port is **per MCP session** unless pinned — standalone `…/viz/…` URLs go stale after proxy restart. Set a fixed port for stable bookmark URLs. Diagnostic: `GET http://127.0.0.1:<port>/api/health?job_id=<id>`. |
59
- | `BARIVIA_UI_DELIVERY` | No | `auto` | Client-capability override for App tools: `auto` \| `apps` \| `localhost` \| `inline`. Tool responses always lead with a standalone localhost URL (hosts may list `ui://` without mounting widgets) and include a structured `ui_delivery` block. `account(status)` reports detected Apps support, viz port, and active tier. |
59
+ | `BARIVIA_UI_DELIVERY` | No | `auto` | Client-capability override for App tools: `auto` \| `apps` \| `localhost` \| `inline` \| `text_only`. Tool responses always lead with a standalone localhost URL (hosts may list `ui://` without mounting widgets) and include a structured `ui_delivery` block. `account(status)` reports detected Apps support, viz port, and active tier. |
60
+ | `BARIVIA_UI_PREFER_LOCALHOST` | No | (off) | Set `1` to force `localhost` under `auto` even when the host advertises MCP Apps (Cursor list-without-mount workaround). Default `auto` behavior unchanged. |
60
61
  | `BARIVIA_ENFORCE_WORKSPACE_SANDBOX` | No | `1` (enabled) | File uploads are constrained to the MCP workspace root (and optional `BARIVIA_WORKSPACE_EXTRA_ROOTS`) by default. Set to `0` or `false` to allow absolute paths anywhere on the machine (high trust). If uploads fail with "symlink resolves outside workspace", either point `BARIVIA_WORKSPACE_ROOT` / `BARIVIA_WORKSPACE_EXTRA_ROOTS` at the real data dir, copy the file into the workspace, or opt out with `BARIVIA_ENFORCE_WORKSPACE_SANDBOX=0`. |
61
62
 
62
63
  Legacy `BARSOM_API_KEY` / `BARSOM_API_URL` / `BARSOM_WORKSPACE_ROOT` are also accepted as fallbacks.
package/dist/audit.js CHANGED
@@ -161,14 +161,20 @@ export async function runMcpToolAudit(tool, action, args, handler) {
161
161
  /**
162
162
  * Register an MCP tool with structured audit logging on each invocation.
163
163
  */
164
- export function registerAuditedTool(server, name, description, schema, handler) {
165
- server.tool(name, description, schema, (async (args) => {
164
+ export function registerAuditedTool(server, name, description, schema, handler, annotations) {
165
+ const cb = (async (args) => {
166
166
  const rec = args;
167
167
  const action = typeof rec.action === "string" && rec.action.length > 0 ? rec.action : "default";
168
168
  // One trace per tool invocation: every apiCall inside this handler shares one
169
169
  // trace_id, so the API + job chain reconstruct the whole logical action.
170
170
  return runWithTrace(() => runMcpToolAudit(name, action, rec, () => handler(args)));
171
- }));
171
+ });
172
+ if (annotations) {
173
+ server.tool(name, description, schema, annotations, cb);
174
+ }
175
+ else {
176
+ server.tool(name, description, schema, cb);
177
+ }
172
178
  }
173
179
  /** Attach audit flags to a tool result (stripped before returning to MCP client). */
174
180
  export function withAuditFlags(result, flags) {
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as s,registerAppResource as r,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as c,loadViewHtml as p,setVizPort as u,setClientSupportsMcpApps as d,CLIENT_VERSION as m}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as h}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as b}from"./tools/jobs.js";import{registerResultsTool as _}from"./tools/results.js";import{registerExploreMapTool as y,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as k}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as x}from"./tools/training_guidance.js";import{registerFeedbackTool as P}from"./tools/feedback.js";import{registerTrainingMonitorTool as I,TRAINING_MONITOR_URI as A}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as C}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const M=new e({name:"analytics-engine",version:m,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `train(action=map)`, `train(action=siom_map)`, or `train(action=floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool), then `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map; impute + siom_map (SIOM entitlement); floop_siom (FLooP entitlement). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list (slim + limit/cursor/status/job_type/has_results), update (label/description/tags), compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (default=combined only; figures="none" for metrics-only sweeps; figures="all" or results_explorer/download for every plot), export, download, recolor (async). Read/render completed jobs. Results catalog = `jobs(list, has_results=true)` |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, history, **inventory** (markdown datasets+jobs overview), add_funds, **health** (API liveness via GET /health — no R2) |\n| Bootstrap | `guide_barsom_workflow`, `train`, `training_guidance`, `prepare_training` prompt | orientation, submit map/siom/impute/floop, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees; queues locally if API/storage is down |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions (incl. `transition_flow`) and `results(recolor)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Per-request HTTP timeout defaults to 60s (`datasets(analyze)` is async + auto-polled, so it is not bound by it); on slow networks raise `BARIVIA_FETCH_TIMEOUT_MS`. Timeouts are NOT auto-retried (avoids duplicating slow/expensive work).\n\n## Org inventory recipe\n\nTo rebuild a durable markdown overview of an org (datasets + jobs):\n1. `account(action=inventory)` — preferred one-shot (datasets name/id/rows/cols/description/tags + slim recent jobs).\n2. Or compose: `datasets(list)` + `jobs(action=list, limit=50)` (page with `cursor` / filter with `status` / `job_type` / `has_results=true` for a results catalog).\n3. Set `description` + `tags` on `datasets(upload|update)` and `train(...)` / `jobs(update)` so duplicates stay navigable.\n\n## API briefly unavailable (pause, do not crash)\nIf tools return `api_unreachable` / gateway HTML / slim `error_code` lines: summarize in **one calm sentence** — never paste Cloudflare HTML. The proxy already burst-retried (**3** tries, ~**2s** apart); honor `retry_after_sec` (~**30**) before another round. Use `account(action=health)` (no R2). Local downloads and prior results stay usable; `send_feedback` queues under `~/.barivia/deferred-feedback` and flushes when the API returns. Do not start large uploads/trains until `account(status)` or health looks healthy again.\n\n## Constraints\n\n- **Do not auto-subset:** Never call `datasets(subset)` or `sample_n` unless the user explicitly asks for a filter, slice, or random sample. Train and analyze on the uploaded `dataset_id` at full row count — GPU staging/prepare handles hundreds of thousands of rows quickly; silent downsampling hides real prepare/train behavior and wastes scale demos. Use small named fixtures (e.g. sample.csv, periodic_load) only when the user or runbook names them.\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.\n- **Large results / MCP timeouts:** For sweeps, `jobs(compare)`, or metrics-only reads use `results(action=get, figures="none")`. Default `get` inlines combined only. For every plot use `results_explorer` or `results(action=download)`, not `figures="all"` in chat unless the user wants all images inline.\n\n## UI delivery (heterogeneous MCP clients)\n\nHosts differ (Cursor, Claude Desktop, CLI agents): some embed MCP App panels; others list `ui://` but never mount them (expected on some Cursor builds); others are text-only. Tool results **always lead with a standalone localhost URL** — that is the intended fallback, not a broken App.\n\n| Tier | When | Agent should |\n|------|------|--------------|\n| **embedded** | Client advertises MCP Apps | Let the panel render **and** surface the standalone localhost URL from tool output / `ui_delivery.urls` (hosts often list Apps but do not mount). |\n| **localhost** | No MCP Apps; viz server running | Post the markdown link from tool output to the user (never broken workspace paths). Re-post after job completes if needed. |\n| **inline_image** | No Apps / override `inline` | Summarize metrics; inline raster (combined.png, learning curve) is attached automatically — show it in the reply. |\n| **text_only** | Viz server down | Use `jobs(status)` + `results(get)`; no panel or localhost link. |\n\nEvery `training_monitor` / `results_explorer` response includes a structured `ui_delivery` block (`delivery`, `urls`, `hint_for_agent`). Follow `hint_for_agent` without user prompting. Standalone viz pages cross-link monitor ↔ results for the same `job_id`.\n\n**Env overrides:** `BARIVIA_UI_DELIVERY=auto|apps|localhost|inline` (default `auto`). `BARIVIA_VIZ_PORT` pins the localhost viz port across proxy restarts. **Diagnostics:** `account(action=status)` reports detected MCP Apps support, viz port, and active tier.'});r(M,w,w,{mimeType:n},async()=>{const e=await p("results-explorer");return{contents:[{uri:w,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),r(M,A,A,{mimeType:n},async()=>{const e=await p("training-monitor");return{contents:[{uri:A,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),k(M),y(M),I(M),f(M),h(M),g(M,b),_(M),v(M),j(M),x(M),P(M),M.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map; siom_map/impute with SIOM entitlement; floop_siom when entitled; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not auto-subset — never call `datasets(subset)` / `sample_n` unless the user explicitly asks; train on the full uploaded `dataset_id`. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),M.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>{const t=await C(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const T=new t;(async function(){try{const e=await a(l,c,p);u(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=M.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=s(t);d(!!o?.mimeTypes?.includes(n))},await M.connect(T)})().catch(console.error);
2
+ import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as t}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as o}from"zod";import{getUiCapability as r,registerAppResource as s,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as c,loadViewHtml as p,setVizPort as m,setClientSupportsMcpApps as u,CLIENT_VERSION as d}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as _}from"./tools/train.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as h}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as w,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as v}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as k}from"./tools/training_guidance.js";import{registerFeedbackTool as I}from"./tools/feedback.js";import{registerTrainingMonitorTool as A,TRAINING_MONITOR_URI as P}from"./tools/training_monitor.js";import{resolvePrepareTrainingPrompt as S}from"./prepare_training_prompt.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const T=new e({name:"analytics-engine",version:d,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics via the Barivia API.\n\n## Bootstrap (do this first)\n\nCall `guide_barsom_workflow` for the plan-scoped tool map, training modes, async rules, and SOP (API when online). Call `training_guidance` / `prepare_training` for parameter and quality hints before guessing knobs.\n\n## Workflow (short)\n\n`datasets(upload)` → preview/analyze → `train(action=...)` (only modes your plan allows) → poll `jobs(status)` every 10–15s → `results(get)` then optional `results_explorer`. Scoring and frozen-map ops live in `inference`.\n\n## Tool taxonomy (one tool per stage)\n\n| Stage | Tool(s) |\n|-------|---------|\n| Data | `datasets` |\n| Train submit | `train` (map / siom_map / impute / floop_siom where entitled) |\n| Lifecycle | `jobs` (status/list/compare/cancel/delete — no train/score) |\n| Artifacts | `results` (get/export/download/recolor) |\n| Frozen-map ops | `inference` (predict/batch_predict/impute_column/compare/project_columns/transition_flow/report) |\n| Account | `account` (status/inventory/health) |\n| Bootstrap | `guide_barsom_workflow`, `training_guidance`, `prepare_training` |\n| Explore (UI) | `results_explorer`, `training_monitor` |\n| Feedback | `send_feedback` (after user agrees) |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics across **training runs**. `inference(compare)` = density-diff vs a **second dataset**.\n- Post-training analysis is `inference`, not `results` (presentation/artifacts only).\n- Project columns → `inference(project_columns)`; formula → `datasets(add_expression, project_onto_job=...)`.\n\n## Async & constraints\n\n- Training returns `job_id` immediately — poll `jobs(status)` every 10–15s; running is not failed.\n- **Do not auto-subset** unless the user asks. Prefer `results(get, figures="none")` for sweeps.\n- Column names are case-sensitive. After recolor/transition_flow/project_columns, use the **new** job_id when returned.\n- On `api_unreachable`: one calm sentence, honor `retry_after_sec`; use `account(health)`. Timeouts are not auto-retried.\n\n## UI delivery\n\nTool results lead with a localhost viz URL and a structured `ui_delivery` block — follow `hint_for_agent`. Env: `BARIVIA_UI_DELIVERY` (auto|apps|localhost|inline|text_only), `BARIVIA_UI_PREFER_LOCALHOST=1` (opt-in localhost even when Apps advertised), `BARIVIA_VIZ_PORT`. Details: `account(status)`.'});s(T,y,y,{mimeType:n},async()=>{const e=await p("results-explorer");return{contents:[{uri:y,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(T,P,P,{mimeType:n},async()=>{const e=await p("training-monitor");return{contents:[{uri:P,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),x(T),w(T),A(T),f(T),_(T),g(T,h),b(T),v(T),j(T),k(T),I(T),T.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map; siom_map/impute with SIOM entitlement; floop_siom when entitled; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not auto-subset — never call `datasets(subset)` / `sample_n` unless the user explicitly asks; train on the full uploaded `dataset_id`. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),T.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>{const t=await S(e);return{messages:[{role:"user",content:{type:"text",text:(t.used_fallback?`[offline fallback ${t.fallback_version}]\n\n`:"")+t.text}}]}});const O=new t;(async function(){try{const e=await a(l,c,p);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=T.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);u(!!o?.mimeTypes?.includes(n))},await T.connect(O)})().catch(console.error);
@@ -10,6 +10,9 @@ export const DEFAULT_BLOCK_UNTIL_SEC = 900;
10
10
  export const DEFAULT_POLL_INTERVAL_SEC = 5;
11
11
  export const MIN_POLL_INTERVAL_SEC = 5;
12
12
  export const HEARTBEAT_POLLS = 1;
13
+ /** After this many unchanged polls, gently raise interval (never below MIN_POLL). */
14
+ export const IDLE_BACKOFF_AFTER_POLLS = 8;
15
+ export const MAX_POLL_INTERVAL_SEC = 15;
13
16
  function sleep(ms) {
14
17
  return new Promise((r) => setTimeout(r, ms));
15
18
  }
@@ -218,12 +221,22 @@ export async function monitorJob(job_id, options = {}) {
218
221
  let lastSnap = null;
219
222
  let data = {};
220
223
  let heartbeat = 0;
224
+ let idleStreak = 0;
221
225
  while (Date.now() - start < blockMs) {
222
226
  data = (await apiCall("GET", `/v1/jobs/${job_id}`));
223
227
  const elapsedSec = Math.round((Date.now() - start) / 1000);
224
228
  const snap = snapshotFromJob(data, elapsedSec);
225
229
  heartbeat += 1;
226
230
  const heartbeatDue = heartbeat >= HEARTBEAT_POLLS;
231
+ const unchanged = lastSnap != null &&
232
+ lastSnap.status === snap.status &&
233
+ lastSnap.progress_pct === snap.progress_pct &&
234
+ lastSnap.phase === snap.phase &&
235
+ lastSnap.epoch === snap.epoch;
236
+ if (unchanged)
237
+ idleStreak += 1;
238
+ else
239
+ idleStreak = 0;
227
240
  if (shouldRecordSnapshot(lastSnap, snap) || heartbeatDue) {
228
241
  snapshots.push(snap);
229
242
  lastSnap = snap;
@@ -233,7 +246,13 @@ export async function monitorJob(job_id, options = {}) {
233
246
  if (status === "completed" || status === "failed" || status === "cancelled") {
234
247
  break;
235
248
  }
236
- await sleep(pollMs);
249
+ // Gentle backoff on idle polls — raise interval, never below the 5s floor.
250
+ let waitMs = pollMs;
251
+ if (idleStreak >= IDLE_BACKOFF_AFTER_POLLS) {
252
+ const steps = Math.floor((idleStreak - IDLE_BACKOFF_AFTER_POLLS) / 2);
253
+ waitMs = Math.min(pollMs + steps * 5000, MAX_POLL_INTERVAL_SEC * 1000);
254
+ }
255
+ await sleep(Math.max(MIN_POLL_INTERVAL_SEC * 1000, waitMs));
237
256
  }
238
257
  const status = String(data.status ?? "");
239
258
  const terminal = status === "completed" || status === "failed" || status === "cancelled";
@@ -1,15 +1,9 @@
1
1
  import { apiCall } from "./shared.js";
2
- export const PREPARE_TRAINING_FALLBACK_VERSION = "2026.07f-offline";
2
+ export const PREPARE_TRAINING_FALLBACK_VERSION = "2026.07h-offline";
3
3
  const FALLBACK_LINES = [
4
- "Before upload: for train(action=map) ensure training columns have no NaNs; for train(action=impute) missing cells in training columns are OK (plain numeric only).",
5
- "Do not auto-subset: never call datasets(subset) or sample_n unless the user explicitly asks — on CPU, preset=quick 15×15 is typically very fast up to ~100k rows; only suggest sample_n when n>100k or the user asks. Train on the full uploaded dataset_id.",
6
- "Small-n: if preview/analyze shows n<1000 (especially n<100), omit grid_x/grid_y for auto (~sqrt(5·√n)); avoid preset=quick/standard on tiny tables (dead nodes / TE≈1). Drop redundant correlated geo features before train.",
7
- 'Please run datasets(action=preview, dataset_id="{id}") to inspect columns, then datasets(action=analyze, dataset_id="{id}") to see which columns and temporal periods are most informative.',
8
- "Choose the training path:",
9
- '- train(action=map, dataset_id="{id}", ...) for complete-case data',
10
- '- train(action=impute, dataset_id="{id}", ...) for sparse matrices',
11
- '- train(action=siom_map, dataset_id="{id}", ...) for a fixed-grid SIOM',
12
- '- train(action=floop_siom, dataset_id="{id}", ...) for FLooP-SIOM (default topology=free / CHL; optional topology=chain)',
4
+ "API unreachable cannot load the tier-scoped prepare_training checklist.",
5
+ "Retry when BARIVIA_API_KEY / BARIVIA_API_URL reconnect. Meanwhile: datasets(preview/analyze) on dataset_id={id} is still useful locally if those tools work; do not auto-subset unless the user asks.",
6
+ "When the API returns, call prepare_training again (or guide_barsom_workflow / training_guidance) for plan-scoped train actions and parameter hints.",
13
7
  ];
14
8
  /** Used by the `prepare_training` MCP prompt; prefers tier-scoped text from the API when online. */
15
9
  export async function resolvePrepareTrainingPrompt(datasetId) {
package/dist/shared.js CHANGED
@@ -27,10 +27,37 @@ export const RETRY_BASE_MS = parseInt(process.env.BARIVIA_RETRY_BASE_MS ?? "2000
27
27
  export const RETRYABLE_STATUS = new Set([502, 503, 504]);
28
28
  /** Cool-down hint after the burst is exhausted — agents should wait before another round. */
29
29
  export const UNAVAILABLE_RETRY_AFTER_SEC = 30;
30
- /** Fixed delay between burst retries (not exponential). */
31
- export function retryDelayMs(_attempt) {
30
+ /**
31
+ * Delay between burst retries. Keeps the flat RETRY_BASE_MS default (short gateway
32
+ * blips); when the server sends Retry-After / retry_after_sec, sleep at least that long.
33
+ */
34
+ export function retryDelayMs(_attempt, retryAfterMs) {
35
+ if (retryAfterMs != null && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
36
+ return Math.max(RETRY_BASE_MS, Math.floor(retryAfterMs));
37
+ }
32
38
  return RETRY_BASE_MS;
33
39
  }
40
+ /** Parse Retry-After header (delta-seconds) and/or JSON body retry_after_sec → ms. */
41
+ export function parseRetryAfterMs(resp, bodyText) {
42
+ const header = resp.headers.get("Retry-After");
43
+ if (header) {
44
+ const sec = parseInt(header.trim(), 10);
45
+ if (!Number.isNaN(sec) && sec >= 0)
46
+ return sec * 1000;
47
+ }
48
+ if (bodyText) {
49
+ try {
50
+ const j = JSON.parse(bodyText);
51
+ const sec = Number(j.retry_after_sec);
52
+ if (Number.isFinite(sec) && sec >= 0)
53
+ return sec * 1000;
54
+ }
55
+ catch {
56
+ /* not JSON */
57
+ }
58
+ }
59
+ return undefined;
60
+ }
34
61
  function newRequestId() {
35
62
  return randomUUID();
36
63
  }
@@ -39,7 +66,7 @@ function newRequestId() {
39
66
  * X-Barsom-Client-Version so the server can annotate tool guidance with the
40
67
  * wrapper version each action requires. Keep in sync with package.json on bump.
41
68
  */
42
- export const CLIENT_VERSION = "0.23.9";
69
+ export const CLIENT_VERSION = "0.23.11";
43
70
  /** User-facing links; keep aligned with barivia.se / api.barivia.se. */
44
71
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
45
72
  /** Self-serve account dashboard (manage plan, billing, and API keys). */
@@ -244,7 +271,11 @@ export function isTransientError(err, status) {
244
271
  return true; // network-level fetch failure
245
272
  return false;
246
273
  }
247
- /** Streaming SHA-256 of a file (for idempotency keys) without reading it into memory. */
274
+ /**
275
+ * Streaming SHA-256 of a file without reading it into memory.
276
+ * Used by tools/datasets.ts upload as Idempotency-Key = sha256(name+body) — do not
277
+ * invent a second key path here.
278
+ */
248
279
  export async function streamFileSha256(srcPath) {
249
280
  return new Promise((resolve, reject) => {
250
281
  const h = createHash("sha256");
@@ -814,7 +845,7 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
814
845
  const text = await resp.text();
815
846
  if (!resp.ok) {
816
847
  if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
817
- const delayMs = retryDelayMs(attempt);
848
+ const delayMs = retryDelayMs(attempt, parseRetryAfterMs(resp, text));
818
849
  logWarn("API retry", {
819
850
  rid: requestId,
820
851
  method,
@@ -845,7 +876,10 @@ export async function apiCall(method, path, body, extraHeaders, requestTimeoutMs
845
876
  catch (err) {
846
877
  lastError = err;
847
878
  if (attempt < MAX_RETRIES && isTransientError(err)) {
848
- const delayMs = retryDelayMs(attempt);
879
+ const errRetryMs = typeof err.retryAfterSec === "number"
880
+ ? err.retryAfterSec * 1000
881
+ : undefined;
882
+ const delayMs = retryDelayMs(attempt, errRetryMs);
849
883
  logWarn("API retry", {
850
884
  rid: requestId,
851
885
  method,
@@ -889,8 +923,9 @@ export async function apiRawCall(path, requestTimeoutMs) {
889
923
  },
890
924
  }, effectiveTimeout);
891
925
  if (!resp.ok) {
926
+ const text = await resp.text();
892
927
  if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
893
- const delayMs = retryDelayMs(attempt);
928
+ const delayMs = retryDelayMs(attempt, parseRetryAfterMs(resp, text));
894
929
  logWarn("API retry", {
895
930
  rid: requestId,
896
931
  path,
@@ -902,7 +937,6 @@ export async function apiRawCall(path, requestTimeoutMs) {
902
937
  await new Promise((r) => setTimeout(r, delayMs));
903
938
  continue;
904
939
  }
905
- const text = await resp.text();
906
940
  throwApiError(resp.status, text, requestId);
907
941
  }
908
942
  const arrayBuf = await resp.arrayBuffer();
@@ -914,7 +948,10 @@ export async function apiRawCall(path, requestTimeoutMs) {
914
948
  catch (err) {
915
949
  lastError = err;
916
950
  if (attempt < MAX_RETRIES && isTransientError(err)) {
917
- const delayMs = retryDelayMs(attempt);
951
+ const errRetryMs = typeof err.retryAfterSec === "number"
952
+ ? err.retryAfterSec * 1000
953
+ : undefined;
954
+ const delayMs = retryDelayMs(attempt, errRetryMs);
918
955
  logWarn("API retry", {
919
956
  rid: requestId,
920
957
  path,
@@ -46,7 +46,7 @@ export function registerAccountTool(server) {
46
46
  action=status: Returns plan tier, compute class (CPU/GPU), usage limits, live queue state, training time estimates, and credit balance.
47
47
  Use BEFORE large jobs to check GPU availability and estimate wait time.
48
48
  When the API/gateway is down, returns structured \`{ok:false, retry_after_sec, …}\` — treat as a pause, not a crash.
49
- action=inventory: Lists datasets (name/id/rows/cols/description/tags) and slim recent jobs (label/id/status/job_type/dataset_id/created_at/result). Prefer this over hand-assembling datasets(list)+jobs(list). Page jobs with jobs(action=list, cursor=…).
49
+ action=inventory: One-shot markdown overview — all datasets (slim) plus the latest N jobs (limit default 50). Not paginated for datasets; for more jobs use jobs(action=list, cursor=next_cursor, limit=…). Prefer this over hand-assembling datasets(list)+jobs(list).
50
50
  action=health: Lightweight liveness/readiness (no R2). Use when tools return api_unreachable or before retrying deferred send_feedback.
51
51
  Capacity is shared LOCAL/GKE worker pools — there is no per-key cloud burst lease.
52
52
  NOT FOR: Training itself — use train(action=map). This tool only manages the account.`, {
@@ -164,6 +164,7 @@ async function handleResultsExplorer(job_id) {
164
164
  jobId: job_id,
165
165
  jobStatus: "completed",
166
166
  inlineImageAttached,
167
+ uiResourceUri: RESULTS_EXPLORER_URI,
167
168
  });
168
169
  appendUiDeliveryContent(content, uiDelivery, {
169
170
  linkLabel: "Open results explorer",
@@ -23,7 +23,7 @@ export function registerFeedbackTool(server) {
23
23
 
24
24
  For a substantial issue, prefer feedback_items: submit several focused instances (each max 1400 chars) covering, e.g., symptoms, exact reproduction steps, environment, and concrete asks — they are stored together as one batch so developers see the full picture. Use the single feedback field for a short one-off note.
25
25
 
26
- If the API or object storage is unavailable, this tool queues feedback locally (~/.barivia/deferred-feedback) and returns deferred=true with request_id / api_health — call again later to flush. Probe API liveness with account(action=health) (GET /health; does not touch R2).`, {
26
+ If the API or object storage is unavailable, this tool queues feedback locally (~/.barivia/deferred-feedback) and returns deferred=true with request_id / api_health — call again later to flush (idempotent enqueue of the same draft). Probe API liveness with account(action=health) (GET /health; does not touch R2).`, {
27
27
  feedback: z.string().max(1400).optional().describe("Single feedback note (max 1400 characters). Use feedback_items instead for a multi-part report."),
28
28
  feedback_items: z.array(z.string().max(1400)).max(10).optional().describe("Several feedback instances (each max 1400 characters, up to 10), stored together as one batch. Prefer this for a detailed issue: split it into focused parts (symptoms, reproduction, environment, asks)."),
29
29
  }, async ({ feedback, feedback_items }) => {
@@ -104,5 +104,10 @@ If the API or object storage is unavailable, this tool queues feedback locally (
104
104
  }
105
105
  throw err;
106
106
  }
107
+ }, {
108
+ readOnlyHint: false,
109
+ destructiveHint: false,
110
+ idempotentHint: true,
111
+ openWorldHint: true,
107
112
  });
108
113
  }
@@ -53,42 +53,20 @@ export async function runBatchPredict(args) {
53
53
  };
54
54
  }
55
55
  export function registerInferenceTool(server) {
56
- registerAuditedTool(server, "inference", `Use a trained map as a persistent inference artifact — score data, annotate the source CSV, compare datasets, project columns, or generate a report manifest.
56
+ registerAuditedTool(server, "inference", `Use a trained map as a persistent inference artifact — score data, annotate CSVs, compare cohorts, project columns, or fetch a report manifest. Domain details (impute pooling, regimes, FLooP limits): **guide_barsom_workflow** / **training_guidance**.
57
57
 
58
- | Action | Use when | Timing |
59
- |--------|----------|--------|
60
- | predict | Scoring rows against the trained map (new data OR the training set itself) | 5–120s |
61
- | batch_predict | Fan out many predict jobs at once against one trained map (several datasets/row-sets in one call) | submits N jobs |
62
- | impute_column | Fill a numeric column (not used in training) by pooling observed values on the BMU plus topology neighbors (typically 6 on hex; periodic maps wrap) | 5–120s |
63
- | compare | Comparing hit distributions of a second dataset against training (drift, A/B); needs one trained job_id + dataset B. NOT jobs(compare), which tabulates metrics across multiple training runs. | 30–120s |
64
- | project_columns | Project one or more dataset columns onto the map (component planes); dataset can be training set or partial-feature set | 10–90s |
65
- | transition_flow | Temporal state-transition arrows on the trained map. Only meaningful when the scored rows are in chronological order. | 30–60s |
66
- | report | Get a report manifest (artifact keys + URLs) to build your own report in Quarto/Notebook/script | Immediate (sync) |
58
+ | Action | Use when |
59
+ |--------|----------|
60
+ | predict | Score rows (new data or training set); output compact|annotated |
61
+ | batch_predict | Fan out many predict jobs against one map |
62
+ | impute_column | Fill a non-training numeric column on a frozen map (not sparse train+impute) |
63
+ | compare | Density-diff of dataset B vs training on ONE map NOT jobs(compare) |
64
+ | project_columns | Project dataset columns onto the map (component planes) |
65
+ | transition_flow | Temporal arrows; time-ordered rows only |
66
+ | report | Sync report manifest for custom notebooks |
67
67
 
68
- Sync/async: predict, impute_column, compare, and transition_flow are async jobs that the proxy auto-polls and usually returns on completion; if one returns a job_id instead (e.g. timeout), poll jobs(action=status, job_id=...) then results(action=download, job_id=...). batch_predict does NOT auto-poll it returns a list of submitted job_ids; poll each with jobs(action=status).
69
- Artifacts: When complete, use results(action=download, job_id=<returned_job_id>) to get: predict (output="compact")predictions.csv; predict (output="annotated") → annotated.csv; impute_column imputed.csv; compare → density-diff figure (e.g. density_diff.png).
70
- report is the only synchronous inference action — returns manifest immediately; no job to poll.
71
- NOT FOR: Retraining or changing the map — all actions treat the trained map as frozen.
72
- ESCALATION: If any action returns "missing column", verify column names with datasets(action=preview). Column names are case-sensitive and must match the training feature set exactly.
73
-
74
- action=predict: Score rows against the trained map.
75
- Inputs (one of):
76
- - dataset_id (default = parent training job's dataset). Use a different dataset for new/unseen data; omit it (or pass the training dataset_id) to score the training set itself.
77
- - rows (≤500 inline). Always treated as new data.
78
- Output style (output param, default "compact"):
79
- - "compact" → predictions.csv (row_id, bmu_x, bmu_y, bmu_node_index, cluster_id [, quantization_error, potential_anomaly]).
80
- - "annotated" → annotated.csv (full source CSV with bmu_x, bmu_y, bmu_node_index, cluster_id appended). Requires a dataset (no inline rows).
81
- Regime (auto): if the resolved dataset is the parent training dataset, regime="training" and QE / qe_p95 / potential_anomaly are omitted (QE on training data is fitting error, not generalisation — use a held-out dataset for quality). Otherwise regime="new" and full QE columns are returned.
82
- Schema rules: prefer dataset_id for scoring. Inline rows may use raw cyclic keys (e.g. hour) — the API/worker expands them from training config to hour_cos/hour_sin; pre-expanded cos/sin columns also work. For a single inline row, the proxy uses the stateless model endpoint (same cyclic expansion). Raw categorical strings are allowed for baseline categorical_features models on that single-row path.
83
- Routing: prefer dataset_id for many rows or whenever the map uses irregular SIOM / GeneralTopology layouts — the async worker path is the supported batch scorer. Single-row rows take a fast stateless path that may return invalid_inference_input on some topologies; if so, retry with dataset_id (a one-row dataset is fine). FLooP-SIOM: use dataset_id predict first.
84
- When the scored set has at most ${PREDICT_PREVIEW_ROW_CAP} rows, completed responses include a short per-line preview in the tool text for chat agents.
85
-
86
- action=impute_column: Map-local imputation on the frozen trained map (read-only; not a held-out validity claim). Requires dataset_id + target_column; the dataset must contain all training features (same names + cyclic expansion as predict) plus the target column. target_column must NOT have been a training feature. Pools finite target values from rows on this row's BMU and its topology neighbors (BMU + neighbors; ~7 on hex interior, fewer on non-periodic borders), neighbourhood-distance-weighted by default (weighting="uniform" for a flat pool); excludes the current row. only_missing (default true) keeps observed values; impute_aggregation mean|median. Optional cv_folds (2-20) → quality.csv (held-out MAE/RMSE/R2). target_column_kind: categorical (mode) / cumulative (warns). Output imputed.csv columns: row_id, target_original, target_imputed, impute_source (observed|imputed|insufficient_data), bmu_node_index, n_patch_nodes, n_pool_rows, pool_std, pool_p5, pool_p95.
87
-
88
- action=compare: density-diff of a cohort dataset against the training set on ONE trained map — NOT jobs(action=compare) (which tabulates QE/TE/EV metrics across multiple training runs to pick the best). dataset_id must refer to a dataset with the same feature set as training (same column names and preprocessing, including cyclic expansion). A = training dataset; B = cohort to compare. Density-diff: positive = B gained vs A; negative = A had more. Returns density-diff heatmap (e.g. density_diff.png).
89
- action=project_columns: Project one or more columns from a dataset onto the trained map. Pass dataset_id (the dataset containing the columns) and columns (array of column names). Uses cached BMUs when dataset is the training set; supports partial-feature mapping when dataset has only a subset of training features. Returns one component plane image per column. Get files via results(action=download, job_id=<returned_job_id>).
90
- action=transition_flow: Temporal state-transition arrows drawn on the trained map grid (U-matrix background). Post-training analysis of how observations move across the map over time — use ONLY when rows are in chronological order; on unordered data the arrows are meaningless. You pass the training job_id; the API returns a NEW result job_id (use it for results(action=get/download)). lag=1 (default) = consecutive rows; lag=N = N-step horizon (e.g. 24 for daily cycles in hourly data). min_transitions filters noisy arrows (increase for large datasets). Auto-polled.
91
- action=report: Returns a report manifest for the given job_id (job must be completed). Includes figure_manifest (logical names → filenames), download_urls for all artifacts, cluster_summary when available, and summary metrics. Stakeholder report PDF (if generated) is available via results(action=download, job_id=<training_job_id>), filename e.g. report.pdf.`, {
68
+ ASYNC: predict / impute_column / compare / transition_flow auto-poll; if you get a job_id, poll jobs(status) then results(download). batch_predict returns job_ids (poll each). Prefer dataset_id for batch / irregular topologies; single-row rows may need retry with dataset_id.
69
+ NOT FOR: Retraining. ESCALATION: missing columndatasets(preview); column names are case-sensitive.`, {
92
70
  action: z
93
71
  .enum(["predict", "batch_predict", "impute_column", "compare", "project_columns", "transition_flow", "report"])
94
72
  .describe("predict: score rows; batch_predict: fan out many predict jobs at once; impute_column: topology-neighbor pool imputation for a column not in training; compare: drift/cohort diff heatmap; project_columns: project dataset columns onto map; transition_flow: temporal state-transition arrows on the map (time-ordered rows only); report: manifest of primitives for custom report."),
@@ -24,13 +24,13 @@ ASYNC POLLING PROTOCOL (action=monitor):
24
24
  - Optional UI: training_monitor(job_id) for embedded charts; jobs(action=monitor) is the headless equivalent.
25
25
 
26
26
  ASYNC POLLING PROTOCOL (action=status):
27
- - One-shot check only. If not using monitor, poll every 10-15 seconds manually.
27
+ - One-shot check only. If not using monitor, poll every 10-15 seconds manually — do not give up after one or two polls; large jobs need patience.
28
28
  - Progress increases through phases (ordering then convergence for map training); other job types differ.
29
29
  - For large grids (40×40+), do not assume failure before 3 minutes on CPU.
30
30
  - Map training typical times: 10×10 ~30s | 20×20 ~3–5 min | 40×40 ~15–30 min.
31
31
  - completed → results(action=get); failed → read error + optional failure_stage (preprocessing/training/metrics/visualization/upload): memory error → smaller grid/batch and retrain; column missing → datasets(action=preview); NaN on a standard map → train(action=impute) for sparse columns or clean the CSV.
32
32
 
33
- action=list: Slim rows by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). Use status/job_type/has_results/limit/cursor to page. has_results=true is the results catalog. Pass fields="full" only when you need params/error.
33
+ action=list: Slim rows by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). Use status/job_type/has_results/limit/cursor to page; when the response includes next_cursor, call again with cursor=that value. has_results=true is the results catalog. Pass fields="full" only when you need params/error.
34
34
  action=update: PATCH label/description/tags (pass at train submit when possible).
35
35
  action=compare: metrics table (QE, TE, explained variance, silhouette) for 2+ COMPLETED TRAINING jobs — pick the best run in a sweep. NOT inference(action=compare) (single trained job + second dataset → density-diff heatmap). Only hyperparameter columns that differ across runs are shown (including SIOM knobs gamma/gamma_f/siom_decay/siom_penalty/reset_per_epoch when they vary); pass label="…" at train time for readable rows.
36
36
  action=cancel: Not instant — worker checks between phases. Expect up to 30s delay.
@@ -19,12 +19,12 @@ Temporal state-transition arrows moved to **inference(action=transition_flow)**
19
19
  ONLY call this after jobs(action=status) returns "completed".
20
20
  ESCALATION: If job not found, verify job_id. If "job not complete", poll with jobs(action=status).
21
21
 
22
- action=get: Returns text summary with quality metrics and inline images.
22
+ Job states (for async parents): pending running completed | failed | cancelled (finalize_training may run after compute completes — wait until status=completed before get).
23
+ action=get: Returns text summary with quality metrics and inline images; structuredContent includes a slim verification block (status, file_count, has_summary).
23
24
  - figures: omit = combined only. Prefer string "all" / "none" (not an array). "all" = all published plots (excludes unpublished cyclic cos/sin expansions unless train used viz_upload_cyclic_components=true). "none" = metrics text only, no images (recommended for sweeps and LLM clients to keep tool payloads small). Array = specific logical names (combined, umatrix, hit_histogram, learning_curve, correlation, divergence_kl, component_1..N) or download filenames. Hosts that stringify arrays are coerced. Request unpublished cyclic planes via results(recolor, figures=[component_N, ...]).
24
25
  - include_individual: if true (and figures omitted), inlines every component plane.
25
26
  - After recolor or project, use the job_id returned by that operation to get the new artifacts, not the original training job_id.
26
- - After showing results, guide the user: QE interpretation, whether to retrain, which features to explore.
27
- - METRIC INTERPRETATION: QE lower=better (<1.5 good) | TE<0.1 good | Explained variance>0.7 good | Silhouette higher=better | Davies-Bouldin lower=better.
27
+ - After showing results, guide the user using **training_guidance** metric_interpretation / quality_targets (whether to retrain, which features to explore).
28
28
 
29
29
  action=export: Structured data exports (not the same as download). Use export_type= to choose what to export.
30
30
  - export_type=training_log: learning curve sparklines + plot. Diagnose convergence/plateau/divergence. Prefer this over download for the training log.
@@ -479,6 +479,13 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
479
479
  summary,
480
480
  files,
481
481
  figures_inlined: [...inlinedImages],
482
+ verification: {
483
+ status: "completed",
484
+ has_summary: Object.keys(summary).length > 0,
485
+ file_count: files.length,
486
+ figures_inlined_count: inlinedImages.size,
487
+ job_type: jobType2,
488
+ },
482
489
  };
483
490
  const summaryText = content.filter((c) => c.type === "text").map((c) => c.text).join("\n\n");
484
491
  return structuredTextResult(structuredPayload, summaryText, content);
@@ -9,21 +9,15 @@ export const TRAIN_DESCRIPTION = `Submit a self-organizing map training job. Ret
9
9
  | Action | Use when |
10
10
  |--------|----------|
11
11
  | map | Standard SOM on a fixed hex grid — complete-case numeric data (no NaNs in training columns). |
12
- | siom_map | Self-interacting map — same grid flow plus SIOM coverage control (gamma, siom_decay, penalty) to avoid dead nodes. |
13
- | impute | Sparse training data (requires SIOM entitlement): trains a missing-tolerant map (SIOM missSOM when model=auto/SIOM; plain missSOM when model=SOM) AND returns dense imputed.csv in one job. Plain numeric columns only. Optional SIOM knobs: gamma, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch. |
14
- | floop_siom | FLooP-SIOM — a growing node-budget manifold instead of a fixed grid; default topology=free (CHL). Requires a plan with all_algorithms (Premium/Enterprise). |
12
+ | siom_map | Fixed-grid SIOM (coverage control) requires SIOM entitlement. |
13
+ | impute | Sparse training matrix (map + dense imputed.csv) requires SIOM entitlement. Prefer inference(impute_column) when you already have a complete-case map and need one held-out column. |
14
+ | floop_siom | Growing node-budget manifold (not a fixed grid) requires FLooP entitlement. |
15
15
 
16
- First quick map (no separate baseline action): use \`train(action=map, preset=quick)\` (or \`normalize=mad\` with a grid sized from the preview row count); see the \`baseline_explore\` preset in **training_guidance**.
16
+ Call **guide_barsom_workflow**, **training_guidance**, and/or the **prepare_training** prompt for plan-scoped parameter details, timing heuristics, quality.csv semantics, SIOM/FLooP knobs, and impute constraints do not guess. Backend: "cpu" | "gpu" | "gpu_graphs" (on CPU-only hosts pass backend=cpu).
17
17
 
18
- Parameter details (grid, epochs, batch, model, presets, normalize, categorical_features, backend, FLooP max_nodes/effort, SIOM geometry) live in the **training_guidance** tool / **prepare_training** prompt call them instead of guessing. Backend strings are "cpu" | "gpu" | "gpu_graphs" (no colon); on CPU-only hosts pass backend=cpu.
19
-
20
- ASYNC: every action returns a job_id immediately. Poll jobs(action=status, job_id=...) every 10–15s (do NOT poll faster). Map training typical times: 10×10 ~30s | 20×20 ~3–5 min | 40×40 ~15–30 min do not assume failure before 3 minutes on CPU. When status is completed, call results(action=get, job_id=...).
21
-
22
- action=impute: use when many cells are missing across training columns; use inference(impute_column) instead when you already have a complete-case map and need to fill one held-out column. Supports transforms (log/sqrt/etc., not rank), temporal_features, cyclic_features, optional cyclic_pairs=[[cos,sin],...] (circular prototype refresh + atomic chord BMU; those columns get circular quality metrics only — never quote Euclidean R² on cos/sin), and normalization_methods (zscore/mad/sigmoidal/none — not sepd). Does not support auto_log_transforms, rank transforms, or normalize=sepd. Requires SIOM entitlement (train_impute). Defaults model=auto (SIOM missSOM; model=SOM for plain missSOM), cv_folds=5 → quality.csv (linear: kfold_holdout_pool + resubstitution_optimistic; cyclic pairs: kfold_holdout_circular). Aggregate MAE/RMSE excludes cyclic columns. Do not treat k-fold alone as true missing-cell ground truth. Optional SIOM knobs: gamma, gamma_f, siom_decay, siom_penalty, penalty_alpha, reset_per_epoch. Artifacts: imputed.csv, imputation_mask.csv, optional imputation_uncertainty.csv (pool_std = neighbourhood dispersion). A completed impute job_id is a valid parent for inference(impute_column).
23
- action=floop_siom: max_nodes is a TOTAL node budget (not a grid width/area); if coverage collapses, reduce max_nodes before increasing effort.
24
- NOT FOR: lifecycle (status/list/compare/cancel/delete) → use jobs; scoring data → use inference.
25
-
26
- Rank transform (map/siom/floop only): training-only — batch predict, project_columns, and impute_column cannot score rank-transformed columns.`;
18
+ ASYNC: every action returns a job_id immediately. Prefer jobs(action=monitor) to block with snapshots, or poll jobs(action=status) every 10–15s (not faster) do not abandon after one poll. When completed, call results(action=get). Timing and quality heuristics: **training_guidance**.
19
+ NOT FOR: lifecycle → jobs; scoring → inference.
20
+ Rank transform (map/siom/floop only) is train-onlysee training_guidance.`;
27
21
  function assertImputeTrainingArgs(args) {
28
22
  assertValidNormalizationArgs(args);
29
23
  const normalize = args.normalize;
@@ -46,8 +46,8 @@ export async function fetchTrainingConfig() {
46
46
  export const TRAINING_INPUT_SCHEMA = {
47
47
  preset: z.enum(["quick", "standard", "refined", "high_res"]).optional(),
48
48
  grid_x: z.number().int().optional()
49
- .describe("Grid width. Omit grid_x AND grid_y (and preset) to auto-size via sqrt(5·√n) per side (clamped 5–50; ~6×6 at n≈45). Pass grid_x/grid_y not a bare `grid` field. Results report hit_stats.active_node_fraction and grid_suggestion when many nodes are dead."),
50
- grid_y: z.number().int().optional().describe("Grid height. See grid_x for auto-sizing."),
49
+ .describe("Grid width. Omit grid_x and grid_y (and preset) for API auto-size — see training_guidance."),
50
+ grid_y: z.number().int().optional().describe("Grid height. See grid_x / training_guidance."),
51
51
  epochs: z.preprocess((v) => {
52
52
  if (v === undefined || v === null)
53
53
  return v;
@@ -69,7 +69,7 @@ export const TRAINING_INPUT_SCHEMA = {
69
69
  period: z.number(),
70
70
  })).optional(),
71
71
  cyclic_pairs: z.array(z.array(z.string()).min(2).max(2)).optional()
72
- .describe("impute only: [[cos_col, sin_col], ...] for circular prototype means + atomic chord BMU. Those columns get circular quality metrics only (no Euclidean R²)."),
72
+ .describe("impute only: [[cos_col, sin_col], ...] see training_guidance / prepare_training for circular metrics."),
73
73
  temporal_features: z.array(z.object({
74
74
  columns: z.array(z.string()),
75
75
  format: z.string(),
@@ -79,7 +79,7 @@ export const TRAINING_INPUT_SCHEMA = {
79
79
  })).optional(),
80
80
  feature_weights: z.record(z.number()).optional(),
81
81
  transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "rank", "none"])).optional()
82
- .describe("Per-column transform before normalization. rank is train-only (no batch predict/project_columns). Not for train(impute)."),
82
+ .describe("Per-column transform before normalization see training_guidance for train-only / impute constraints."),
83
83
  auto_log_transforms: z.boolean().optional().default(false),
84
84
  time_delay_embeddings: z.array(z.object({
85
85
  feature: z.string(),
@@ -92,7 +92,7 @@ export const TRAINING_INPUT_SCHEMA = {
92
92
  })).optional(),
93
93
  normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal", "sepd"]), z.array(z.string())]).optional().default("auto"),
94
94
  normalization_methods: z.record(z.enum(["zscore", "mad", "sigmoidal", "sepd", "none"])).optional()
95
- .describe("Per-feature normalization override. Use MAD for heavy tails; SEPD for skewed financial/policy columns (see training_guidance). Default auto=zscore on non-cyclics. train(impute) rejects sepd."),
95
+ .describe("Per-feature normalization override see training_guidance and GET /v1/docs/normalization."),
96
96
  sigma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional()),
97
97
  learning_rate: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.union([z.number(), z.object({
98
98
  ordering: z.tuple([z.number(), z.number()]),
@@ -100,7 +100,7 @@ export const TRAINING_INPUT_SCHEMA = {
100
100
  })]).optional()),
101
101
  batch_size: z.number().int().optional(),
102
102
  te_panel_size: z.number().int().optional()
103
- .describe("Fixed evaluation-panel size for live topographic error during training (default clamp(grid_nodes*6, 500, 10000))"),
103
+ .describe("Fixed evaluation-panel size for live topographic error see training_guidance"),
104
104
  quality_metrics: z.union([z.enum(["fast", "standard", "full"]), z.array(z.string())]).optional(),
105
105
  backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().default("auto"),
106
106
  output_format: z.enum(["png", "pdf", "svg"]).optional().default("png"),
@@ -108,30 +108,30 @@ export const TRAINING_INPUT_SCHEMA = {
108
108
  colormap: z.string().optional(),
109
109
  row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional(),
110
110
  cv_folds: z.number().int().min(0).max(20).optional()
111
- .describe("impute only: quality.csv — linear cols: kfold_holdout_pool + resubstitution_optimistic; cyclic_pairs cols: circular metrics only (no Euclidean R²). Aggregate MAE/RMSE excludes cyclic columns. 0=off, default 5."),
111
+ .describe("impute only: quality.csv folds see training_guidance / prepare_training (default 5; 0=off)"),
112
112
  viz_mode: z.enum(["full", "summary", "summary_plus_top"]).optional()
113
- .describe("Visualization density; auto-capped when feature count > 40"),
113
+ .describe("Visualization density see training_guidance"),
114
114
  viz_top_components: z.number().int().min(0).max(64).optional()
115
- .describe("With viz_mode=summary_plus_top: upload top-N component maps by variance (default 8)"),
115
+ .describe("With viz_mode=summary_plus_top: top-N component maps"),
116
116
  viz_upload_cyclic_components: z.boolean().optional()
117
- .describe("When true with individual component PNGs, also publish cos/sin cyclic expansion planes (default false; recovered hour/month panels stay in combined.png)"),
117
+ .describe("Publish cos/sin cyclic expansion planes (default false)"),
118
118
  emit_cell_uncertainty: z.boolean().optional()
119
- .describe("impute: write imputation_uncertainty.csv (pool_std = prototype-neighbourhood dispersion, not calibrated SD; pool_n = contributing nodes)"),
119
+ .describe("impute: write imputation_uncertainty.csv see training_guidance"),
120
120
  gamma: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
121
- .describe("siom_map / impute (SIOM or auto on som_siom+): self-interaction strength γ (default 0.5). Prefer γ∈[0.5,1.5] on large n; γ>1.5 often diminishing returns."),
121
+ .describe("siom_map / impute SIOM γ see training_guidance (siom_gamma)"),
122
122
  gamma_f: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
123
- .describe("siom_map / impute SIOM: final γ for exponential decay (0 = constant γ). Pattern: start strict, relax late (e.g. 1.5→0.2)."),
123
+ .describe("siom_map / impute SIOM final γ see training_guidance (siom_gamma_f)"),
124
124
  siom_decay: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
125
- .describe("siom_map / impute SIOM: occupation EMA decay λ (default 0.01; 0 = cumulative). Avoid pairing high γ with aggressive decay (e.g. 0.05)."),
125
+ .describe("siom_map / impute SIOM occupation decay see training_guidance"),
126
126
  siom_penalty: z.enum(["linear", "log", "exp"]).optional()
127
- .describe("siom_map / impute SIOM: occupation penalty shape (linear default; prefer exp over log log often worsens TE)"),
127
+ .describe("siom_map / impute SIOM penalty shape — see training_guidance"),
128
128
  penalty_alpha: z.preprocess((v) => (v !== undefined && v !== null && typeof v === "string") ? parseFloat(v) : v, z.number().optional())
129
- .describe("siom_map / impute SIOM: scaling α inside log/exp penalty"),
129
+ .describe("siom_map / impute SIOM penalty α see training_guidance"),
130
130
  reset_per_epoch: z.boolean().optional()
131
- .describe("siom_map / impute SIOM: reset occupation μ each epoch (default false; avoid with high γ can collapse EV)"),
132
- siom_feature_geometry: z.enum(["l2", "mixed", "auto"]).optional().describe("siom_map: l2 (default) = legacy L2 on cos/sin columns; mixed = cyclic torus distance when cyclic pairs exist; auto = mixed only when cyclic encodings are present"),
133
- siom_qe_backend: z.enum(["cpu", "cuda", "auto"]).optional().describe("siom_map: backend for metric-aligned siom_qe when using mixed geometry; auto uses GPU when available"),
134
- siom_qe_batch_size: z.number().int().min(1).max(1_000_000).optional().describe("siom_map: batch size for GPU SIOM QE (default 256 on worker)"),
131
+ .describe("siom_map / impute SIOM reset occupation each epoch — see training_guidance"),
132
+ siom_feature_geometry: z.enum(["l2", "mixed", "auto"]).optional().describe("siom_map feature geometry see training_guidance"),
133
+ siom_qe_backend: z.enum(["cpu", "cuda", "auto"]).optional().describe("siom_map QE backend see training_guidance"),
134
+ siom_qe_batch_size: z.number().int().min(1).max(1_000_000).optional().describe("siom_map GPU QE batch size"),
135
135
  topology: z.enum(["free", "chain"]).optional().default("free"),
136
136
  max_nodes: z.number().int().min(2).optional(),
137
137
  effort: z.enum(["quick", "standard", "thorough"]).optional().default("standard"),
@@ -1,7 +1,16 @@
1
1
  import { z } from "zod";
2
2
  import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
3
3
  import { runMcpToolAudit } from "../audit.js";
4
+ import { logWarn } from "../logger.js";
4
5
  import { apiCall, BARSOM_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, structuredTextResult, } from "../shared.js";
6
+ /** Loose shape check — warn + pass through on failure (never drop a successful monitor payload). */
7
+ const TrainingMonitorStructuredSchema = z
8
+ .object({
9
+ type: z.literal("training-monitor"),
10
+ jobId: z.string(),
11
+ refresh_interval_ms: z.number(),
12
+ })
13
+ .passthrough();
5
14
  import { appendUiDeliveryContent, beginInlineFallback, resolveUiDelivery, tryAttachLearningCurve, } from "../ui-delivery.js";
6
15
  import { lastEpochTeFromCurves } from "../training_monitor_curve.js";
7
16
  import { buildVizStandaloneUrl, resultsExplorerStandaloneLinkText, } from "../viz_links.js";
@@ -114,8 +123,8 @@ export async function buildTrainingMonitorResult(job_id, opts) {
114
123
  timingParts.push(`epoch ${epoch}/${totalEpochs}`);
115
124
  const timingNote = timingParts.length > 0 ? ` ${timingParts.join(", ")}.` : "";
116
125
  const text = (preamble ? `${preamble}\n\n` : "") +
117
- `Training monitor (live when embedded, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
118
- `Optional UI; jobs(action=status) is enough to finish the workflow headless.`;
126
+ `Training monitor (standalone URL + optional App panel; refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s when mounted): job ${job_id} — ${status} (${progress.toFixed(1)}%).${timingNote} ` +
127
+ `For headless progress use jobs(action=status); this tool is optional.`;
119
128
  const content = [{ type: "text", text }];
120
129
  const terminal = status === "completed" || status === "failed" || status === "cancelled";
121
130
  let inlineImageAttached = false;
@@ -127,6 +136,7 @@ export async function buildTrainingMonitorResult(job_id, opts) {
127
136
  jobId: job_id,
128
137
  jobStatus: status,
129
138
  inlineImageAttached,
139
+ uiResourceUri: TRAINING_MONITOR_URI,
130
140
  });
131
141
  appendUiDeliveryContent(content, uiDelivery, {
132
142
  linkLabel: "Open training monitor",
@@ -142,6 +152,13 @@ export async function buildTrainingMonitorResult(job_id, opts) {
142
152
  }
143
153
  }
144
154
  structuredContent.ui_delivery = uiDelivery;
155
+ const parsed = TrainingMonitorStructuredSchema.safeParse(structuredContent);
156
+ if (!parsed.success) {
157
+ logWarn("training_monitor structuredContent validation failed — passing through", {
158
+ job_id,
159
+ issues: parsed.error.issues.slice(0, 5).map((i) => i.message),
160
+ });
161
+ }
145
162
  return { structuredContent, content, text };
146
163
  }
147
164
  /** After terminal + finalize, append results explorer open hint (monitor completion path). */
@@ -158,13 +175,13 @@ export function appendResultsExplorerCompletionHint(content, job_id) {
158
175
  export function registerTrainingMonitorTool(server) {
159
176
  registerAppTool(server, "training_monitor", {
160
177
  title: "Training Monitor",
161
- description: "Optional embedded or standalone view of training progress for a job_id. When embedded in a client that supports MCP Apps, the panel auto-refreshes every 5s until the job completes: live QE/TE learning curves (uniformly subsampled to ≤1000 batch samples per phase, not one point per epoch) plus a per-epoch hit-distribution heatmap (sampled BMU counts across the map, showing where observations are landing as training proceeds). A standalone browser URL is also available — its localhost port is per MCP session and goes stale if the proxy restarts (set BARIVIA_VIZ_PORT for a persistent port; re-run this tool for a fresh link). The same scalar fields are available via jobs(action=status)—not required to complete training or read results.",
178
+ description: "Optional view of training progress for a job_id. Prefer the standalone localhost URL in the tool result (always surfaced when the viz server is up) — some hosts list MCP Apps without mounting panels. When a host mounts the App, the panel auto-refreshes every 5s: QE/TE curves (≤1000 batch samples per phase) plus a hit-distribution heatmap. Set BARIVIA_VIZ_PORT for a stable port; re-run for a fresh link after proxy restart. Headless: jobs(action=status) has the same scalars not required to finish training or read results.",
162
179
  inputSchema: {
163
180
  job_id: z.string().describe("Training job ID to monitor"),
164
181
  fetch_training_log: z
165
182
  .boolean()
166
183
  .optional()
167
- .describe("Internal: merge completed-job training-log arrays when live progress is empty"),
184
+ .describe("[INTERNAL App/refresh path] Merge completed-job training-log arrays when live progress is empty. Agents should omit this."),
168
185
  },
169
186
  _meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
170
187
  }, async (args) => runMcpToolAudit("training_monitor", "default", args, async () => {
@@ -40,19 +40,19 @@ export function buildSmallNGuardrailLines(opts) {
40
40
  ? gridX * gridY
41
41
  : presetGridCells(preset);
42
42
  if (n < SMALL_N_HARD) {
43
- lines.push(`JUMPBACK — small-n (${n} rows): maps are fragile below ~100 rows. Prefer grid_x/grid_y ≈ ${autoSide}×${autoSide} (omit both for auto ≈ sqrt(5·√n)), not preset=quick/standard (15×15/25×25 → mostly dead nodes). Drop redundant geo features (keep distance OR lat/lon, not all three when |r|>0.9). Check hit_stats.active_node_fraction / TE before narrating insights.`);
43
+ lines.push(`JUMPBACK — small-n (${n} rows): prefer auto grid ≈ ${autoSide}×${autoSide} (omit grid_x/grid_y); oversized presets leave dead nodes. See training_guidance / prepare_training. Check hit_stats.active_node_fraction / TE before narrating insights.`);
44
44
  }
45
45
  else if (n < SMALL_N_SOFT) {
46
- lines.push(`Warning — small-n (${n} rows < 1000): oversized grids leave dead nodes. Auto grid ≈ ${autoSide}×${autoSide}; omit grid_x/grid_y for auto, or size so |features|×grid_cells is not ≫ n. preset=quick (15×15) is often too large below a few hundred rows — verify active_node_fraction after train.`);
46
+ lines.push(`Warning — small-n (${n} rows < 1000): auto grid ≈ ${autoSide}×${autoSide}; omit grid_x/grid_y or shrink oversized presets. See training_guidance. Verify active_node_fraction after train.`);
47
47
  }
48
48
  if (cells !== null && cells > n * 0.75) {
49
- lines.push(`Grid risk: ${cells} cells vs ${n} rows (cells n) → expect dead nodes / TE≈1. Use auto (~${autoSide}×${autoSide}) or a smaller explicit grid_x/grid_y.`);
49
+ lines.push(`Grid risk: ${cells} cells vs ${n} rows prefer auto (~${autoSide}×${autoSide}) or a smaller grid. See training_guidance.`);
50
50
  }
51
51
  if (nFeatures !== undefined &&
52
52
  nFeatures > 0 &&
53
53
  cells !== null &&
54
54
  nFeatures * cells > n * 2) {
55
- lines.push(`Capacity risk: ${nFeatures} features × ${cells} cells ${n} rows. Drop highly correlated columns (see analyze recommendations) and/or shrink the grid before train.`);
55
+ lines.push(`Capacity risk: ${nFeatures} features × ${cells} cells vs ${n} rows shrink grid and/or drop correlated columns (analyze). See training_guidance.`);
56
56
  }
57
57
  return lines;
58
58
  }
@@ -62,11 +62,11 @@ export function buildCpuScaleGuidanceLines(n) {
62
62
  return [];
63
63
  if (n <= SAMPLE_N_SUGGEST_THRESHOLD) {
64
64
  return [
65
- `Scale: on CPU, preset=quick (15×15) is typically very fast up to ~100k rows (tens of seconds). Do NOT call datasets(subset)/sample_n for n=${n.toLocaleString()} — train the full dataset_id unless the user asks to downsample. Only suggest sample_n when n>100k or the user requests it.`,
65
+ `Scale: n=${n.toLocaleString()} — do NOT auto-subset; train the full dataset_id unless the user asks. Timing/preset guidance: training_guidance (scale_cpu_quick).`,
66
66
  ];
67
67
  }
68
68
  return [
69
- `Scale: n=${n.toLocaleString()} > 100k — full-table train is still supported; suggest sample_n only if the user wants a faster exploratory pass or ops runbook says so. Never auto-subset silently.`,
69
+ `Scale: n=${n.toLocaleString()} > 100k — full-table train is supported; suggest sample_n only if the user asks. Never auto-subset silently. See training_guidance.`,
70
70
  ];
71
71
  }
72
72
  /** Periodicity caveat for analyze next_steps (row-order dependent). */
@@ -6,11 +6,20 @@ import { apiRawCall, getClientSupportsMcpApps, getVizPort, mimeForFilename, rese
6
6
  import { buildVizStandaloneUrl } from "./viz_links.js";
7
7
  export function parseUiDeliveryOverride() {
8
8
  const raw = (process.env.BARIVIA_UI_DELIVERY ?? "auto").trim().toLowerCase();
9
- if (raw === "apps" || raw === "localhost" || raw === "inline" || raw === "auto") {
9
+ if (raw === "apps" ||
10
+ raw === "localhost" ||
11
+ raw === "inline" ||
12
+ raw === "text_only" ||
13
+ raw === "auto") {
10
14
  return raw;
11
15
  }
12
16
  return "auto";
13
17
  }
18
+ /** Opt-in: force localhost under auto even when the host advertises MCP Apps (e.g. Cursor list-without-mount). */
19
+ export function preferLocalhostUi() {
20
+ const v = (process.env.BARIVIA_UI_PREFER_LOCALHOST ?? "").trim().toLowerCase();
21
+ return v === "1" || v === "true" || v === "yes";
22
+ }
14
23
  function vizPortPinned() {
15
24
  return Boolean(process.env.BARIVIA_VIZ_PORT?.trim());
16
25
  }
@@ -30,17 +39,26 @@ export function buildHealthUrl(jobId) {
30
39
  }
31
40
  function hintForTier(tier, tool, opts) {
32
41
  const label = tool === "training_monitor" ? "training monitor" : "results explorer";
42
+ const linkFirst = opts.standaloneUrl
43
+ ? `AGENT: Post a clickable markdown link first (e.g. "[Open ${label}](${opts.standaloneUrl})") — do not bury it after metrics.`
44
+ : undefined;
33
45
  switch (tier) {
34
46
  case "embedded":
35
- return `The ${label} MCP App panel is embedded in this client. Also mention the standalone URL if the user may open a browser tab.`;
47
+ return linkFirst
48
+ ? `${linkFirst} Embedded MCP App panel if the host mounts it; some hosts list ui:// without mounting (use the URL).`
49
+ : `The ${label} MCP App panel may embed. Use jobs(status) / results(get) if no panel or URL appears.`;
36
50
  case "localhost":
37
- return opts.standaloneUrl
38
- ? `AGENT: Post a clickable markdown link to the user (e.g. "[Open ${label}](${opts.standaloneUrl})") — do not bury it in tool output or use broken workspace file paths. Re-post after the job completes if status was running.`
51
+ return linkFirst
52
+ ? `${linkFirst} Re-post after the job completes if status was running. Set BARIVIA_VIZ_PORT for a stable port.`
39
53
  : `AGENT: Viz server unavailable — use jobs(status) and results(get) for progress and figures.`;
40
54
  case "inline_image":
41
- return `AGENT: Inline raster figure(s) are attached below. Summarize metrics in your reply; offer results_explorer or results(get) for additional figures.`;
55
+ return linkFirst
56
+ ? `${linkFirst} Inline raster figure(s) may also be attached; summarize metrics; offer results_explorer or results(get) for more.`
57
+ : `AGENT: Inline raster figure(s) are attached below. Summarize metrics; offer results_explorer or results(get) for additional figures.`;
42
58
  case "text_only":
43
- return `AGENT: No embedded panel or localhost viz — use jobs(status) for training progress and results(get) or results_explorer for figures.`;
59
+ return linkFirst
60
+ ? `${linkFirst} Otherwise use jobs(status) for progress and results(get) for figures.`
61
+ : `AGENT: No embedded panel or localhost viz — use jobs(status) for training progress and results(get) or results_explorer for figures.`;
44
62
  }
45
63
  }
46
64
  export function resolveUiDelivery(options) {
@@ -58,9 +76,15 @@ export function resolveUiDelivery(options) {
58
76
  else if (override === "inline") {
59
77
  delivery = options.inlineImageAttached ? "inline_image" : "text_only";
60
78
  }
79
+ else if (override === "text_only") {
80
+ delivery = "text_only";
81
+ }
61
82
  else {
62
- // auto
63
- if (mcpApps) {
83
+ // auto — default unchanged: prefer embedded when Apps advertised unless opt-in prefer-localhost
84
+ if (preferLocalhostUi() && port) {
85
+ delivery = "localhost";
86
+ }
87
+ else if (mcpApps) {
64
88
  delivery = "embedded";
65
89
  }
66
90
  else if (port) {
@@ -73,12 +97,13 @@ export function resolveUiDelivery(options) {
73
97
  delivery = "text_only";
74
98
  }
75
99
  }
100
+ // Always include standalone URL when the viz port exists (all tiers) — hosts may not mount Apps.
76
101
  const urls = [];
77
- if (standaloneUrl && (delivery === "embedded" || delivery === "localhost")) {
102
+ if (standaloneUrl) {
78
103
  urls.push(standaloneUrl);
79
104
  }
80
105
  const healthUrl = buildHealthUrl(options.jobId);
81
- if (healthUrl && delivery === "localhost") {
106
+ if (healthUrl && (delivery === "localhost" || preferLocalhostUi())) {
82
107
  urls.push(healthUrl);
83
108
  }
84
109
  const crossLink = options.tool === "training_monitor" && options.jobStatus === "completed"
@@ -97,6 +122,7 @@ export function resolveUiDelivery(options) {
97
122
  viz_port: port,
98
123
  port_pinned: vizPortPinned(),
99
124
  override,
125
+ ...(options.uiResourceUri ? { ui_resource_uri: options.uiResourceUri } : {}),
100
126
  };
101
127
  }
102
128
  /**
@@ -154,6 +180,7 @@ export function getMcpClientDiagnostics() {
154
180
  const override = parseUiDeliveryOverride();
155
181
  const mcpApps = getClientSupportsMcpApps();
156
182
  const port = getVizPort() || null;
183
+ const preferLocal = preferLocalhostUi();
157
184
  let activeTier = "text_only";
158
185
  if (override === "apps") {
159
186
  activeTier = mcpApps ? "embedded" : port ? "localhost" : "text_only";
@@ -164,6 +191,12 @@ export function getMcpClientDiagnostics() {
164
191
  else if (override === "inline") {
165
192
  activeTier = "inline_image";
166
193
  }
194
+ else if (override === "text_only") {
195
+ activeTier = "text_only";
196
+ }
197
+ else if (preferLocal && port) {
198
+ activeTier = "localhost";
199
+ }
167
200
  else if (mcpApps) {
168
201
  activeTier = "embedded";
169
202
  }
@@ -175,6 +208,7 @@ export function getMcpClientDiagnostics() {
175
208
  viz_port: port,
176
209
  viz_port_pinned: vizPortPinned(),
177
210
  ui_delivery_override: override,
211
+ ui_prefer_localhost: preferLocal,
178
212
  active_delivery_tier: activeTier,
179
213
  health_url: buildHealthUrl(),
180
214
  };
@@ -305,7 +305,7 @@ Note: This type uses \`Record<K, string | undefined>\` rather than \`Partial<Rec
305
305
  for compatibility with Zod schema generation. Both are functionally equivalent for validation.`);c.object({method:c.literal("ui/open-link"),params:c.object({url:c.string().describe("URL to open in the host's browser")})});var lI=c.object({isError:c.boolean().optional().describe("True if the host failed to open the URL (e.g., due to security policy).")}).passthrough(),cI=c.object({isError:c.boolean().optional().describe("True if the host rejected or failed to deliver the message.")}).passthrough();c.object({method:c.literal("ui/notifications/sandbox-proxy-ready"),params:c.object({})});var oo=c.object({connectDomains:c.array(c.string()).optional().describe("Origins for network requests (fetch/XHR/WebSocket)."),resourceDomains:c.array(c.string()).optional().describe("Origins for static resources (scripts, images, styles, fonts)."),frameDomains:c.array(c.string()).optional().describe("Origins for nested iframes (frame-src directive)."),baseUriDomains:c.array(c.string()).optional().describe("Allowed base URIs for the document (base-uri directive).")}),so=c.object({camera:c.object({}).optional().describe("Request camera access (Permission Policy `camera` feature)."),microphone:c.object({}).optional().describe("Request microphone access (Permission Policy `microphone` feature)."),geolocation:c.object({}).optional().describe("Request geolocation access (Permission Policy `geolocation` feature)."),clipboardWrite:c.object({}).optional().describe("Request clipboard write access (Permission Policy `clipboard-write` feature).")});c.object({method:c.literal("ui/notifications/size-changed"),params:c.object({width:c.number().optional().describe("New width in pixels."),height:c.number().optional().describe("New height in pixels.")})});var dI=c.object({method:c.literal("ui/notifications/tool-input"),params:c.object({arguments:c.record(c.string(),c.unknown().describe("Complete tool call arguments as key-value pairs.")).optional().describe("Complete tool call arguments as key-value pairs.")})}),mI=c.object({method:c.literal("ui/notifications/tool-input-partial"),params:c.object({arguments:c.record(c.string(),c.unknown().describe("Partial tool call arguments (incomplete, may change).")).optional().describe("Partial tool call arguments (incomplete, may change).")})}),fI=c.object({method:c.literal("ui/notifications/tool-cancelled"),params:c.object({reason:c.string().optional().describe('Optional reason for the cancellation (e.g., "user action", "timeout").')})}),pI=c.object({fonts:c.string().optional()}),vI=c.object({variables:uI.optional().describe("CSS variables for theming the app."),css:pI.optional().describe("CSS blocks that apps can inject.")}),hI=c.object({method:c.literal("ui/resource-teardown"),params:c.object({})});c.record(c.string(),c.unknown());var Go=c.object({text:c.object({}).optional().describe("Host supports text content blocks."),image:c.object({}).optional().describe("Host supports image content blocks."),audio:c.object({}).optional().describe("Host supports audio content blocks."),resource:c.object({}).optional().describe("Host supports resource content blocks."),resourceLink:c.object({}).optional().describe("Host supports resource link content blocks."),structuredContent:c.object({}).optional().describe("Host supports structured content.")}),gI=c.object({experimental:c.object({}).optional().describe("Experimental features (structure TBD)."),openLinks:c.object({}).optional().describe("Host supports opening external URLs."),serverTools:c.object({listChanged:c.boolean().optional().describe("Host supports tools/list_changed notifications.")}).optional().describe("Host can proxy tool calls to the MCP server."),serverResources:c.object({listChanged:c.boolean().optional().describe("Host supports resources/list_changed notifications.")}).optional().describe("Host can proxy resource reads to the MCP server."),logging:c.object({}).optional().describe("Host accepts log messages."),sandbox:c.object({permissions:so.optional().describe("Permissions granted by the host (camera, microphone, geolocation)."),csp:oo.optional().describe("CSP domains approved by the host.")}).optional().describe("Sandbox configuration applied by the host."),updateModelContext:Go.optional().describe("Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns."),message:Go.optional().describe("Host supports receiving content messages (ui/message) from the view.")}),_I=c.object({experimental:c.object({}).optional().describe("Experimental features (structure TBD)."),tools:c.object({listChanged:c.boolean().optional().describe("App supports tools/list_changed notifications.")}).optional().describe("App exposes MCP-style tools that the host can call."),availableDisplayModes:c.array($t).optional().describe("Display modes the app supports.")});c.object({method:c.literal("ui/notifications/initialized"),params:c.object({}).optional()});c.object({csp:oo.optional().describe("Content Security Policy configuration."),permissions:so.optional().describe("Sandbox permissions requested by the UI."),domain:c.string().optional().describe("Dedicated origin for view sandbox."),prefersBorder:c.boolean().optional().describe("Visual boundary preference - true if UI prefers a visible border.")});c.object({method:c.literal("ui/request-display-mode"),params:c.object({mode:$t.describe("The display mode being requested.")})});var $I=c.object({mode:$t.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),bI=c.union([c.literal("model"),c.literal("app")]).describe("Tool visibility scope - who can access the tool.");c.object({resourceUri:c.string().optional(),visibility:c.array(bI).optional().describe(`Who can access this tool. Default: ["model", "app"]
306
306
  - "model": Tool visible to and callable by the agent
307
307
  - "app": Tool callable by the app from this server only`)});c.object({mimeTypes:c.array(c.string()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});c.object({method:c.literal("ui/message"),params:c.object({role:c.literal("user").describe('Message role, currently only "user" is supported.'),content:c.array(Ot).describe("Message content blocks (text, image, etc.).")})});c.object({method:c.literal("ui/notifications/sandbox-resource-ready"),params:c.object({html:c.string().describe("HTML content to load into the inner iframe."),sandbox:c.string().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:oo.optional().describe("CSP configuration from resource metadata."),permissions:so.optional().describe("Sandbox permissions from resource metadata.")})});var yI=c.object({method:c.literal("ui/notifications/tool-result"),params:ji.describe("Standard MCP tool execution result.")}),Of=c.object({toolInfo:c.object({id:wt.optional().describe("JSON-RPC id of the tools/call request."),tool:cr.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:oI.optional().describe("Current color theme preference."),styles:vI.optional().describe("Style configuration for theming the app."),displayMode:$t.optional().describe("How the UI is currently displayed."),availableDisplayModes:c.array($t).optional().describe("Display modes the host supports."),containerDimensions:c.union([c.object({height:c.number().describe("Fixed container height in pixels.")}),c.object({maxHeight:c.union([c.number(),c.undefined()]).optional().describe("Maximum container height in pixels.")})]).and(c.union([c.object({width:c.number().describe("Fixed container width in pixels.")}),c.object({maxWidth:c.union([c.number(),c.undefined()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
308
- container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:c.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:c.string().optional().describe("User's timezone in IANA format."),userAgent:c.string().optional().describe("Host application identifier."),platform:c.union([c.literal("web"),c.literal("desktop"),c.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:c.object({touch:c.boolean().optional().describe("Whether the device supports touch input."),hover:c.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:c.object({top:c.number().describe("Top safe area inset in pixels."),right:c.number().describe("Right safe area inset in pixels."),bottom:c.number().describe("Bottom safe area inset in pixels."),left:c.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),kI=c.object({method:c.literal("ui/notifications/host-context-changed"),params:Of.describe("Partial context update containing only changed fields.")});c.object({method:c.literal("ui/update-model-context"),params:c.object({content:c.array(Ot).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:c.record(c.string(),c.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});c.object({method:c.literal("ui/initialize"),params:c.object({appInfo:Ti.describe("App identification (name and version)."),appCapabilities:_I.describe("Features and capabilities this app provides."),protocolVersion:c.string().describe("Protocol version this app supports.")})});var II=c.object({protocolVersion:c.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ti.describe("Host application identification and version."),hostCapabilities:gI.describe("Features and capabilities provided by the host."),hostContext:Of.describe("Rich context about the host environment.")}).passthrough();class wI extends S${_appInfo;_capabilities;options;_hostCapabilities;_hostInfo;_hostContext;constructor(t,n={},r={autoResize:!0}){super(r),this._appInfo=t,this._capabilities=n,this.options=r,this.setRequestHandler(Ni,i=>(console.log("Received ping:",i.params),{})),this.onhostcontextchanged=()=>{}}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}set ontoolinput(t){this.setNotificationHandler(dI,n=>t(n.params))}set ontoolinputpartial(t){this.setNotificationHandler(mI,n=>t(n.params))}set ontoolresult(t){this.setNotificationHandler(yI,n=>t(n.params))}set ontoolcancelled(t){this.setNotificationHandler(fI,n=>t(n.params))}set onhostcontextchanged(t){this.setNotificationHandler(kI,n=>{this._hostContext={...this._hostContext,...n.params},t(n.params)})}set onteardown(t){this.setRequestHandler(hI,(n,r)=>t(n.params,r))}set oncalltool(t){this.setRequestHandler(Ls,(n,r)=>t(n.params,r))}set onlisttools(t){this.setRequestHandler(Cs,(n,r)=>t(n.params,r))}assertCapabilityForMethod(t){}assertRequestHandlerCapability(t){switch(t){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${t})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${t} registered`)}}assertNotificationCapability(t){}assertTaskCapability(t){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(t){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(t,n){return await this.request({method:"tools/call",params:t},ji,n)}sendMessage(t,n){return this.request({method:"ui/message",params:t},cI,n)}sendLog(t){return this.notification({method:"notifications/message",params:t})}updateModelContext(t,n){return this.request({method:"ui/update-model-context",params:t},Gn,n)}openLink(t,n){return this.request({method:"ui/open-link",params:t},lI,n)}sendOpenLink=this.openLink;requestDisplayMode(t,n){return this.request({method:"ui/request-display-mode",params:t},$I,n)}sendSizeChanged(t){return this.notification({method:"ui/notifications/size-changed",params:t})}setupSizeChangedNotifications(){let t=!1,n=0,r=0,i=()=>{t||(t=!0,requestAnimationFrame(()=>{t=!1;let o=document.documentElement,s=o.style.width,u=o.style.height;o.style.width="fit-content",o.style.height="fit-content";let l=o.getBoundingClientRect();o.style.width=s,o.style.height=u;let d=window.innerWidth-o.clientWidth,f=Math.ceil(l.width+d),p=Math.ceil(l.height);(f!==n||p!==r)&&(n=f,r=p,this.sendSizeChanged({width:f,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(t=new z$(window.parent,window.parent),n){await super.connect(t);try{let r=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Z$}},II,n);if(r===void 0)throw Error(`Server sent invalid initialize result: ${r}`);this._hostCapabilities=r.hostCapabilities,this._hostInfo=r.hostInfo,this._hostContext=r.hostContext,await this.notification({method:"ui/notifications/initialized"}),this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(r){throw this.close(),r}}}let Ze=null,rt=null,Ie=null,An=null;const Ko=new Map,mi=document.getElementById("loading"),SI=document.getElementById("app"),Tf=document.getElementById("title"),Nf=document.getElementById("subtitle"),xI=document.getElementById("metrics"),zI=document.getElementById("highlights"),bt=document.getElementById("figure-select"),ZI=document.getElementById("figure-title"),UI=document.getElementById("figure-file"),OI=document.getElementById("figure-caption"),$e=document.getElementById("main-image"),ci=document.getElementById("open-standalone"),Zn=document.getElementById("open-monitor-btn"),Ge=document.getElementById("copy-url"),Xo=document.getElementById("related-monitor"),TI=document.getElementById("download-current");function NI(e){const t=String(e.job_type??"train_som"),n=e.grid??[],r=e.quantization_error!=null?Number(e.quantization_error).toFixed(4):"N/A",i=e.topographic_error!=null?Number(e.topographic_error).toFixed(4):"N/A",a=String(e.n_samples??"N/A"),o=String(e.n_features??"N/A"),s=e.coverage??{},u=l=>l!=null?`${(Number(l)*100).toFixed(1)}%`:"N/A";return t==="train_floop_siom"?[{label:"Topology",value:String(e.topology??"free")},{label:"Nodes",value:String(e.occupied_nodes??e.final_length??"N/A")},{label:"QE",value:r},{label:"TE",value:i},{label:"Utilization",value:u(s.utilization)},{label:"Dead",value:u(s.dead_fraction)},{label:"Coverage",value:String(s.status??"N/A")},{label:"Samples",value:a},{label:"Features",value:o}]:t==="train_siom"?[{label:"Grid",value:n.length>=2?`${n[0]}×${n[1]}`:"N/A"},{label:"Model",value:"SIOM"},{label:"QE",value:r},{label:"TE",value:i},{label:"Utilization",value:u(s.utilization)},{label:"Dead",value:u(s.dead_fraction)},{label:"Coverage",value:String(s.status??"N/A")},{label:"Samples",value:a},{label:"Features",value:o}]:[{label:"Grid",value:n.length>=2?`${n[0]}×${n[1]}`:"N/A"},{label:"Model",value:String(e.model??"SOM")},{label:"QE",value:r},{label:"TE",value:i},{label:"Samples",value:a},{label:"Features",value:o}]}function jI(e,t){const n=e.summary??{},r=(n.files??[]).filter(s=>/\.(png|pdf|svg)$/i.test(s)),i=String(n.output_format??"png"),o=(r.length>0?r:[`combined.${i}`]).map(s=>{const u=s.replace(/\.(png|pdf|svg)$/i,""),l=u.startsWith("component_")?"component":u==="combined"||u==="coverage_overview"?"summary":"diagnostic";return{key:u,label:u.replace(/^component_\d+_/,"Component: ").replace(/_/g," "),filename:s,kind:l,caption:/\.(png|jpe?g|gif|webp)$/i.test(s)?void 0:"This figure is available for download, but it is not inline-displayable in this first iteration.",url:/\.(png|jpe?g|gif|webp)$/i.test(s)?`/api/results/${t}/image/${s}`:void 0}});return{type:"results-explorer",jobId:t,title:"Results Explorer",subtitle:String(e.label??""),metrics:NI(n),highlights:[n.training_duration_seconds!=null?`Training duration: ${n.training_duration_seconds}s`:"",n.selected_columns?`Variables: ${n.selected_columns.join(", ")}`:""].filter(Boolean),availableFigures:o,defaultFigureKey:o.find(s=>s.key==="combined"&&s.url)?.key??o.find(s=>!!s.url)?.key??o.find(s=>s.key==="combined")?.key??o[0]?.key,trainingMonitorUrl:`/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(t)}`,standaloneUrl:`${window.location.origin}/viz/results-explorer?mode=standalone&job_id=${encodeURIComponent(t)}`,relatedUrls:{trainingMonitor:`${window.location.origin}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(t)}`,resultsExplorer:`${window.location.origin}/viz/results-explorer?mode=standalone&job_id=${encodeURIComponent(t)}`}}}function PI(e){xI.innerHTML=e.map(t=>`<div class="metric"><span class="metric-label">${t.label}</span><span class="metric-value">${t.value}</span></div>`).join("")}function EI(e){zI.innerHTML=e.length>0?e.map(t=>`<div class="highlight">${t}</div>`).join(""):'<div class="highlight">No extra highlights for this result yet.</div>'}const DI={summary:"Summary",diagnostic:"Diagnostics",component:"Components",artifact:"Other"},RI=["summary","diagnostic","component","artifact"];function AI(e){const t=new Map;for(const n of e){const r=n.kind??"diagnostic",i=t.get(r)??[];i.push(n),t.set(r,i)}bt.innerHTML="";for(const n of RI){const r=t.get(n);if(!r||r.length===0)continue;const i=document.createElement("optgroup");i.label=DI[n]??n;for(const a of r){const o=document.createElement("option");o.value=a.key,o.textContent=`${a.label} (${a.filename})`,a.key===rt?.key&&(o.selected=!0),i.appendChild(o)}bt.appendChild(i)}}bt.addEventListener("change",()=>{const e=Ze?.availableFigures.find(t=>t.key===bt.value);e&&jf(e)});async function CI(e,t){const n=`${e}/${t}`,r=Ko.get(n);if(r)return r;if(!Ie)return null;try{const a=(await Ie.callServerTool({name:"_fetch_figure",arguments:{job_id:e,filename:t}})).content?.find(o=>o.type==="image");if(a){const o=`data:${a.mimeType};base64,${a.data}`;return Ko.set(n,o),o}}catch{}return null}function LI(e){!Ie||!Ze||Ie.updateModelContext({content:[{type:"text",text:`User is viewing the "${e.label}" figure (${e.filename}) of results job ${Ze.jobId} in the Results Explorer.`}]}).catch(()=>{})}async function jf(e){if(rt=e,ZI.textContent=e.label,UI.textContent=e.filename,OI.textContent=e.caption??"",bt.value=e.key,LI(e),e.url)$e.src=e.url;else if(Ie&&Ze){const t=Ze.defaultFigureKey;if(e.key===t&&An)$e.src=An;else{$e.removeAttribute("src");const n=await CI(Ze.jobId,e.filename);n&&rt?.key===e.key&&($e.src=n)}}else $e.removeAttribute("src")}function MI(e){return e.standaloneUrl??window.location.href}function FI(e){const t=MI(e);e.standaloneUrl?(ci.href=e.standaloneUrl,ci.style.display="inline-flex"):Ef&&(ci.href=window.location.href,ci.style.display="inline-flex"),Ge.style.display="inline-flex",Ge.dataset.url=t;const n=e.relatedUrls?.trainingMonitor;n&&(Xo.href=n,Xo.style.display="inline-flex")}function Yo(e){Ze=e,Tf.textContent=e.title,Nf.textContent=e.subtitle??`Job ${e.jobId}`,PI(e.metrics),EI(e.highlights),FI(e);const t=e.trainingMonitorUrl??e.relatedUrls?.trainingMonitor??`/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(e.jobId)}`;Zn.href=t,Zn.style.display="inline-flex",Zn.title="View live or completed training curves for this job";const n=e.availableFigures.find(r=>r.key===e.defaultFigureKey)??e.availableFigures[0]??null;rt=n,AI(e.availableFigures),n&&jf(n),mi.style.display="none",SI.style.display="block"}TI.addEventListener("click",()=>{const e=$e.src;if(!e||!rt)return;const t=document.createElement("a");t.href=e,t.download=rt.filename,t.click()});Ge.addEventListener("click",async()=>{const e=Ge.dataset.url||Ze?.standaloneUrl||window.location.href;try{await navigator.clipboard.writeText(e),Ge.textContent="Copied!",setTimeout(()=>{Ge.textContent="Copy explorer URL"},1500)}catch{window.prompt("Copy explorer URL:",e)}});$e.style.cursor="zoom-in";$e.addEventListener("click",()=>{if(!Ie||!$e.src)return;const e=Ie.getHostContext?.(),t=e?.availableDisplayModes;if(!Array.isArray(t)||!t.includes("fullscreen"))return;const n=e?.displayMode==="fullscreen"?"inline":"fullscreen";Ie.requestDisplayMode({mode:n}).then(r=>{$e.style.cursor=r.mode==="fullscreen"?"zoom-out":"zoom-in"}).catch(()=>{})});const Pf=new URLSearchParams(window.location.search),Ef=Pf.get("mode")==="standalone",di=Pf.get("job_id");if(Ef&&di){const e="The local viz port is assigned per MCP session and changes when the proxy restarts. Re-run results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port.";(async()=>{for(let n=1;n<=3;n++)try{const r=await fetch(`/api/results/${di}`);if(r.status===404){mi.textContent=`Results for job ${di} were not found (HTTP 404). The job may still be finalizing or was deleted, or this link points at a stale session. ${e}`;return}if(!r.ok)throw new Error(`HTTP ${r.status}`);const i=await r.json();Yo(jI(i,di));return}catch(r){if(n<3){await new Promise(i=>setTimeout(i,1e3*n));continue}mi.textContent=`Could not load results from the local viz server (${r instanceof Error?r.message:"error"}). ${e}`}})()}else{const e=new wI({name:"Results Explorer",version:"1.0.0"},{},{autoResize:!0});Ie=e,e.ontoolinput=t=>{const n=t?.arguments??{},r=n.job_id!=null?String(n.job_id):"";r&&(Tf.textContent="Results Explorer",Nf.textContent=`Loading job ${r}...`)},e.ontoolresult=t=>{const n=t.content?.find(i=>i.type==="image");n&&(An=`data:${n.mimeType};base64,${n.data}`);const r=t.structuredContent;if(!r||typeof r!="object"){mi.textContent="The results explorer did not receive structured data.";return}Yo(r)},e.connect()}</script>
308
+ container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:c.string().optional().describe("User's language and region preference in BCP 47 format."),timeZone:c.string().optional().describe("User's timezone in IANA format."),userAgent:c.string().optional().describe("Host application identifier."),platform:c.union([c.literal("web"),c.literal("desktop"),c.literal("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:c.object({touch:c.boolean().optional().describe("Whether the device supports touch input."),hover:c.boolean().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:c.object({top:c.number().describe("Top safe area inset in pixels."),right:c.number().describe("Right safe area inset in pixels."),bottom:c.number().describe("Bottom safe area inset in pixels."),left:c.number().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),kI=c.object({method:c.literal("ui/notifications/host-context-changed"),params:Of.describe("Partial context update containing only changed fields.")});c.object({method:c.literal("ui/update-model-context"),params:c.object({content:c.array(Ot).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:c.record(c.string(),c.unknown().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});c.object({method:c.literal("ui/initialize"),params:c.object({appInfo:Ti.describe("App identification (name and version)."),appCapabilities:_I.describe("Features and capabilities this app provides."),protocolVersion:c.string().describe("Protocol version this app supports.")})});var II=c.object({protocolVersion:c.string().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Ti.describe("Host application identification and version."),hostCapabilities:gI.describe("Features and capabilities provided by the host."),hostContext:Of.describe("Rich context about the host environment.")}).passthrough();class wI extends S${_appInfo;_capabilities;options;_hostCapabilities;_hostInfo;_hostContext;constructor(t,n={},r={autoResize:!0}){super(r),this._appInfo=t,this._capabilities=n,this.options=r,this.setRequestHandler(Ni,i=>(console.log("Received ping:",i.params),{})),this.onhostcontextchanged=()=>{}}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}set ontoolinput(t){this.setNotificationHandler(dI,n=>t(n.params))}set ontoolinputpartial(t){this.setNotificationHandler(mI,n=>t(n.params))}set ontoolresult(t){this.setNotificationHandler(yI,n=>t(n.params))}set ontoolcancelled(t){this.setNotificationHandler(fI,n=>t(n.params))}set onhostcontextchanged(t){this.setNotificationHandler(kI,n=>{this._hostContext={...this._hostContext,...n.params},t(n.params)})}set onteardown(t){this.setRequestHandler(hI,(n,r)=>t(n.params,r))}set oncalltool(t){this.setRequestHandler(Ls,(n,r)=>t(n.params,r))}set onlisttools(t){this.setRequestHandler(Cs,(n,r)=>t(n.params,r))}assertCapabilityForMethod(t){}assertRequestHandlerCapability(t){switch(t){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${t})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${t} registered`)}}assertNotificationCapability(t){}assertTaskCapability(t){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(t){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(t,n){return await this.request({method:"tools/call",params:t},ji,n)}sendMessage(t,n){return this.request({method:"ui/message",params:t},cI,n)}sendLog(t){return this.notification({method:"notifications/message",params:t})}updateModelContext(t,n){return this.request({method:"ui/update-model-context",params:t},Gn,n)}openLink(t,n){return this.request({method:"ui/open-link",params:t},lI,n)}sendOpenLink=this.openLink;requestDisplayMode(t,n){return this.request({method:"ui/request-display-mode",params:t},$I,n)}sendSizeChanged(t){return this.notification({method:"ui/notifications/size-changed",params:t})}setupSizeChangedNotifications(){let t=!1,n=0,r=0,i=()=>{t||(t=!0,requestAnimationFrame(()=>{t=!1;let o=document.documentElement,s=o.style.width,u=o.style.height;o.style.width="fit-content",o.style.height="fit-content";let l=o.getBoundingClientRect();o.style.width=s,o.style.height=u;let d=window.innerWidth-o.clientWidth,f=Math.ceil(l.width+d),p=Math.ceil(l.height);(f!==n||p!==r)&&(n=f,r=p,this.sendSizeChanged({width:f,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(t=new z$(window.parent,window.parent),n){await super.connect(t);try{let r=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:Z$}},II,n);if(r===void 0)throw Error(`Server sent invalid initialize result: ${r}`);this._hostCapabilities=r.hostCapabilities,this._hostInfo=r.hostInfo,this._hostContext=r.hostContext,await this.notification({method:"ui/notifications/initialized"}),this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(r){throw this.close(),r}}}let Ze=null,rt=null,Ie=null,An=null;const Ko=new Map,mi=document.getElementById("loading"),SI=document.getElementById("app"),Tf=document.getElementById("title"),Nf=document.getElementById("subtitle"),xI=document.getElementById("metrics"),zI=document.getElementById("highlights"),bt=document.getElementById("figure-select"),ZI=document.getElementById("figure-title"),UI=document.getElementById("figure-file"),OI=document.getElementById("figure-caption"),$e=document.getElementById("main-image"),ci=document.getElementById("open-standalone"),Zn=document.getElementById("open-monitor-btn"),Ge=document.getElementById("copy-url"),Xo=document.getElementById("related-monitor"),TI=document.getElementById("download-current");function NI(e){const t=String(e.job_type??"train_som"),n=e.grid??[],r=e.quantization_error!=null?Number(e.quantization_error).toFixed(4):"N/A",i=e.topographic_error!=null?Number(e.topographic_error).toFixed(4):"N/A",a=String(e.n_samples??"N/A"),o=String(e.n_features??"N/A"),s=e.coverage??{},u=l=>l!=null?`${(Number(l)*100).toFixed(1)}%`:"N/A";return t==="train_floop_siom"?[{label:"Topology",value:String(e.topology??"free")},{label:"Nodes",value:String(e.occupied_nodes??e.final_length??"N/A")},{label:"QE",value:r},{label:"TE",value:i},{label:"Utilization",value:u(s.utilization)},{label:"Dead",value:u(s.dead_fraction)},{label:"Coverage",value:String(s.status??"N/A")},{label:"Samples",value:a},{label:"Features",value:o}]:t==="train_siom"?[{label:"Grid",value:n.length>=2?`${n[0]}×${n[1]}`:"N/A"},{label:"Model",value:"SIOM"},{label:"QE",value:r},{label:"TE",value:i},{label:"Utilization",value:u(s.utilization)},{label:"Dead",value:u(s.dead_fraction)},{label:"Coverage",value:String(s.status??"N/A")},{label:"Samples",value:a},{label:"Features",value:o}]:[{label:"Grid",value:n.length>=2?`${n[0]}×${n[1]}`:"N/A"},{label:"Model",value:String(e.model??"SOM")},{label:"QE",value:r},{label:"TE",value:i},{label:"Samples",value:a},{label:"Features",value:o}]}function jI(e,t){const n=e.summary??{},r=(n.files??[]).filter(s=>/\.(png|pdf|svg)$/i.test(s)),i=String(n.output_format??"png"),o=(r.length>0?r:[`combined.${i}`]).map(s=>{const u=s.replace(/\.(png|pdf|svg)$/i,""),l=u.startsWith("component_")?"component":u==="combined"||u==="coverage_overview"?"summary":"diagnostic";return{key:u,label:u.replace(/^component_\d+_/,"Component: ").replace(/_/g," "),filename:s,kind:l,caption:/\.(png|jpe?g|gif|webp)$/i.test(s)?void 0:"This figure is available for download, but it is not inline-displayable in this first iteration.",url:/\.(png|jpe?g|gif|webp)$/i.test(s)?`/api/results/${t}/image/${s}`:void 0}});return{type:"results-explorer",jobId:t,title:"Results Explorer",subtitle:String(e.label??""),metrics:NI(n),highlights:[n.training_duration_seconds!=null?`Training duration: ${n.training_duration_seconds}s`:"",n.selected_columns?`Variables: ${n.selected_columns.join(", ")}`:""].filter(Boolean),availableFigures:o,defaultFigureKey:o.find(s=>s.key==="combined"&&s.url)?.key??o.find(s=>!!s.url)?.key??o.find(s=>s.key==="combined")?.key??o[0]?.key,trainingMonitorUrl:`/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(t)}`,standaloneUrl:`${window.location.origin}/viz/results-explorer?mode=standalone&job_id=${encodeURIComponent(t)}`,relatedUrls:{trainingMonitor:`${window.location.origin}/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(t)}`,resultsExplorer:`${window.location.origin}/viz/results-explorer?mode=standalone&job_id=${encodeURIComponent(t)}`}}}function PI(e){xI.innerHTML=e.map(t=>`<div class="metric"><span class="metric-label">${t.label}</span><span class="metric-value">${t.value}</span></div>`).join("")}function EI(e){zI.innerHTML=e.length>0?e.map(t=>`<div class="highlight">${t}</div>`).join(""):'<div class="highlight">No extra highlights for this result yet.</div>'}const DI={summary:"Summary",diagnostic:"Diagnostics",component:"Components",artifact:"Other"},RI=["summary","diagnostic","component","artifact"];function AI(e){const t=new Map;for(const n of e){const r=n.kind??"diagnostic",i=t.get(r)??[];i.push(n),t.set(r,i)}bt.innerHTML="";for(const n of RI){const r=t.get(n);if(!r||r.length===0)continue;const i=document.createElement("optgroup");i.label=DI[n]??n;for(const a of r){const o=document.createElement("option");o.value=a.key,o.textContent=`${a.label} (${a.filename})`,a.key===rt?.key&&(o.selected=!0),i.appendChild(o)}bt.appendChild(i)}}bt.addEventListener("change",()=>{const e=Ze?.availableFigures.find(t=>t.key===bt.value);e&&jf(e)});async function CI(e,t){const n=`${e}/${t}`,r=Ko.get(n);if(r)return r;if(!Ie)return null;try{const a=(await Ie.callServerTool({name:"_fetch_figure",arguments:{job_id:e,filename:t}})).content?.find(o=>o.type==="image");if(a){const o=`data:${a.mimeType};base64,${a.data}`;return Ko.set(n,o),o}}catch{}return null}function LI(e){!Ie||!Ze||Ie.updateModelContext({content:[{type:"text",text:`User is viewing the "${e.label}" figure (${e.filename}) of results job ${Ze.jobId} in the Results Explorer.`}]}).catch(()=>{})}async function jf(e){if(rt=e,ZI.textContent=e.label,UI.textContent=e.filename,OI.textContent=e.caption??"",bt.value=e.key,LI(e),e.url)$e.src=e.url;else if(Ie&&Ze){const t=Ze.defaultFigureKey;if(e.key===t&&An)$e.src=An;else{$e.removeAttribute("src");const n=await CI(Ze.jobId,e.filename);n&&rt?.key===e.key&&($e.src=n)}}else $e.removeAttribute("src")}function MI(e){return e.standaloneUrl??window.location.href}function FI(e){const t=MI(e);e.standaloneUrl?(ci.href=e.standaloneUrl,ci.style.display="inline-flex"):Ef&&(ci.href=window.location.href,ci.style.display="inline-flex"),Ge.style.display="inline-flex",Ge.dataset.url=t;const n=e.relatedUrls?.trainingMonitor;n&&(Xo.href=n,Xo.style.display="inline-flex")}function Yo(e){Ze=e,Tf.textContent=e.title,Nf.textContent=e.subtitle??`Job ${e.jobId}`,PI(e.metrics),EI(e.highlights),FI(e);const t=e.trainingMonitorUrl??e.relatedUrls?.trainingMonitor??`/viz/training-monitor?mode=standalone&job_id=${encodeURIComponent(e.jobId)}`;Zn.href=t,Zn.style.display="inline-flex",Zn.title="View live or completed training curves for this job";const n=e.availableFigures.find(r=>r.key===e.defaultFigureKey)??e.availableFigures[0]??null;rt=n,AI(e.availableFigures),n&&jf(n),mi.style.display="none",SI.style.display="block"}TI.addEventListener("click",()=>{const e=$e.src;if(!e||!rt)return;const t=document.createElement("a");t.href=e,t.download=rt.filename,t.click()});Ge.addEventListener("click",async()=>{const e=Ge.dataset.url||Ze?.standaloneUrl||window.location.href;try{await navigator.clipboard.writeText(e),Ge.textContent="Copied!",setTimeout(()=>{Ge.textContent="Copy explorer URL"},1500)}catch{window.prompt("Copy explorer URL:",e)}});$e.style.cursor="zoom-in";$e.addEventListener("click",()=>{if(!Ie||!$e.src)return;const e=Ie.getHostContext?.(),t=e?.availableDisplayModes;if(!Array.isArray(t)||!t.includes("fullscreen"))return;const n=e?.displayMode==="fullscreen"?"inline":"fullscreen";Ie.requestDisplayMode({mode:n}).then(r=>{$e.style.cursor=r.mode==="fullscreen"?"zoom-out":"zoom-in"}).catch(()=>{})});const Pf=new URLSearchParams(window.location.search),Ef=Pf.get("mode")==="standalone",di=Pf.get("job_id");if(Ef&&di){const e="The local viz port is assigned per MCP session and changes when the proxy restarts. Re-run results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port.";(async()=>{for(let n=1;n<=3;n++)try{const r=await fetch(`/api/results/${di}`);if(r.status===404){mi.innerHTML=`Results for job ${di} were not found (HTTP 404). The job may still be finalizing or was deleted, or this link points at a stale session. ${e}<br><button type="button" id="viz-retry-load" style="margin-top:0.75rem">Retry</button>`,document.getElementById("viz-retry-load")?.addEventListener("click",()=>location.reload());return}if(!r.ok)throw new Error(`HTTP ${r.status}`);const i=await r.json();Yo(jI(i,di));return}catch(r){if(n<3){await new Promise(i=>setTimeout(i,1e3*n));continue}mi.innerHTML=`Could not load results from the local viz server (${r instanceof Error?r.message:"error"}). ${e}<br><button type="button" id="viz-retry-load" style="margin-top:0.75rem">Retry</button>`,document.getElementById("viz-retry-load")?.addEventListener("click",()=>location.reload())}})()}else{const e=new wI({name:"Results Explorer",version:"1.0.0"},{},{autoResize:!0});Ie=e,e.ontoolinput=t=>{const n=t?.arguments??{},r=n.job_id!=null?String(n.job_id):"";r&&(Tf.textContent="Results Explorer",Nf.textContent=`Loading job ${r}...`)},e.ontoolresult=t=>{const n=t.content?.find(i=>i.type==="image");n&&(An=`data:${n.mimeType};base64,${n.data}`);const r=t.structuredContent;if(!r||typeof r!="object"){mi.textContent="The results explorer did not receive structured data.";return}Yo(r)},e.connect()}</script>
309
309
  </head>
310
310
  <body>
311
311
  <div id="loading">Loading results explorer...</div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barivia/barsom-mcp",
3
- "version": "0.23.9",
3
+ "version": "0.23.11",
4
4
  "description": "barSOM MCP proxy — connect any MCP client to the barSOM cloud API for Self-Organizing Map analytics",
5
5
  "keywords": [
6
6
  "mcp",