@barivia/barsom-mcp 0.11.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
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`, `jobs` (`train_map`, `train_siom_map`, `train_floop_siom` where entitled; `train_floop_chain` is deprecated), `results`, and `inference`. **Optional MCP App tools** (`training_prep`, `training_monitor`, `results_explorer`) add embedded or localhost viz; they are **not required** to complete upload → train → poll → results.
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, `baseline_study`), `jobs` (status/list/compare/cancel/delete), `results`, and `inference`. **Optional MCP App tools** (`training_prep`, `training_monitor`, `results_explorer`) add embedded or localhost viz; they are **not required** to complete upload → train → poll → results.
4
4
 
5
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; **`training_prep`** tool = interactive UI plus **`submit_prepared_training`**.
6
6
 
@@ -75,7 +75,7 @@ All multi-action tools follow the `datasets` / `jobs` / `results` / `inference`
75
75
  | Inference | `inference` |
76
76
  | Account | `account` |
77
77
  | Optional UI | `training_prep`, `training_monitor`, `results_explorer` |
78
- | Advanced / legacy | `training_guidance`, `explore_map` (deprecated alias) |
78
+ | Advanced | `training_guidance` |
79
79
 
80
80
  Agents should not call **`_fetch_figure`** from chat; it exists for the Results Explorer MCP App host. Use `results(action=get)` or `results_explorer` instead.
81
81
 
@@ -95,22 +95,29 @@ Call at the **start of mapping work** (or when the user asks what the MCP can do
95
95
  | `reduce_spectral` | Pre-training reducer for long ordered numeric blocks (spectra, time series, sensor fingerprints, gene panels). Methods: **pca** (top-k principal components), **log_sample** (k columns at log-spaced indices — scattering, audio bands), **uniform_sample** (k columns at evenly-spaced indices — regularly-sampled time series), **stats** (6 fixed per-row summary columns). All produce one feature vector per row; appends derived columns to the dataset. |
96
96
  | `delete` | Removing a dataset |
97
97
 
98
- ### `jobs(action)`
98
+ ### `train(action)`
99
+
100
+ All training is submitted via the `train` tool; every action is async and returns a `job_id` (poll with `jobs(action=status, job_id=...)`).
101
+
102
+ | Action | Use when |
103
+ |--------|----------|
104
+ | `map` | New map training job — full control: model type, grid, epochs, cyclic/temporal features, transforms. Complete-case data only (no NaNs in training columns). |
105
+ | `impute` | Sparse training data: accelerated missSOM / SIOM missSOM — trains a map and imputes missing cells in one job. Defaults: `model=auto`, `cv_folds=5`. Returns map artifacts + `imputed.csv`, `imputation_mask.csv`, optional `quality.csv` / `imputation_uncertainty.csv`. `quality.csv` reports two labeled numbers per column: `kfold_holdout_pool` (honest held-out) and `resubstitution_optimistic` (in-sample, optimistic — not validation). Plain numeric columns only. Valid parent for `inference(impute_column)`. |
106
+ | `siom_map` | Self-interacting map — same grid-map workflow plus SIOM controls such as `gamma`, `siom_decay`, and penalty selection. |
107
+ | `floop_siom` | FLooP-SIOM — growing node-budget manifold; default `topology=free` (CHL); optional `topology=chain` for strict 1D linked list. Requires Premium/Enterprise (`all_algorithms`). |
108
+ | `baseline_study` | Auto-configured baseline SOM for a dataset (heuristic grid + MAD normalization). |
109
+
110
+ ### `jobs(action)` — lifecycle
99
111
 
100
112
  | Action | Use when |
101
113
  |--------|----------|
102
- | `train_map` | Submitting a new map training job — full control: model type, grid, epochs, cyclic/temporal features, transforms. Returns `job_id`; poll with `jobs(action=status, job_id=...)`. Complete-case data only (no NaNs in training columns). |
103
- | `train_impute` | Sparse training data: accelerated missSOM / SIOM missSOM — trains a map and imputes missing cells in one job. Defaults: `model=auto`, `cv_folds=5`. Returns map artifacts + `imputed.csv`, `imputation_mask.csv`, optional `quality.csv` / `imputation_uncertainty.csv`. `quality.csv` reports two labeled numbers per column: `kfold_holdout_pool` (honest held-out) and `resubstitution_optimistic` (in-sample, optimistic — not validation). Plain numeric columns only. Valid parent for `inference(impute_column)`. |
104
- | `train_siom_map` | Submitting a self-interacting map training job — same grid-map workflow plus SIOM controls such as `gamma`, `siom_decay`, and penalty selection. |
105
- | `train_floop_siom` | Submitting a FLooP-SIOM job — growing node-budget manifold; default `topology=free` (CHL); optional `topology=chain` for strict 1D linked list. |
106
- | `train_floop_chain` | Deprecated alias for `train_floop_siom` — same behavior. |
107
114
  | `status` | Polling after any async job — every 10–15s; map jobs expose `progress_phase` (ordering, convergence, cv, artifacts) |
108
115
  | `list` | Finding job IDs, checking pipeline state |
109
116
  | `compare` | Picking the best run from a set (QE, TE, silhouette table) |
110
117
  | `cancel` | Stopping a running job |
111
118
  | `delete` | Permanently removing a job + its S3 files |
112
- | `batch_predict` | Submit multiple predict jobs from one parent training job |
113
- | `run_baseline_study` | Auto-configured baseline SOM for a dataset |
119
+
120
+ `jobs(action=train_map | train_siom_map | train_impute | train_floop_siom | run_baseline_study | batch_predict)` still work as **deprecated** forwarders to `train(...)` / `inference(batch_predict)` for one release (each prints a deprecation note). Migrate to `train` / `inference`.
114
121
 
115
122
  ### `results(action)`
116
123
 
@@ -130,7 +137,8 @@ All actions use a frozen trained map — no retraining. Derived columns use **`d
130
137
  | Action | Output | Timing |
131
138
  |--------|--------|--------|
132
139
  | `predict` | Score rows against the trained map. **Inputs:** `dataset_id` (defaults to the parent training dataset) **or** inline `rows` (≤500). **Output style** (`output` param): `"compact"` → `predictions.csv` (row_id, bmu_x/y, bmu_node_index, cluster_id [+ QE / qe_p95 / potential_anomaly when scoring **new** data]); `"annotated"` → `annotated.csv` (original CSV + BMU columns appended). **Regime auto-detected:** when the resolved dataset matches the training dataset, QE columns are intentionally omitted in compact output (training-set fit ≠ generalisation; the p95 anomaly flag would be circular). Prefer `dataset_id` for batches and SIOM/irregular maps. | 5–120s |
133
- | `impute_column` | Fill a numeric **target_column** not used in training: **requires** `dataset_id` + `target_column`. Parent job: completed **train_map** or **train_impute**. Dataset must contain all training features plus the target. Pools observed target values from rows mapped to this row's BMU and topology neighbors (BMU + neighbors, often 7 nodes on hex interior; fewer on borders unless the map is periodic). `only_missing` (default true); `impute_aggregation`: mean or median. **Not** held-out validated — map-local estimate. Output **`imputed.csv`**. For holes across many training columns, prefer **jobs(train_impute)** first. | 5–120s |
140
+ | `batch_predict` | Fan out many predict jobs at once against one parent training `job_id`: pass `inputs` (array of `{ dataset_id? , rows? }`). Returns the list of submitted job_ids (does not auto-poll poll each with `jobs(action=status)`). | submits N jobs |
141
+ | `impute_column` | Fill a numeric **target_column** not used in training: **requires** `dataset_id` + `target_column`. Parent job: completed **train(action=map)** or **train(action=impute)**. Dataset must contain all training features plus the target. Pools observed target values from rows mapped to this row's BMU and topology neighbors (BMU + neighbors, often 7 nodes on hex interior; fewer on borders unless the map is periodic). `only_missing` (default true); `impute_aggregation`: mean or median. **Not** held-out validated — map-local estimate. Output **`imputed.csv`**. For holes across many training columns, prefer **train(action=impute)** first. | 5–120s |
134
142
  | `compare` | density-diff heatmap + top gained/lost nodes — drift, A/B, cohort | 30–120s |
135
143
  | `project_columns` | Project one or more dataset columns onto the trained map (component planes) | async |
136
144
  | `report` | Report **manifest** (figure names, download URLs, metrics, cluster summary) — sync; use with `results(download)` on the training `job_id` for `report.pdf` when present; build custom PDFs in Quarto/Jupyter | immediate |
@@ -154,7 +162,6 @@ All actions use a frozen trained map — no retraining. Derived columns use **`d
154
162
  | `submit_prepared_training` | Submit the reviewed prep (`review_token` + `explicit_confirm=true`) |
155
163
  | `training_monitor` | Visual progress for a `job_id`; optional — `jobs(action=status)` is enough |
156
164
  | `results_explorer` | Browse metrics and figures after training completes |
157
- | `explore_map` | **Deprecated** — alias of `results_explorer`; migrate configs to `results_explorer` |
158
165
  | `_fetch_figure` | **Host / App only** — Results Explorer invokes this for one raster figure; not for agent chat |
159
166
 
160
167
  When the client does not advertise MCP Apps support, the proxy starts **`viz-server`** on **`127.0.0.1`** (localhost-only). Tool responses can include `http://127.0.0.1:PORT/viz/...` links. See **Local viz fallback** under Environment Variables.
@@ -171,7 +178,9 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
171
178
 
172
179
  ### Migration notes
173
180
 
174
- - **`explore_map` `results_explorer`:** Update Cursor, Claude Desktop, or other MCP configs that still reference `explore_map`. The alias remains for backward compatibility.
181
+ - **Training moved to the `train` tool (0.14.0):** submit training via `train(action=map | siom_map | impute | floop_siom | baseline_study)`; `jobs` now covers lifecycle only (`status`/`list`/`compare`/`cancel`/`delete`). `batch_predict` moved to `inference(action=batch_predict)`. The old `jobs(action=train_map | train_siom_map | train_impute | train_floop_siom | run_baseline_study | batch_predict)` actions still work as **deprecated forwarders** for one release (each prints a deprecation note); they will be removed later. Update Cursor / Claude Desktop configs and prompts to the new tools.
182
+ - **`explore_map` removed (0.13.0):** the deprecated alias of `results_explorer` is gone. Update any Cursor / Claude Desktop / MCP config or call site to use **`results_explorer`**.
183
+ - **`train_floop_chain` removed (0.13.0):** the deprecated alias of `jobs(action=train_floop_siom)` is gone. Use **`train(action=floop_siom)`**.
175
184
  - **Shorter `info` prompt:** Clients that relied on the old long `info` text should use **`guide_barsom_workflow`** or server **instructions** for the full story.
176
185
 
177
186
  ### `send_feedback`
@@ -188,7 +197,7 @@ When adding or refining tools, follow [MCP best practices](https://modelcontextp
188
197
 
189
198
  ## Data preparation
190
199
 
191
- To train on a subset of your data (e.g. first 2000 rows, or rows where region=Europe) without re-uploading: use **datasets(action=subset)** with `row_range` and/or `filter` to create a new dataset, then train with **jobs(action=train_map, dataset_id=...)**, **jobs(action=train_siom_map, dataset_id=...)**, or **jobs(action=train_floop_siom, dataset_id=...)** on the new dataset_id; or pass **row_range** in the training job params for a one-off slice.
200
+ To train on a subset of your data (e.g. first 2000 rows, or rows where region=Europe) without re-uploading: use **datasets(action=subset)** with `row_range` and/or `filter` to create a new dataset, then train with **train(action=map, dataset_id=...)**, **train(action=siom_map, dataset_id=...)**, or **train(action=floop_siom, dataset_id=...)** on the new dataset_id; or pass **row_range** in the training job params for a one-off slice.
192
201
 
193
202
  ## How It Works
194
203
 
@@ -219,7 +228,7 @@ npm run build # Compile to dist/
219
228
  npm test # Vitest
220
229
  ```
221
230
 
222
- **Post-change spot-check (real MCP client):** confirm `guide_barsom_workflow` appears near the top of the tool list, `explore_map` still works as an alias of `results_explorer`, and Results Explorer can still load figures (it uses `_fetch_figure` internally — agents should rely on `results` / `results_explorer`).
231
+ **Post-change spot-check (real MCP client):** confirm `guide_barsom_workflow` appears near the top of the tool list, the removed `explore_map` / `train_floop_chain` aliases are no longer listed, and Results Explorer can still load figures (it uses `_fetch_figure` internally — agents should rely on `results` / `results_explorer`).
223
232
 
224
233
  For local development against a local API stack:
225
234
 
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 r,registerAppResource as n,RESOURCE_MIME_TYPE as s}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as m,setClientSupportsMcpApps as d,CLIENT_VERSION as u}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerJobsTool as g,JOBS_DESCRIPTION_BASE as _}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as h,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as w}from"./tools/account.js";import{registerInferenceTool as j}from"./tools/inference.js";import{registerGuideBarsomTool as v}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as P}from"./tools/training_guidance.js";import{registerFeedbackTool as x}from"./tools/feedback.js";import{registerTrainingPrepTools as I,TRAINING_PREP_URI as k}from"./tools/training_prep.js";import{registerTrainingMonitorTool as T,TRAINING_MONITOR_URI as M}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText as O}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 A=new e({name:"analytics-engine",version:u,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 `jobs(train_map)`, `jobs(train_siom_map)`, or `jobs(train_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 `jobs(compare)`, `results(download/recolor/transition_flow)`, or `inference` 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 map (compact)\n\n| Area | Tool | Notes |\n|------|------|--------|\n| Data | `datasets` | upload, preview, analyze, list, subset, add_expression, reduce_spectral (pca/log_sample/uniform_sample/stats for long ordered numeric blocks), delete |\n| Jobs | `jobs` | train_map, train_impute (sparse data: map + dense imputed.csv), train_siom_map, train_floop_siom (entitled), status, list, compare, cancel, delete, batch_predict, run_baseline_study; `train_floop_chain` = deprecated alias for train_floop_siom |\n| Results | `results` | get (figures="none" for metrics-only), export, download, recolor (async), transition_flow (async; time-ordered rows only) |\n| Inference | `inference` | predict (regime-aware; output="compact"|"annotated"), impute_column (neighbor-pool fill for a non-training column), compare, project_columns, report |\n| Account | `account` | status, burst/compute actions, history, add_funds |\n| Bootstrap | `guide_barsom_workflow` | orientation + SOP |\n| Parameters | `training_guidance` | presets and field hints (API-scoped) |\n| Prep | `prepare_training` prompt, `training_prep` + `submit_prepared_training` | checklist / interactive UI |\n| Explore | `results_explorer`, `training_monitor` | optional MCP Apps; `explore_map` = deprecated alias of `results_explorer`; `jobs(status)` and `results(get)` suffice without them |\n| Other | `send_feedback` | only after user agrees |\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, `results(recolor)`, `results(transition_flow)` 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## Constraints\n\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints; `training_prep` = UI + guarded submit. 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 `recolor`, `transition_flow`, or `project_columns`, use the **new** `job_id` returned for follow-up `results` if applicable.'});n(A,y,y,{mimeType:s},async()=>{const e=await c("results-explorer");return{contents:[{uri:y,mimeType:s,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),n(A,k,k,{mimeType:s},async()=>{const e=await c("training-prep");return{contents:[{uri:k,mimeType:s,text:e??"<html><body>Training Preparation view not built yet.</body></html>"}]}}),n(A,M,M,{mimeType:s},async()=>{const e=await c("training-monitor");return{contents:[{uri:M,mimeType:s,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),v(A),h(A),I(A),T(A),f(A),g(A,_),b(A),w(A),j(A),P(A),x(A),A.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).","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `jobs` (train/poll/compare/…; train_map accepts an optional `label` for readable compare rows), `results` (get/download/export/recolor/transition_flow; figures="none" for metrics-only), `inference` (predict; impute_column for topology-neighbor pool fill; compare; project_columns; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints) · `training_prep` + `submit_prepared_training` (interactive UI).","","**Optional UI:** `results_explorer`, `training_monitor` — nice for browsing; not required if you use `results` + `jobs(status)`.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `transition_flow` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. 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")}}]})),A.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; training_prep tool = interactive UI + submit_prepared_training.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>({messages:[{role:"user",content:{type:"text",text:await O(e)}}]}));const S=new t;(async function(){try{const e=await a(l,p,c);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=A.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);d(!!o?.mimeTypes?.includes(s))},await A.connect(S)})().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 n,RESOURCE_MIME_TYPE as s}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as a}from"./viz-server.js";import{API_KEY as i,apiCall as l,apiRawCall as p,loadViewHtml as c,setVizPort as m,setClientSupportsMcpApps as d,CLIENT_VERSION as u}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as g}from"./tools/train.js";import{registerJobsTool as _,JOBS_DESCRIPTION_BASE as b}from"./tools/jobs.js";import{registerResultsTool as y}from"./tools/results.js";import{registerExploreMapTool as h,RESULTS_EXPLORER_URI as w}from"./tools/explore_map.js";import{registerAccountTool as j}from"./tools/account.js";import{registerInferenceTool as v}from"./tools/inference.js";import{registerGuideBarsomTool as P}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as x}from"./tools/training_guidance.js";import{registerFeedbackTool as I}from"./tools/feedback.js";import{registerTrainingPrepTools as k,TRAINING_PREP_URI as T}from"./tools/training_prep.js";import{registerTrainingMonitorTool as M,TRAINING_MONITOR_URI as O}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText as A}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 S=new e({name:"analytics-engine",version:u,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 `jobs(compare)`, `results(download/recolor/transition_flow)`, or `inference` 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 map (compact)\n\n| Area | Tool | Notes |\n|------|------|--------|\n| Data | `datasets` | upload, preview, analyze, list, subset, add_expression, reduce_spectral (pca/log_sample/uniform_sample/stats for long ordered numeric blocks), delete |\n| Train | `train` | map, siom_map, impute (sparse data: map + dense imputed.csv), floop_siom (entitled), baseline_study — all async, returns job_id |\n| Jobs | `jobs` | status, list, compare, cancel, delete (lifecycle). jobs(train_*/run_baseline_study/batch_predict) still work as deprecated forwarders use train / inference |\n| Results | `results` | get (figures="none" for metrics-only), export, download, recolor (async), transition_flow (async; time-ordered rows only) |\n| Inference | `inference` | predict (regime-aware; output="compact"|"annotated"), batch_predict (fan out many predict jobs), impute_column (neighbor-pool fill for a non-training column), compare, project_columns, report |\n| Account | `account` | status, burst/compute actions, history, add_funds |\n| Bootstrap | `guide_barsom_workflow` | orientation + SOP |\n| Parameters | `training_guidance` | presets and field hints (API-scoped) |\n| Prep | `prepare_training` prompt, `training_prep` + `submit_prepared_training` | checklist / interactive UI |\n| Explore | `results_explorer`, `training_monitor` | optional MCP Apps; `jobs(status)` and `results(get)` suffice without them |\n| Other | `send_feedback` | only after user agrees |\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, `results(recolor)`, `results(transition_flow)` 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## Constraints\n\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints; `training_prep` = UI + guarded submit. 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 `recolor`, `transition_flow`, or `project_columns`, use the **new** `job_id` returned for follow-up `results` if applicable.'});n(S,w,w,{mimeType:s},async()=>{const e=await c("results-explorer");return{contents:[{uri:w,mimeType:s,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),n(S,T,T,{mimeType:s},async()=>{const e=await c("training-prep");return{contents:[{uri:T,mimeType:s,text:e??"<html><body>Training Preparation view not built yet.</body></html>"}]}}),n(S,O,O,{mimeType:s},async()=>{const e=await c("training-monitor");return{contents:[{uri:O,mimeType:s,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),P(S),h(S),k(S),M(S),f(S),g(S),_(S,b),y(S),j(S),v(S),x(S),I(S),S.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).","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom/baseline_study; pass `label` for readable compare rows), `jobs` (status/list/compare/cancel/delete), `results` (get/download/export/recolor/transition_flow; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints) · `training_prep` + `submit_prepared_training` (interactive UI).","","**Optional UI:** `results_explorer`, `training_monitor` — nice for browsing; not required if you use `results` + `jobs(status)`.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `transition_flow` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. 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")}}]})),S.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; training_prep tool = interactive UI + submit_prepared_training.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>({messages:[{role:"user",content:{type:"text",text:await A(e)}}]}));const C=new t;(async function(){try{const e=await a(l,p,c);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=S.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);d(!!o?.mimeTypes?.includes(s))},await S.connect(C)})().catch(console.error);
@@ -1,5 +1,5 @@
1
1
  import { apiCall } from "./shared.js";
2
- const FALLBACK_PREFIX = "Before upload: for jobs(action=train_map) ensure training columns have no NaNs; for jobs(action=train_impute) missing cells in training columns are OK (plain numeric only). 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. Then choose the training path: jobs(action=train_map, dataset_id=\"{id}\", ...) for complete-case data, jobs(action=train_impute, dataset_id=\"{id}\", ...) for sparse matrices, jobs(action=train_siom_map, dataset_id=\"{id}\", ...) for a fixed-grid SIOM, or jobs(action=train_floop_siom, dataset_id=\"{id}\", ...) for FLooP-SIOM (default topology=free / CHL; optional topology=chain).";
2
+ const FALLBACK_PREFIX = "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). 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. Then choose the training path: train(action=map, dataset_id=\"{id}\", ...) for complete-case data, train(action=impute, dataset_id=\"{id}\", ...) for sparse matrices, train(action=siom_map, dataset_id=\"{id}\", ...) for a fixed-grid SIOM, or train(action=floop_siom, dataset_id=\"{id}\", ...) for FLooP-SIOM (default topology=free / CHL; optional topology=chain).";
3
3
  /** Used by the `prepare_training` MCP prompt; prefers tier-scoped text from the API when online. */
4
4
  export async function resolvePrepareTrainingPromptText(datasetId) {
5
5
  let promptText = FALLBACK_PREFIX.replaceAll("{id}", datasetId);
package/dist/shared.js CHANGED
@@ -24,7 +24,7 @@ export const RETRYABLE_STATUS = new Set([502, 503, 504]);
24
24
  * X-Barsom-Client-Version so the server can annotate tool guidance with the
25
25
  * wrapper version each action requires. Keep in sync with package.json on bump.
26
26
  */
27
- export const CLIENT_VERSION = "0.11.1";
27
+ export const CLIENT_VERSION = "0.14.0";
28
28
  /** User-facing links; keep aligned with barivia.se / api.barivia.se. */
29
29
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
30
30
  /** Self-serve account dashboard (manage plan, billing, and API keys). */
@@ -16,7 +16,7 @@ export function registerAccountTool(server) {
16
16
  action=status: Returns plan tier, compute class (CPU/GPU), usage limits, live queue state, training time estimates, and credit balance.
17
17
  Use BEFORE large jobs to check GPU availability and estimate wait time.
18
18
  action=request_compute: Provisions an ephemeral cloud burst EC2 instance. Data is in shared R2 — no re-upload needed. Wait ~3 min after requesting before submitting jobs.
19
- NOT FOR: Training itself — use jobs(action=train_map). This tool only manages the account and compute lease.`, {
19
+ NOT FOR: Training itself — use train(action=map). This tool only manages the account and compute lease.`, {
20
20
  action: z
21
21
  .enum(["status", "request_compute", "compute_status", "release_compute", "history", "add_funds"])
22
22
  .describe("status: plan/license/queue info; request_compute: provision burst; compute_status: check active lease; release_compute: stop lease; history: recent compute usage; add_funds: instructions"),
@@ -45,7 +45,7 @@ function formatAnalyzeResult(data, dataset_id) {
45
45
  ``,
46
46
  ...(periodicityCaveat ? [`Note: ${periodicityCaveat}`, ``] : []),
47
47
  `Column recommendations:`,
48
- ` Train (use in jobs(action=train_map)): ${trainList.length ? trainList.join(", ") : "—"}`,
48
+ ` Train (use in train(action=map)): ${trainList.length ? trainList.join(", ") : "—"}`,
49
49
  ` Consider dropping (highly correlated with another): ${considerDropping.length ? considerDropping.join(", ") : "—"}`,
50
50
  ` Project later (after training, use inference(action=project_columns, job_id=<training_job_id>, dataset_id=<dataset_id>, columns=[...]) onto map): ${projectLater.length ? projectLater.join(", ") : "—"}`,
51
51
  ` Low variance (near-constant): ${lowVariance.length ? lowVariance.join(", ") : "—"}`,
@@ -88,7 +88,7 @@ function formatAnalyzeResult(data, dataset_id) {
88
88
  nextSteps.push(`Columns show periodic behavior at lags ${lags.join(", ")} (assuming row order is meaningful). Consider cyclic_features or temporal_features with matching periods.`);
89
89
  nextSteps.push(`For datetime columns: run datasets(action=preview) for temporal_suggestions; match lags (e.g. 365→day_of_year, 12→month, 7→day_of_week, 24→hour_of_day) to choose which temporal components to extract.`);
90
90
  }
91
- nextSteps.push(`Then call jobs(action=train_map, dataset_id=${dataset_id}, columns=[...], ...).`);
91
+ nextSteps.push(`Then call train(action=map, dataset_id=${dataset_id}, columns=[...], ...).`);
92
92
  lines.push(`Next steps:`, ...nextSteps.map((s) => ` ${s}`));
93
93
  return lines.join("\n");
94
94
  }
@@ -98,20 +98,20 @@ export function registerDatasetsTool(server) {
98
98
  | Action | Use when |
99
99
  |--------|----------|
100
100
  | upload | You have a CSV file to add — do this first |
101
- | preview | Before jobs(action=train_map) — always preview an unfamiliar dataset to spot cyclics, nulls, column types |
101
+ | preview | Before train(action=map) — always preview an unfamiliar dataset to spot cyclics, nulls, column types |
102
102
  | analyze | Pre-training column analysis: correlation matrix, periodicity ranking, column recommendations (train / drop / project later). Run after preview, before train_map. |
103
103
  | list | Finding dataset IDs for train_map, preview, or subset — see all available datasets |
104
104
  | subset | Creating a filtered/sliced view without re-uploading the full CSV |
105
- | add_expression | Add a computed column from an expression (same as project(expression) without project_onto_job) dataset_id + name + expression |
105
+ | add_expression | Add a computed column from an expression (dataset_id + name + expression). With project_onto_job=<training_job_id>, instead projects the computed expression onto that map as a component plane in one step (no column added). |
106
106
  | reduce_spectral | Collapse a long ordered numeric block (e.g. 1000-point spectrum, time series, sensor fingerprint) into a small per-row feature set so the SOM can train on signal-dense features. Methods: pca, log_sample, uniform_sample, stats. |
107
107
  | delete | Cleaning up after experiments or freeing the dataset slot |
108
108
 
109
- action=upload: PREFER file_path — server reads from workspace root (token-efficient; no file content in context). Use csv_data only for small inline pastes (e.g. <10KB). Returns dataset ID. Then use datasets(action=preview) before jobs(action=train_map).
109
+ action=upload: PREFER file_path — server reads from workspace root (token-efficient; no file content in context). Use csv_data only for small inline pastes (e.g. <10KB). Returns dataset ID. Then use datasets(action=preview) before train(action=map).
110
110
 
111
- Step 0 (before upload): (1) CSV with header; one row per observation. (2) Columns you will use for training must be numeric (or datetime if you will use temporal extraction). (3) Complete-case path (jobs action=train_map): no NaNs in training columns. Sparse path (jobs action=train_impute): missing cells in training columns are OK — the job trains missSOM and returns dense imputed.csv; plain numeric columns only. (4) Categoricals: encode (e.g. one-hot, label) or exclude — do not use raw categorical columns as training features; preview shows which columns are non-numeric. (5) Optional: if you plan to use transition_flow, sort rows chronologically before upload.
112
- Step 1 (after upload): Upload, then always datasets(action=preview) to verify column types and spot cyclics/datetime. Add derived columns with datasets(action=add_expression, dataset_id=..., name=..., expression=...); choose train_map (complete-case) or train_impute (sparse matrix) before submit.
111
+ Step 0 (before upload): (1) CSV with header; one row per observation. (2) Columns you will use for training must be numeric (or datetime if you will use temporal extraction). (3) Complete-case path (train action=map): no NaNs in training columns. Sparse path (train action=impute): missing cells in training columns are OK — the job trains missSOM and returns dense imputed.csv; plain numeric columns only. (4) Categoricals: encode (e.g. one-hot, label) or exclude — do not use raw categorical columns as training features; preview shows which columns are non-numeric. (5) Optional: if you plan to use transition_flow, sort rows chronologically before upload.
112
+ Step 1 (after upload): always datasets(action=preview) to verify column types and spot cyclics/datetime, then choose train_map (complete-case) or train_impute (sparse) before submit.
113
113
 
114
- action=preview: Show columns, stats, sample rows, cyclic/datetime detections. ALWAYS preview before jobs(action=train_map) on an unfamiliar dataset.
114
+ action=preview: Show columns, stats, sample rows, cyclic/datetime detections. ALWAYS preview before train(action=map) on an unfamiliar dataset.
115
115
  action=analyze: Pre-training analysis on numeric columns — Pearson correlation, autocorrelation periodicity scores, and column recommendations (train, consider_dropping, project_later, low_variance). Use to choose which columns to include in training and which to project onto the map after training.
116
116
  action=list: List all datasets belonging to the organisation with id, name, rows, cols, created_at (use created_at or id to distinguish datasets with the same name).
117
117
  action=subset: Create a new dataset from a subset of an existing one. Requires name and at least one of row_range, filters, or sample_n.
@@ -132,10 +132,10 @@ action=delete: Remove a dataset and all S3 data permanently.
132
132
 
133
133
  BEST FOR: Tabular numeric data. CSV with header required.
134
134
  NOT FOR: Real-time data streams or binary files — upload a snapshot CSV instead.
135
- ESCALATION: If upload fails with column errors, open the file locally and verify the header row. If preview shows nulls in training columns: use jobs(action=train_impute) for sparse data, or clean/subset for jobs(action=train_map).`, {
135
+ ESCALATION: If upload fails with column errors, open the file locally and verify the header row. If preview shows nulls in training columns: use train(action=impute) for sparse data, or clean/subset for train(action=map).`, {
136
136
  action: z
137
137
  .enum(["upload", "preview", "analyze", "list", "subset", "delete", "add_expression", "reduce_spectral"])
138
- .describe("upload: add CSV; preview: inspect columns/stats; analyze: pre-training correlation and periodicity; list: see all datasets; subset: create filtered subset; delete: remove dataset; add_expression: add derived column from expression; reduce_spectral: collapse a long ordered numeric block (e.g. spectrum, time series) into a small per-row feature set via PCA / log_sample / uniform_sample / stats"),
138
+ .describe("upload: add CSV; preview: inspect columns/stats; analyze: pre-training correlation and periodicity; list: see all datasets; subset: create filtered subset; delete: remove dataset; add_expression: add derived column from expression (or, with project_onto_job, project the expression onto a trained map as a component plane); reduce_spectral: collapse a long ordered numeric block (e.g. spectrum, time series) into a small per-row feature set via PCA / log_sample / uniform_sample / stats"),
139
139
  name: z.string().optional().describe("Dataset name (required for action=upload and subset)"),
140
140
  file_path: z.string().optional().describe("Path to local CSV (PREFERRED): absolute path, file:// URI, or relative to workspace root. Token-efficient; server reads file."),
141
141
  csv_data: z.string().optional().describe("Inline CSV string for small pastes only (<10KB). Avoid for large files — use file_path instead to avoid token explosion."),
@@ -179,6 +179,18 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
179
179
  })
180
180
  .optional()
181
181
  .describe("action=add_expression: evaluation options (missing, window for rolling)"),
182
+ project_onto_job: z
183
+ .string()
184
+ .optional()
185
+ .describe("action=add_expression: a COMPLETED training job_id. When set, compute the expression and project it onto that map as a component plane in one step (instead of adding a column to the dataset). Columns in the expression are read from dataset_id when given, else the training dataset. Returns a projection job; get the figure via results(action=download)."),
186
+ aggregation: z
187
+ .enum(["mean", "median", "sum", "min", "max", "std", "count"])
188
+ .optional()
189
+ .describe("action=add_expression with project_onto_job: per-node aggregation of the derived value (default mean)."),
190
+ output_format: z
191
+ .enum(["png", "pdf", "svg"])
192
+ .optional()
193
+ .describe("action=add_expression with project_onto_job: component-plane image format (default pdf)."),
182
194
  method: z
183
195
  .enum(["pca", "log_sample", "uniform_sample", "stats"])
184
196
  .optional()
@@ -204,7 +216,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
204
216
  .int()
205
217
  .optional()
206
218
  .describe("action=subset: RNG seed for sample_n (default 42; same seed + dataset reproduces the same sample)."),
207
- }, async ({ action, name, file_path, csv_data, dataset_id, n_rows, row_range, filters, filter, expression, options, method, columns_block, k, sample_n, sample_seed }) => {
219
+ }, async ({ action, name, file_path, csv_data, dataset_id, n_rows, row_range, filters, filter, expression, options, project_onto_job, aggregation, output_format, method, columns_block, k, sample_n, sample_seed }) => {
208
220
  if (action === "upload") {
209
221
  if (!name)
210
222
  throw new Error("datasets(upload) requires name");
@@ -355,8 +367,8 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
355
367
  const temporalCols = temporalSugg.flatMap((ts) => ts.columns ?? []);
356
368
  const allCols = [...new Set([...cyclicCols, ...temporalCols])];
357
369
  lines.push(``, allCols.length > 0
358
- ? `Consider using cyclic_features or temporal_features for these columns when calling jobs(action=train_map): ${allCols.join(", ")}.`
359
- : `Consider using cyclic_features or temporal_features for the columns above when calling jobs(action=train_map).`);
370
+ ? `Consider using cyclic_features or temporal_features for these columns when calling train(action=map): ${allCols.join(", ")}.`
371
+ : `Consider using cyclic_features or temporal_features for the columns above when calling train(action=map).`);
360
372
  }
361
373
  if (samples.length > 0) {
362
374
  lines.push(``, `Sample Rows (first ${samples.length}):`);
@@ -366,7 +378,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
366
378
  lines.push(`| ${cols.map((c) => String(row[c] ?? "")).join(" | ")} |`);
367
379
  }
368
380
  }
369
- lines.push(``, `Suggested next step: jobs(action=train_map, dataset_id=${dataset_id}, ...) or use the prepare_training prompt; to add derived columns use datasets(action=add_expression, dataset_id=${dataset_id}, name=..., expression=...).`);
381
+ lines.push(``, `Suggested next step: train(action=map, dataset_id=${dataset_id}, ...) or use the prepare_training prompt; to add derived columns use datasets(action=add_expression, dataset_id=${dataset_id}, name=..., expression=...).`);
370
382
  return { content: [{ type: "text", text: lines.join("\n") }] };
371
383
  }
372
384
  if (action === "analyze") {
@@ -402,12 +414,47 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
402
414
  return { content: [{ type: "text", text: formatAnalyzeResult(data, dataset_id) }] };
403
415
  }
404
416
  if (action === "add_expression") {
405
- if (!dataset_id)
406
- throw new Error("datasets(add_expression) requires dataset_id");
407
417
  if (!name)
408
418
  throw new Error("datasets(add_expression) requires name");
409
419
  if (!expression)
410
420
  throw new Error("datasets(add_expression) requires expression");
421
+ // One-step formula -> component plane: compute the expression and project
422
+ // it onto a trained map without first materializing a dataset column.
423
+ if (project_onto_job) {
424
+ const body = { name, expression };
425
+ if (aggregation)
426
+ body.aggregation = aggregation;
427
+ if (output_format)
428
+ body.output_format = output_format;
429
+ if (dataset_id)
430
+ body.dataset_id = dataset_id; // optional: read expression columns from a different dataset
431
+ if (options)
432
+ body.options = options;
433
+ const data = (await apiCall("POST", `/v1/results/${project_onto_job}/derive`, body));
434
+ const projJobId = data.id;
435
+ const poll = await pollUntilComplete(projJobId, POLL_DERIVE_MAX_MS);
436
+ if (poll.status === "completed") {
437
+ return {
438
+ content: [
439
+ {
440
+ type: "text",
441
+ text: [
442
+ `Projected derived variable "${name}" onto map (training job ${project_onto_job}).`,
443
+ `Expression: ${expression} | Aggregation: ${aggregation ?? "mean"}`,
444
+ `Component plane ready. Get the figure with results(action=download, job_id="${projJobId}").`,
445
+ ].join("\n"),
446
+ },
447
+ ],
448
+ };
449
+ }
450
+ if (poll.status === "failed")
451
+ return { content: [{ type: "text", text: `datasets(add_expression, project_onto_job) job ${projJobId} failed: ${poll.error ?? "unknown error"}` }] };
452
+ return {
453
+ content: [{ type: "text", text: `datasets(add_expression, project_onto_job) job ${projJobId} submitted. Poll with jobs(action=status, job_id="${projJobId}").` }],
454
+ };
455
+ }
456
+ if (!dataset_id)
457
+ throw new Error("datasets(add_expression) requires dataset_id (or project_onto_job to project onto a map)");
411
458
  const body = { name, expression };
412
459
  if (options)
413
460
  body.options = options;
@@ -426,7 +473,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
426
473
  `Expression: ${expression} | Rows: ${summary.n_rows ?? "?"}`,
427
474
  summary.nan_count ? `NaN values: ${summary.nan_count}` : "",
428
475
  `Min: ${summary.min ?? "?"} | Max: ${summary.max ?? "?"} | Mean: ${summary.mean ?? "?"}`,
429
- `Column now available. Use datasets(action=preview) to verify, or include in jobs(action=train_map) via the 'columns' parameter.`,
476
+ `Column now available. Use datasets(action=preview) to verify, or include in train(action=map) via the 'columns' parameter.`,
430
477
  ]
431
478
  .filter(Boolean)
432
479
  .join("\n"),
@@ -484,7 +531,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
484
531
  lines.push(`Selected source-column indices (1-based): [${idxs.join(", ")}]`);
485
532
  }
486
533
  }
487
- lines.push(`Use datasets(action=preview, dataset_id=${dataset_id}) to inspect the new columns, then jobs(action=train_map, dataset_id=${dataset_id}, columns=[..., "${outCols[0] ?? ""}", …]) to train on the reduced features.`);
534
+ lines.push(`Use datasets(action=preview, dataset_id=${dataset_id}) to inspect the new columns, then train(action=map, dataset_id=${dataset_id}, columns=[..., "${outCols[0] ?? ""}", …]) to train on the reduced features.`);
488
535
  return { content: [{ type: "text", text: lines.filter(Boolean).join("\n") }] };
489
536
  }
490
537
  if (poll.status === "failed") {
@@ -3,7 +3,6 @@ import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
3
3
  import { registerAuditedTool, runMcpToolAudit } from "../audit.js";
4
4
  import { apiCall, apiRawCall, getClientSupportsMcpApps, getVizPort, getCaptionForImage, getResultsImagesToFetch, mimeForFilename, structuredTextResult, tryAttachImage, } from "../shared.js";
5
5
  export const RESULTS_EXPLORER_URI = "ui://barsom/results-explorer";
6
- export const MAP_EXPLORER_URI = RESULTS_EXPLORER_URI;
7
6
  function labelForFigure(filename) {
8
7
  const base = filename.replace(/\.(png|pdf|svg)$/i, "");
9
8
  if (base === "combined")
@@ -166,12 +165,12 @@ export function registerExploreMapTool(server) {
166
165
  _meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
167
166
  };
168
167
  registerAppTool(server, "results_explorer", toolConfig, async (args) => runMcpToolAudit("results_explorer", "default", args, () => handleResultsExplorer(String(args.job_id))));
169
- registerAppTool(server, "explore_map", {
170
- ...toolConfig,
171
- title: "Results Explorer",
172
- description: `${toolConfig.description} Deprecated alias for results_explorer migrate configs to results_explorer.`,
173
- }, async (args) => runMcpToolAudit("explore_map", "default", args, () => handleResultsExplorer(String(args.job_id))));
174
- registerAuditedTool(server, "_fetch_figure", "Host / MCP App use only do NOT invoke from agent chat. Results Explorer calls this to load one raster figure as base64; agents should use results(action=get) or results_explorer instead.", {
168
+ // NOTE: this MUST stay a registered tool — the Results Explorer MCP App view
169
+ // calls it through the App bridge (mcpApp.callServerTool) to lazy-load non-default
170
+ // raster figures, and the bridge can only reach registered server tools. It cannot
171
+ // be hidden from tools/list without breaking figure-switching in the App. The guard
172
+ // below + SERVER_INSTRUCTIONS keep agents from calling it directly.
173
+ registerAuditedTool(server, "_fetch_figure", "[INTERNAL — host / MCP App only; agents MUST NOT call this.] The Results Explorer App view uses it to lazy-load one raster figure as base64. From chat, use results(action=get) or results_explorer instead; calling this directly just returns a single image with no context.", {
175
174
  job_id: z.string(),
176
175
  filename: z.string(),
177
176
  }, async ({ job_id, filename }) => {
@@ -30,18 +30,40 @@ export function formatPredictPreviewLines(summary) {
30
30
  }
31
31
  return lines;
32
32
  }
33
+ /**
34
+ * Fan out N predict jobs against one trained map. Shared by inference(batch_predict)
35
+ * and the deprecated jobs(batch_predict) wrapper. Posts to the same route and returns
36
+ * the list of submitted job_ids (does not auto-poll — poll each via jobs(status)).
37
+ */
38
+ export async function runBatchPredict(args) {
39
+ const { job_id, inputs } = args;
40
+ if (!job_id)
41
+ throw new Error("inference(batch_predict) requires job_id (parent training job)");
42
+ if (!inputs || !Array.isArray(inputs) || inputs.length === 0) {
43
+ throw new Error("inference(batch_predict) requires inputs (non-empty array of { dataset_id? or rows? })");
44
+ }
45
+ const data = (await apiCall("POST", `/v1/results/${job_id}/batch_predict`, { inputs }));
46
+ const ids = (data.ids ?? []);
47
+ return {
48
+ content: [{
49
+ type: "text",
50
+ text: `Batch predict: ${ids.length} job(s) submitted. Parent: ${job_id}. Poll each with jobs(action=status, job_id="<id>"). IDs: ${ids.join(", ")}`,
51
+ }],
52
+ };
53
+ }
33
54
  export function registerInferenceTool(server) {
34
55
  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.
35
56
 
36
57
  | Action | Use when | Timing |
37
58
  |--------|----------|--------|
38
59
  | predict | Scoring rows against the trained map (new data OR the training set itself) | 5–120s |
60
+ | batch_predict | Fan out many predict jobs at once against one trained map (several datasets/row-sets in one call) | submits N jobs |
39
61
  | 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 |
40
- | compare | Comparing hit distributions of a second dataset against training (drift, A/B) | 30–120s |
62
+ | 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 |
41
63
  | project_columns | Project one or more dataset columns onto the map (component planes); dataset can be training set or partial-feature set | 10–90s |
42
64
  | report | Get a report manifest (artifact keys + URLs) to build your own report in Quarto/Notebook/script | Immediate (sync) |
43
65
 
44
- Sync/async: predict, impute_column, and compare are async jobs. The proxy auto-polls and usually returns when the job completes. If it returns a job_id instead (e.g. timeout), poll jobs(action=status, job_id=...) then results(action=download, job_id=...) to retrieve the artifact.
66
+ Sync/async: predict, impute_column, and compare 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).
45
67
  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).
46
68
  report is the only synchronous inference action — returns manifest immediately; no job to poll.
47
69
  NOT FOR: Retraining or changing the map — all actions treat the trained map as frozen.
@@ -54,22 +76,27 @@ action=predict: Score rows against the trained map.
54
76
  Output style (output param, default "compact"):
55
77
  - "compact" → predictions.csv (row_id, bmu_x, bmu_y, bmu_node_index, cluster_id [, quantization_error, potential_anomaly]).
56
78
  - "annotated" → annotated.csv (full source CSV with bmu_x, bmu_y, bmu_node_index, cluster_id appended). Requires a dataset (no inline rows).
57
- Regime auto-detected:
58
- - If the resolved dataset matches the parent training dataset, regime="training" and QE / qe_p95 / potential_anomaly fields are omitted from the compact output. QE on training data is fitting error, not a generalisation metric, and the p95 anomaly flag would be circular. Use a held-out dataset for quality assessment.
59
- - Otherwise regime="new" and the full QE columns are returned.
79
+ 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.
60
80
  Schema rules: dataset / batch rows must match the trained feature set including cyclic-expanded columns (e.g. key_cos, key_sin, not raw key). For a single inline row, the proxy uses the stateless model endpoint — raw categorical strings are allowed for baseline categorical_features models, and raw cyclic inputs are expanded from training config.
61
81
  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.
62
82
  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.
63
83
 
64
84
  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.
65
85
 
66
- action=compare: 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).
86
+ 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).
67
87
  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>).
68
88
  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.`, {
69
89
  action: z
70
- .enum(["predict", "impute_column", "compare", "project_columns", "report"])
71
- .describe("predict: score rows; impute_column: topology-neighbor pool imputation for a column not in training; compare: drift/cohort diff heatmap; project_columns: project dataset columns onto map; report: manifest of primitives for custom report."),
90
+ .enum(["predict", "batch_predict", "impute_column", "compare", "project_columns", "report"])
91
+ .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; report: manifest of primitives for custom report."),
72
92
  job_id: z.string().describe("Job ID of a completed map training job"),
93
+ inputs: z
94
+ .array(z.object({
95
+ dataset_id: z.string().optional(),
96
+ rows: z.array(z.record(z.string(), z.number())).optional(),
97
+ }))
98
+ .optional()
99
+ .describe("action=batch_predict: array of inputs; each item has dataset_id and/or rows (same shape as a single predict)."),
73
100
  dataset_id: z.string().optional().describe("action=predict/impute_column/compare/project_columns: Dataset ID. predict=data to score (defaults to the training dataset when omitted); impute_column=dataset with training features + target_column; compare=dataset B; project_columns=dataset with columns to project."),
74
101
  columns: z.array(z.string()).optional().describe("action=project_columns: column names to project onto the map (must exist in the dataset)."),
75
102
  rows: z.array(z.record(z.string(), z.union([z.number(), z.string()]))).optional().describe("action=predict: inline rows to score (max 500). For a single inline row, raw categorical strings are allowed for baseline categorical_features models. Batch rows should remain numeric and match the training schema."),
@@ -86,9 +113,11 @@ action=report: Returns a report manifest for the given job_id (job must be compl
86
113
  .describe("action=impute_column: instantaneous (default) = pool mean/median; categorical = pool mode; cumulative = monotonic-counter (pool aggregation is rough; warns). A monotonic-counter warning is emitted automatically regardless."),
87
114
  weighting: z.enum(["distance", "uniform"]).optional()
88
115
  .describe("action=impute_column: distance (default) weights pooled values by map-neighbourhood proximity (closer BMU nodes count more); uniform is a flat pool."),
89
- }, async ({ action, job_id, dataset_id, columns, rows, output, colormap, output_format, output_dpi, top_n, target_column, only_missing, impute_aggregation, cv_folds, target_column_kind, weighting }) => {
116
+ }, async ({ action, job_id, dataset_id, columns, rows, output, colormap, output_format, output_dpi, top_n, target_column, only_missing, impute_aggregation, cv_folds, target_column_kind, weighting, inputs }) => {
90
117
  const dpiMap = { standard: 1, retina: 2, print: 4 };
91
118
  const numericDpi = dpiMap[output_dpi ?? "retina"] ?? 2;
119
+ if (action === "batch_predict")
120
+ return runBatchPredict({ job_id, inputs });
92
121
  if (action === "predict") {
93
122
  const outputStyle = output ?? "compact";
94
123
  if (outputStyle === "annotated" && rows) {