@barivia/barsom-mcp 0.13.0 → 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), `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
 
@@ -95,21 +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
114
  | `status` | Polling after any async job — every 10–15s; map jobs expose `progress_phase` (ordering, convergence, cv, artifacts) |
107
115
  | `list` | Finding job IDs, checking pipeline state |
108
116
  | `compare` | Picking the best run from a set (QE, TE, silhouette table) |
109
117
  | `cancel` | Stopping a running job |
110
118
  | `delete` | Permanently removing a job + its S3 files |
111
- | `batch_predict` | Submit multiple predict jobs from one parent training job |
112
- | `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`.
113
121
 
114
122
  ### `results(action)`
115
123
 
@@ -129,7 +137,8 @@ All actions use a frozen trained map — no retraining. Derived columns use **`d
129
137
  | Action | Output | Timing |
130
138
  |--------|--------|--------|
131
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 |
132
- | `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 |
133
142
  | `compare` | density-diff heatmap + top gained/lost nodes — drift, A/B, cohort | 30–120s |
134
143
  | `project_columns` | Project one or more dataset columns onto the trained map (component planes) | async |
135
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 |
@@ -169,8 +178,9 @@ The right viewer depends on **(MCP App support)** **and** **(can the human reach
169
178
 
170
179
  ### Migration notes
171
180
 
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.
172
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`**.
173
- - **`train_floop_chain` removed (0.13.0):** the deprecated alias of `jobs(action=train_floop_siom)` is gone. Use **`jobs(action=train_floop_siom)`**.
183
+ - **`train_floop_chain` removed (0.13.0):** the deprecated alias of `jobs(action=train_floop_siom)` is gone. Use **`train(action=floop_siom)`**.
174
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.
175
185
 
176
186
  ### `send_feedback`
@@ -187,7 +197,7 @@ When adding or refining tools, follow [MCP best practices](https://modelcontextp
187
197
 
188
198
  ## Data preparation
189
199
 
190
- 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.
191
201
 
192
202
  ## How It Works
193
203
 
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 |\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; `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.13.0";
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,7 +98,7 @@ 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 |
@@ -106,12 +106,12 @@ export function registerDatasetsTool(server) {
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.
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
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,7 +132,7 @@ 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
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"),
@@ -367,8 +367,8 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
367
367
  const temporalCols = temporalSugg.flatMap((ts) => ts.columns ?? []);
368
368
  const allCols = [...new Set([...cyclicCols, ...temporalCols])];
369
369
  lines.push(``, allCols.length > 0
370
- ? `Consider using cyclic_features or temporal_features for these columns when calling jobs(action=train_map): ${allCols.join(", ")}.`
371
- : `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).`);
372
372
  }
373
373
  if (samples.length > 0) {
374
374
  lines.push(``, `Sample Rows (first ${samples.length}):`);
@@ -378,7 +378,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
378
378
  lines.push(`| ${cols.map((c) => String(row[c] ?? "")).join(" | ")} |`);
379
379
  }
380
380
  }
381
- 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=...).`);
382
382
  return { content: [{ type: "text", text: lines.join("\n") }] };
383
383
  }
384
384
  if (action === "analyze") {
@@ -473,7 +473,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
473
473
  `Expression: ${expression} | Rows: ${summary.n_rows ?? "?"}`,
474
474
  summary.nan_count ? `NaN values: ${summary.nan_count}` : "",
475
475
  `Min: ${summary.min ?? "?"} | Max: ${summary.max ?? "?"} | Mean: ${summary.mean ?? "?"}`,
476
- `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.`,
477
477
  ]
478
478
  .filter(Boolean)
479
479
  .join("\n"),
@@ -531,7 +531,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
531
531
  lines.push(`Selected source-column indices (1-based): [${idxs.join(", ")}]`);
532
532
  }
533
533
  }
534
- 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.`);
535
535
  return { content: [{ type: "text", text: lines.filter(Boolean).join("\n") }] };
536
536
  }
537
537
  if (poll.status === "failed") {
@@ -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
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.
@@ -65,9 +87,16 @@ action=compare: density-diff of a cohort dataset against the training set on ONE
65
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>).
66
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.`, {
67
89
  action: z
68
- .enum(["predict", "impute_column", "compare", "project_columns", "report"])
69
- .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."),
70
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)."),
71
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."),
72
101
  columns: z.array(z.string()).optional().describe("action=project_columns: column names to project onto the map (must exist in the dataset)."),
73
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."),
@@ -84,9 +113,11 @@ action=report: Returns a report manifest for the given job_id (job must be compl
84
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."),
85
114
  weighting: z.enum(["distance", "uniform"]).optional()
86
115
  .describe("action=impute_column: distance (default) weights pooled values by map-neighbourhood proximity (closer BMU nodes count more); uniform is a flat pool."),
87
- }, 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 }) => {
88
117
  const dpiMap = { standard: 1, retina: 2, print: 4 };
89
118
  const numericDpi = dpiMap[output_dpi ?? "retina"] ?? 2;
119
+ if (action === "batch_predict")
120
+ return runBatchPredict({ job_id, inputs });
90
121
  if (action === "predict") {
91
122
  const outputStyle = output ?? "compact";
92
123
  if (outputStyle === "annotated" && rows) {