@barivia/barsom-mcp 0.20.0 → 0.20.2

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/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 s,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as i}from"./viz-server.js";import{API_KEY as a,apiCall as l,apiRawCall as c,loadViewHtml as p,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 h,JOBS_DESCRIPTION_BASE as _}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as w,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as j}from"./tools/account.js";import{registerInferenceTool as v}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as P}from"./tools/training_guidance.js";import{registerFeedbackTool as k}from"./tools/feedback.js";import{registerTrainingMonitorTool as I,TRAINING_MONITOR_URI as A}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText as T}from"./prepare_training_prompt.js";a||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const M=new e({name:"analytics-engine",version: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 `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map, siom_map, impute (sparse → map + dense imputed.csv), floop_siom (entitled). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list, compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (figures="none" for metrics-only), export, download, recolor (async). Read/render completed jobs |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, burst/compute actions, history, add_funds |\n| Bootstrap | `guide_barsom_workflow`, `training_guidance`, `prepare_training` prompt | orientation, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions (incl. `transition_flow`) and `results(recolor)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Per-request HTTP timeout defaults to 60s (`datasets(analyze)` is async + auto-polled, so it is not bound by it); on slow networks raise `BARIVIA_FETCH_TIMEOUT_MS`. Timeouts are NOT auto-retried (avoids duplicating slow/expensive work).\n\n## Constraints\n\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.'});s(M,y,y,{mimeType:n},async()=>{const e=await p("results-explorer");return{contents:[{uri:y,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(M,A,A,{mimeType:n},async()=>{const e=await p("training-monitor");return{contents:[{uri:A,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),x(M),w(M),I(M),f(M),g(M),h(M,_),b(M),j(M),v(M),P(M),k(M),M.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),M.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>({messages:[{role:"user",content:{type:"text",text:await T(e)}}]}));const S=new t;(async function(){try{const e=await i(l,c,p);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=M.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);d(!!o?.mimeTypes?.includes(n))},await M.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 s,RESOURCE_MIME_TYPE as n}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as i}from"./viz-server.js";import{API_KEY as a,apiCall as l,apiRawCall as c,loadViewHtml as p,setVizPort as m,setClientSupportsMcpApps as u,CLIENT_VERSION as d}from"./shared.js";import{registerDatasetsTool as f}from"./tools/datasets.js";import{registerTrainTool as g}from"./tools/train.js";import{registerJobsTool as h,JOBS_DESCRIPTION_BASE as _}from"./tools/jobs.js";import{registerResultsTool as b}from"./tools/results.js";import{registerExploreMapTool as w,RESULTS_EXPLORER_URI as y}from"./tools/explore_map.js";import{registerAccountTool as j}from"./tools/account.js";import{registerInferenceTool as v}from"./tools/inference.js";import{registerGuideBarsomTool as x}from"./tools/guide_barsom.js";import{registerTrainingGuidanceTool as P}from"./tools/training_guidance.js";import{registerFeedbackTool as k}from"./tools/feedback.js";import{registerTrainingMonitorTool as I,TRAINING_MONITOR_URI as A}from"./tools/training_monitor.js";import{resolvePrepareTrainingPromptText as M}from"./prepare_training_prompt.js";a||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));const T=new e({name:"analytics-engine",version:d,instructions:'# Barivia Mapping Analytics Engine\n\nSelf-organizing map (SOM) analytics: project high-dimensional data to a 2D grid for clusters, gradients, and anomalies.\n\n## Workflow (short)\n\nUpload (`datasets(upload)`) → `datasets(preview)` and `datasets(analyze)` before train → submit one of `train(action=map)`, `train(action=siom_map)`, or `train(action=floop_siom)` (only if plan allows FLooP) → poll `jobs(status)` every 10–15s until `completed` → `results(get)` for metrics and figures (there is no separate analyze tool), then `results_explorer(job_id)` to browse the figures interactively. Then `jobs(compare)`, `results(download/recolor)`, or `inference` (predict / compare / project_columns / impute_column / transition_flow) as needed.\n\n**Full detail:** Call `guide_barsom_workflow` for plan-scoped tool map, training modes, async rules, optional MCP App UIs, and step-by-step SOP (from the Barivia API when online).\n\n## Tool taxonomy (one tool per stage — do not mix)\n\n| Stage | Tool(s) | Contract |\n|-------|---------|----------|\n| Data | `datasets` | upload, preview, analyze, list, **get** (status/staging), subset, add_expression, reduce_spectral, delete — ingest + feature engineering, no map |\n| Train submit | `train` | map, siom_map, impute (sparse → map + dense imputed.csv), floop_siom (entitled). Async, returns job_id. Submit only |\n| Lifecycle | `jobs` | status, list, compare (**training runs**), cancel, delete. No training/scoring here |\n| Artifacts | `results` | get (default=combined only; figures="none" for metrics-only sweeps; figures="all" or results_explorer/download for every plot), export, download, recolor (async). Read/render completed jobs |\n| Frozen-map ops | `inference` | predict (regime-aware; "compact"|"annotated"), batch_predict, impute_column (neighbor-pool fill for a non-training column), compare (cohort density-diff), project_columns, transition_flow (time-ordered rows only), report. No weight updates |\n| Account | `account` | status, burst/compute actions, history, add_funds |\n| Bootstrap | `guide_barsom_workflow`, `training_guidance`, `prepare_training` prompt | orientation, parameter hints, narrative checklist (API/tier-scoped) |\n| Explore (UI) | `results_explorer`, `training_monitor` | richer UX over the same API; `jobs(status)`+`results(get)` still suffice headless |\n| Other | `send_feedback` | only after user agrees |\n\n### Avoid these mis-routes\n- `jobs(compare)` = metrics table across **training runs** (pick best in a sweep). `inference(compare)` = density-diff of one map vs a **second dataset** (drift/cohort). Different tools.\n- Post-training map analysis (predict, compare, project_columns, impute_column, **transition_flow**) is all **inference** — not `results`. `results` is presentation/artifacts only (`recolor` stays here: it re-renders, it doesn\'t touch data).\n- Project a quantity onto the map: existing columns → `inference(project_columns)`; a formula → `datasets(add_expression, project_onto_job=<job>)`.\n\n## Async pattern\n\n- **Manual poll:** Training submits return `job_id` immediately — poll `jobs(status)` every 10–15s. **Running is not failed**; large grids or FLooP-SIOM can take many minutes. `max_nodes` (FLooP) is a total node budget, not grid side length.\n- **Often auto-polled:** `inference` actions (incl. `transition_flow`) and `results(recolor)` may wait in-proxy; if you get a `job_id`, poll `jobs(status)` the same way.\n\nCredits: jobs consume compute credits; check `account(status)` before big runs. Per-request HTTP timeout defaults to 60s (`datasets(analyze)` is async + auto-polled, so it is not bound by it); on slow networks raise `BARIVIA_FETCH_TIMEOUT_MS`. Timeouts are NOT auto-retried (avoids duplicating slow/expensive work).\n\n## Constraints\n\n- Prep ladder: `prepare_training` prompt = narrative checklist; `training_guidance` = structured hints (presets, resolved normalization/transforms, grid/epochs). Review these, then submit with `train`. Do not guess tiers or FLooP entitlement.\n- `inference(predict)`: prefer `dataset_id` for batch and for SIOM/irregular maps; single-row `rows` uses a fast path that can fail on some topologies — retry with `dataset_id`. FLooP-SIOM: if predict jobs fail while grid SIOM works, capture errors + `job_id`.\n- Column names are case-sensitive — match `datasets(preview)`.\n- Default training path is numeric/cyclic/temporal; use explicit `categorical_features` for baseline categoricals. `predict` must match the model contract.\n- After `results(recolor)`, `inference(transition_flow)`, or `inference(project_columns)`, use the **new** `job_id` returned for follow-up `results` if applicable.\n- **Large results / MCP timeouts:** For sweeps, `jobs(compare)`, or metrics-only reads use `results(action=get, figures="none")`. Default `get` inlines combined only. For every plot use `results_explorer` or `results(action=download)`, not `figures="all"` in chat unless the user wants all images inline.'});s(T,y,y,{mimeType:n},async()=>{const e=await p("results-explorer");return{contents:[{uri:y,mimeType:n,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(T,A,A,{mimeType:n},async()=>{const e=await p("training-monitor");return{contents:[{uri:A,mimeType:n,text:e??"<html><body>Training Monitor view not built yet.</body></html>"}]}}),x(T),w(T),I(T),f(T),g(T),h(T,_),b(T),j(T),v(T),P(T),k(T),T.prompt("info","Short orientation for the Barivia Mapping MCP. For full plan-scoped workflow, tool map, and SOP, the model should call guide_barsom_workflow. Use when the user asks what this MCP can do or how to get started.",{},()=>({messages:[{role:"user",content:{type:"text",text:["Give a concise, scannable answer (headers + bullets):","","**What it is:** MCP client to the Barivia mapping engine (2D SOM / SIOM / FLooP-SIOM when entitled) over HTTPS.","","**First step:** Call `guide_barsom_workflow` for plan-scoped bootstrap (full tool list, async rules, training modes, optional MCP Apps, SOP).","","**Core path:** `datasets(upload)` → `datasets(preview)` + `datasets(analyze)` → choose training action → poll `jobs(status)` every 10–15s until completed → `results(get)` (all main figures/metrics; no separate analyze tool) → `results_explorer(job_id)` to browse figures.","",'**Key tools:** `datasets` (data; reduce_spectral for spectra/long blocks), `train` (map/siom_map/impute/floop_siom; pass `label` for readable compare rows; preset=quick for a fast first map), `jobs` (status/list/compare/cancel/delete — lifecycle only), `results` (get/download/export/recolor; figures="none" for metrics-only), `inference` (predict; batch_predict; impute_column for topology-neighbor pool fill; compare; project_columns; transition_flow; report), `account` (status/credits/queue).',"","**Prep help:** `prepare_training` prompt (checklist) · `training_guidance` (presets/JSON hints + resolved normalization/params) — review, then submit with `train`.","","**Browse results:** `results_explorer(job_id)` is the preferred way to explore figures after a first `results(get)` glance; `training_monitor` shows live progress. Both optional — `results` + `jobs(status)` suffice headless.","","**After training:** `jobs(compare)` across runs, `results(recolor)`, `inference(project_columns)` for variables not in training, `inference(transition_flow)` only if rows are time-ordered.","","**Rules:** Running ≠ failed. Column names must match `datasets(preview)` exactly. Do not call `_fetch_figure` from chat (host/UI only); use `results(get)` or `results_explorer`.","","Offer `send_feedback` only after asking the user."].join("\n")}}]})),T.prompt("prepare_training","Narrative pre-training checklist (prompt). Use after upload and before train. Content is tier-scoped from the API when online. Prep ladder: this prompt = story checklist; training_guidance tool = JSON presets/parameter hints. Review, then submit with train.",{dataset_id:o.string().describe("Dataset ID to prepare for training")},async({dataset_id:e})=>({messages:[{role:"user",content:{type:"text",text:await M(e)}}]}));const S=new t;(async function(){try{const e=await i(l,c,p);m(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("Barivia viz server failed to start:",e)}const e=T.server;e.oninitialized=()=>{const t=e.getClientCapabilities(),o=r(t);u(!!o?.mimeTypes?.includes(n))},await T.connect(S)})().catch(console.error);
@@ -91,7 +91,7 @@ export function formatSnapshotLine(s) {
91
91
  if (s.qe != null)
92
92
  parts.push(`QE ${s.qe.toFixed(4)}`);
93
93
  if (s.te != null)
94
- parts.push(`TE ${s.te.toFixed(4)}`);
94
+ parts.push(`Epoch TE ${s.te.toFixed(4)}`);
95
95
  if (s.eta_sec != null)
96
96
  parts.push(`ETA ~${s.eta_sec}s`);
97
97
  if (s.ordering_errors_tail?.length) {
package/dist/shared.js CHANGED
@@ -26,7 +26,7 @@ export const RETRYABLE_STATUS = new Set([502, 503, 504]);
26
26
  * X-Barsom-Client-Version so the server can annotate tool guidance with the
27
27
  * wrapper version each action requires. Keep in sync with package.json on bump.
28
28
  */
29
- export const CLIENT_VERSION = "0.20.0";
29
+ export const CLIENT_VERSION = "0.20.2";
30
30
  /** User-facing links; keep aligned with barivia.se / api.barivia.se. */
31
31
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
32
32
  /** Self-serve account dashboard (manage plan, billing, and API keys). */
@@ -611,6 +611,19 @@ export function getCaptionForImage(filename) {
611
611
  return "Projected variable — per-node aggregation on the map.";
612
612
  return "";
613
613
  }
614
+ export const MAX_INLINE_BYTES = parseInt(process.env.BARIVIA_MAX_INLINE_BYTES ?? "8000000", 10);
615
+ let _inlineAttachBytesUsed = 0;
616
+ /** Reset inline image byte budget (call at the start of each MCP tool response). */
617
+ export function resetInlineAttachBudget() {
618
+ _inlineAttachBytesUsed = 0;
619
+ }
620
+ export function inlineAttachBytesUsed() {
621
+ return _inlineAttachBytesUsed;
622
+ }
623
+ /** When true, results(get) should attach every image in summary.files not already inlined. */
624
+ export function shouldFetchAllRemainingFigures(figures) {
625
+ return figures === "all" || figures === "images";
626
+ }
614
627
  export async function tryAttachImage(content, jobId, filename) {
615
628
  // MCP image content only supports raster MIME types (png/jpeg/gif/webp).
616
629
  // PDF and SVG must be retrieved out-of-band via get_result_image.
@@ -624,6 +637,16 @@ export async function tryAttachImage(content, jobId, filename) {
624
637
  }
625
638
  try {
626
639
  const { data: imgBuf } = await apiRawCall(`/v1/results/${jobId}/image/${filename}`);
640
+ const nextTotal = _inlineAttachBytesUsed + imgBuf.length;
641
+ if (nextTotal > MAX_INLINE_BYTES) {
642
+ content.push({
643
+ type: "text",
644
+ text: `Skipped inline ${filename} (${imgBuf.length} bytes would exceed inline budget). ` +
645
+ `Use results(action=download, job_id="${jobId}") or results_explorer(job_id="${jobId}").`,
646
+ });
647
+ return;
648
+ }
649
+ _inlineAttachBytesUsed = nextTotal;
627
650
  content.push({
628
651
  type: "image",
629
652
  data: imgBuf.toString("base64"),
@@ -166,7 +166,7 @@ action=reduce_spectral: Run a pre-training reducer over an ordered block of nume
166
166
  - log_sample: keep k columns at log-spaced indices — SAXS/WAXS & powder diffraction, log-frequency / octave-like audio, attenuation vs wavelength (UV–Vis–IR stacks), depth profiling, chromatography retention ladders — anywhere column order is exponential, logarithmic, or perceptually log-spaced.
167
167
  - uniform_sample: keep k columns at evenly-spaced indices — regularly-sampled time series, frame-by-frame features, evenly-binned histograms.
168
168
  - stats: 6 fixed per-row statistics (mean, std, min, max, skew, integral) — cheap baseline for any sequenced numeric block; k is ignored.
169
- Required params: name (prefix for derived columns), method, columns_block (ordered source column names ≥ 2), k ( 1, < length(columns_block); ignored for stats).
169
+ Required params: name (prefix for derived columns), method, a column selector, k (≥ 1, < #columns; ignored for stats). Column selector — pick a base block then optionally exclude non-frequency columns: base = columns_block (explicit ordered names ≥ 2) OR columns_range ([first, last], server expands the inclusive numeric block — preferred for wide spectra); columns_except removes columns that are NOT frequency data (id/label/target/metadata) from that block (combine with a base), or used alone means "all numeric columns except these". Only frequency columns are reduced; excluded columns remain in the dataset.
170
170
  action=delete: Remove a dataset and all S3 data permanently.
171
171
 
172
172
  BEST FOR: Tabular numeric data. CSV with header required.
@@ -237,7 +237,16 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
237
237
  columns_block: z
238
238
  .array(z.string())
239
239
  .optional()
240
- .describe("action=reduce_spectral: ordered list of source column names representing the block (≥ 2; ≥ k+1 for non-stats methods)"),
240
+ .describe("action=reduce_spectral: explicit ordered list of source frequency/spectral column names (≥ 2; ≥ k+1 for non-stats methods). For a large contiguous block prefer columns_range to avoid sending thousands of names. Mutually exclusive with columns_range."),
241
+ columns_range: z
242
+ .array(z.string())
243
+ .length(2)
244
+ .optional()
245
+ .describe("action=reduce_spectral: [first_column, last_column] — server expands to the inclusive block of numeric columns in dataset order (e.g. [\"wl_188\", \"wl_441\"]). Mutually exclusive with columns_block. If non-frequency columns (id/label/target) fall inside the range, drop them with columns_except."),
246
+ columns_except: z
247
+ .array(z.string())
248
+ .optional()
249
+ .describe("action=reduce_spectral: column names to EXCLUDE from the reduction — the non-frequency columns (id/label/target/metadata) that must NOT be dimensionally reduced. Combine with columns_block or columns_range to subtract them from the block, or use alone to mean 'all numeric columns except these'. Excluded columns stay in the dataset untouched."),
241
250
  k: z
242
251
  .number()
243
252
  .int()
@@ -255,7 +264,7 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
255
264
  .int()
256
265
  .optional()
257
266
  .describe("action=subset: RNG seed for sample_n (default 42; same seed + dataset reproduces the same sample)."),
258
- }, 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 }) => {
267
+ }, 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, columns_range, columns_except, k, sample_n, sample_seed }) => {
259
268
  if (action === "upload") {
260
269
  if (!name)
261
270
  throw new Error("datasets(upload) requires name");
@@ -569,16 +578,34 @@ ESCALATION: If upload fails with column errors, open the file locally and verify
569
578
  throw new Error("datasets(reduce_spectral) requires name (prefix for derived columns)");
570
579
  if (!method)
571
580
  throw new Error("datasets(reduce_spectral) requires method: pca | log_sample | uniform_sample | stats");
572
- if (!columns_block || columns_block.length < 2) {
573
- throw new Error("datasets(reduce_spectral) requires columns_block: ordered array of ≥ 2 source column names");
581
+ // Base selector: columns_block OR columns_range (mutually exclusive).
582
+ // columns_except is an independent filter that removes non-frequency
583
+ // columns (id/label/target/metadata) from the base block, so it may be
584
+ // combined with either base — or used alone to mean "all numeric except".
585
+ if (columns_block && columns_range) {
586
+ throw new Error("datasets(reduce_spectral): provide either columns_block or columns_range, not both");
587
+ }
588
+ if (!columns_block && !columns_range && !columns_except) {
589
+ throw new Error("datasets(reduce_spectral) requires a column selector: columns_block (ordered names ≥ 2), columns_range ([first, last]), and/or columns_except (drop non-frequency columns; alone = all numeric except these)");
590
+ }
591
+ if (columns_block && columns_block.length < 2) {
592
+ throw new Error("datasets(reduce_spectral) columns_block must list ≥ 2 source column names");
574
593
  }
575
594
  if (method !== "stats" && (k === undefined || k < 1)) {
576
595
  throw new Error(`datasets(reduce_spectral) requires k ≥ 1 for method '${method}'`);
577
596
  }
578
- if (method !== "stats" && k !== undefined && k >= columns_block.length) {
597
+ // Upper bound on k (k < #columns) is enforced server-side after range/except
598
+ // resolution; for an explicit columns_block with no exclusion we can also check locally.
599
+ if (method !== "stats" && k !== undefined && columns_block && !columns_except && k >= columns_block.length) {
579
600
  throw new Error(`datasets(reduce_spectral) requires k < length(columns_block); got k=${k}, columns_block=${columns_block.length}`);
580
601
  }
581
- const body = { name, method, columns: columns_block };
602
+ const body = { name, method };
603
+ if (columns_block)
604
+ body.columns = columns_block;
605
+ if (columns_range)
606
+ body.column_range = columns_range;
607
+ if (columns_except)
608
+ body.columns_except = columns_except;
582
609
  if (method !== "stats")
583
610
  body.k = k;
584
611
  const data = (await apiCall("POST", `/v1/datasets/${dataset_id}/reduce_spectral`, body));
@@ -157,7 +157,10 @@ async function handleResultsExplorer(job_id) {
157
157
  `This localhost port is assigned per MCP session and changes if the proxy restarts — if the page won't load, re-run results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port.`,
158
158
  });
159
159
  }
160
- return structuredTextResult(payload, undefined, content);
160
+ return {
161
+ ...structuredTextResult(payload, undefined, content),
162
+ _meta: { ui: { resourceUri: RESULTS_EXPLORER_URI } },
163
+ };
161
164
  }
162
165
  export function registerExploreMapTool(server) {
163
166
  const toolConfig = {
@@ -2,7 +2,6 @@ import { z } from "zod";
2
2
  import { registerAuditedTool } from "../audit.js";
3
3
  import { apiCall, textResult } from "../shared.js";
4
4
  import { formatJobCancelText, formatJobStatusText } from "../job_status_format.js";
5
- import { pollFinalizeIfPresent, refreshJobAfterFinalize } from "../train_finalize.js";
6
5
  import { DEFAULT_BLOCK_UNTIL_SEC, DEFAULT_POLL_INTERVAL_SEC, runJobMonitor } from "../job_monitor.js";
7
6
  export const JOBS_DESCRIPTION_BASE = `Manage and inspect jobs (lifecycle only).
8
7
 
@@ -185,19 +184,7 @@ export function registerJobsTool(server, description) {
185
184
  if (action === "status") {
186
185
  if (!job_id)
187
186
  throw new Error("jobs(status) requires job_id");
188
- let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
189
- const status = String(data.status ?? "");
190
- if (status === "completed" && data.finalize_job_id) {
191
- const { note } = await pollFinalizeIfPresent(job_id, data);
192
- data = await refreshJobAfterFinalize(job_id);
193
- const text = formatJobStatusText(job_id, data);
194
- return {
195
- content: [{
196
- type: "text",
197
- text: note ? `${text}\n${note}` : text,
198
- }],
199
- };
200
- }
187
+ const data = (await apiCall("GET", `/v1/jobs/${job_id}`));
201
188
  return { content: [{ type: "text", text: formatJobStatusText(job_id, data) }] };
202
189
  }
203
190
  if (action === "list") {
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import fs from "node:fs/promises";
3
3
  import { z } from "zod";
4
4
  import { registerAuditedTool } from "../audit.js";
5
- import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, pollUntilComplete, tryAttachImage, getResultsImagesToFetch, getCaptionForImage, mimeForFilename, } from "../shared.js";
5
+ import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, pollUntilComplete, tryAttachImage, resetInlineAttachBudget, getResultsImagesToFetch, getCaptionForImage, shouldFetchAllRemainingFigures, mimeForFilename, } from "../shared.js";
6
6
  export function registerResultsTool(server) {
7
7
  registerAuditedTool(server, "results", `Retrieve, recolor, download, or export figures and metrics for a completed map job. Presentation and artifact access only — operations on the frozen map (predict, compare, project, impute_column, transition_flow) live in **inference**.
8
8
 
@@ -87,6 +87,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
87
87
  }, async ({ action, job_id, figures, include_individual, include_json, folder, colormap, output_format, output_dpi, recolor_figures, export_type: exportType }) => {
88
88
  const dpiMap = { standard: 1, retina: 2, print: 4 };
89
89
  if (action === "get") {
90
+ resetInlineAttachBudget();
90
91
  const data = (await apiCall("GET", `/v1/results/${job_id}`));
91
92
  const summary = (data.summary ?? {});
92
93
  const jobLabel = data.label != null && data.label !== "" ? String(data.label) : null;
@@ -453,13 +454,14 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
453
454
  // figures="none": metrics-only mode. Skip any further image attaches and the
454
455
  // "Available figures…" hint; keep the structured-data export hint since it
455
456
  // points at non-image artifacts.
456
- if (figures !== "none") {
457
+ if (shouldFetchAllRemainingFigures(figures)) {
457
458
  for (const fname of files) {
458
459
  if (isImage(fname) && !inlinedImages.has(fname)) {
459
460
  const cap = getCaptionForImage(fname);
460
461
  if (cap)
461
462
  content.push({ type: "text", text: cap });
462
463
  await tryAttachImage(content, job_id, fname);
464
+ inlinedImages.add(fname);
463
465
  }
464
466
  }
465
467
  }
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
3
3
  import { runMcpToolAudit } from "../audit.js";
4
4
  import { apiCall, getClientSupportsMcpApps, getVizPort, structuredTextResult, } from "../shared.js";
5
+ import { lastEpochTeFromCurves } from "../training_monitor_curve.js";
5
6
  export const TRAINING_MONITOR_URI = "ui://barsom/training-monitor";
6
7
  export const TRAINING_MONITOR_REFRESH_MS = 5000;
7
8
  function hasCurveArrays(data) {
@@ -24,37 +25,66 @@ function buildStructuredContent(job_id, data) {
24
25
  refresh_interval_ms: TRAINING_MONITOR_REFRESH_MS,
25
26
  };
26
27
  }
28
+ function hasTeCurveArrays(data) {
29
+ return ((Array.isArray(data.ordering_topographic_errors) &&
30
+ data.ordering_topographic_errors.length > 0) ||
31
+ (Array.isArray(data.convergence_topographic_errors) &&
32
+ data.convergence_topographic_errors.length > 0));
33
+ }
34
+ function needsTrainingLogEnrichment(data) {
35
+ const status = String(data.status ?? "");
36
+ if (status !== "completed" && status !== "failed" && status !== "cancelled")
37
+ return false;
38
+ if (!hasCurveArrays(data))
39
+ return true;
40
+ if (!hasTeCurveArrays(data))
41
+ return true;
42
+ if (data.map_topographic_error == null && data.topographic_error == null)
43
+ return true;
44
+ return false;
45
+ }
27
46
  async function enrichWithTrainingLog(job_id, data) {
28
47
  const status = String(data.status ?? "");
29
- if (status !== "completed" && status !== "failed")
30
- return data;
31
- if (hasCurveArrays(data))
48
+ if (status !== "completed" && status !== "failed" && status !== "cancelled")
32
49
  return data;
50
+ const epochTe = data.epoch_topographic_error ??
51
+ data.topographic_error ??
52
+ lastEpochTeFromCurves(data);
33
53
  try {
34
54
  const log = (await apiCall("GET", `/v1/results/${job_id}/training-log`));
35
- return {
55
+ const merged = {
36
56
  ...data,
37
57
  ordering_errors: log.ordering_errors ?? data.ordering_errors,
38
58
  convergence_errors: log.convergence_errors ?? data.convergence_errors,
39
59
  ordering_topographic_errors: log.ordering_topographic_errors ?? data.ordering_topographic_errors,
40
60
  convergence_topographic_errors: log.convergence_topographic_errors ?? data.convergence_topographic_errors,
41
61
  quantization_error: log.quantization_error ?? data.quantization_error,
42
- topographic_error: log.topographic_error ?? data.topographic_error,
43
62
  grid: log.grid ?? data.grid,
44
63
  model: log.model ?? data.model,
45
64
  epochs: log.epochs ?? data.epochs,
46
65
  n_samples: log.n_samples ?? data.n_samples,
47
66
  n_features: log.n_features ?? data.n_features,
67
+ training_curve_source_batches: log.training_curve_source_batches ?? data.training_curve_source_batches,
48
68
  };
69
+ const mapTe = log.topographic_error ?? data.map_topographic_error ?? data.topographic_error;
70
+ merged.epoch_topographic_error = epochTe ?? lastEpochTeFromCurves(merged);
71
+ merged.map_topographic_error = mapTe;
72
+ if (status === "completed" || status === "failed" || status === "cancelled") {
73
+ merged.topographic_error = mapTe;
74
+ }
75
+ return merged;
49
76
  }
50
77
  catch {
78
+ if (epochTe != null) {
79
+ return { ...data, epoch_topographic_error: epochTe };
80
+ }
51
81
  return data;
52
82
  }
53
83
  }
54
84
  export function registerTrainingMonitorTool(server) {
55
85
  registerAppTool(server, "training_monitor", {
56
86
  title: "Training Monitor",
57
- description: "Optional embedded or standalone view of training progress for a job_id. When embedded in a client that supports MCP Apps, the panel auto-refreshes every 5s until the job completes: live QE/TE learning curves plus a per-epoch hit-distribution heatmap (sampled BMU counts across the map, showing where observations are landing as training proceeds). A standalone browser URL is also available — its localhost port is per MCP session and goes stale if the proxy restarts (set BARIVIA_VIZ_PORT for a persistent port; re-run this tool for a fresh link). The same scalar fields are available via jobs(action=status)—not required to complete training or read results.",
87
+ description: "Optional embedded or standalone view of training progress for a job_id. When embedded in a client that supports MCP Apps, the panel auto-refreshes every 5s until the job completes: live QE/TE learning curves (uniformly subsampled to ≤1000 batch samples per phase, not one point per epoch) plus a per-epoch hit-distribution heatmap (sampled BMU counts across the map, showing where observations are landing as training proceeds). A standalone browser URL is also available — its localhost port is per MCP session and goes stale if the proxy restarts (set BARIVIA_VIZ_PORT for a persistent port; re-run this tool for a fresh link). The same scalar fields are available via jobs(action=status)—not required to complete training or read results.",
58
88
  inputSchema: {
59
89
  job_id: z.string().describe("Training job ID to monitor"),
60
90
  fetch_training_log: z
@@ -67,8 +97,7 @@ export function registerTrainingMonitorTool(server) {
67
97
  const { job_id, fetch_training_log } = args;
68
98
  let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
69
99
  const jobStatus = String(data.status ?? "");
70
- const terminal = jobStatus === "completed" || jobStatus === "failed";
71
- if (fetch_training_log || (terminal && !hasCurveArrays(data))) {
100
+ if (fetch_training_log || needsTrainingLogEnrichment(data)) {
72
101
  data = await enrichWithTrainingLog(job_id, data);
73
102
  }
74
103
  const structuredContent = buildStructuredContent(job_id, data);
@@ -101,6 +130,9 @@ export function registerTrainingMonitorTool(server) {
101
130
  `This localhost port is assigned per MCP session and changes if the proxy restarts — if the page stays at "running 0%" or stops updating, the link is stale: re-run training_monitor for a fresh URL, or set BARIVIA_VIZ_PORT in your MCP env for a persistent port. Health check: http://localhost:${port}/api/health?job_id=${encodeURIComponent(job_id)}`,
102
131
  });
103
132
  }
104
- return structuredTextResult(structuredContent, text, content);
133
+ return {
134
+ ...structuredTextResult(structuredContent, text, content),
135
+ _meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
136
+ };
105
137
  }));
106
138
  }
@@ -0,0 +1,60 @@
1
+ /** Batch-sample x-axis helpers for the training_monitor MCP App chart. */
2
+ export function batchSampleAxis(n, offset = 0) {
3
+ if (n <= 0)
4
+ return [];
5
+ return Array.from({ length: n }, (_, i) => offset + i + 1);
6
+ }
7
+ export function combinedBatchAxis(nOrd, nConv) {
8
+ if (nOrd <= 0 && nConv <= 0)
9
+ return [];
10
+ return [...batchSampleAxis(nOrd, 0), ...batchSampleAxis(nConv, nOrd)];
11
+ }
12
+ export function formatCurveSourceNote(data) {
13
+ const src = data.training_curve_source_batches;
14
+ if (!src)
15
+ return null;
16
+ const parts = [];
17
+ const ordTotal = typeof src.ordering === "number" ? src.ordering : null;
18
+ const convTotal = typeof src.convergence === "number" ? src.convergence : null;
19
+ const ordShown = Array.isArray(data.ordering_errors) ? data.ordering_errors.length : 0;
20
+ const convShown = Array.isArray(data.convergence_errors) ? data.convergence_errors.length : 0;
21
+ if (ordTotal != null && ordTotal > ordShown) {
22
+ parts.push(`${ordShown} of ${ordTotal.toLocaleString()} ordering batch samples`);
23
+ }
24
+ if (convTotal != null && convTotal > convShown) {
25
+ parts.push(`${convShown} of ${convTotal.toLocaleString()} convergence batch samples`);
26
+ }
27
+ if (parts.length === 0)
28
+ return null;
29
+ return `Displaying uniformly subsampled QE+TE curves (≤1000 points/phase, joint indices): ${parts.join("; ")}.`;
30
+ }
31
+ /** Map TE onto QE batch axis; when batch-aligned (same length), use values directly. */
32
+ export function alignTeToQeAxis(te, qeLen) {
33
+ if (qeLen <= 0)
34
+ return [];
35
+ if (te.length === qeLen)
36
+ return te;
37
+ if (te.length === 0)
38
+ return Array(qeLen).fill(null);
39
+ if (te.length >= qeLen)
40
+ return te.slice(0, qeLen);
41
+ const pad = qeLen - te.length;
42
+ return [...Array(pad).fill(null), ...te];
43
+ }
44
+ /**
45
+ * Last sampled TE point from the (batch-aligned) live TE curve. This is the most
46
+ * recent per-epoch TE estimate, distinct from the final trained-map TE in the summary.
47
+ */
48
+ export function lastEpochTeFromCurves(data) {
49
+ const conv = data.convergence_topographic_errors;
50
+ if (Array.isArray(conv) && conv.length > 0) {
51
+ const v = Number(conv[conv.length - 1]);
52
+ return Number.isFinite(v) ? v : null;
53
+ }
54
+ const ord = data.ordering_topographic_errors;
55
+ if (Array.isArray(ord) && ord.length > 0) {
56
+ const v = Number(ord[ord.length - 1]);
57
+ return Number.isFinite(v) ? v : null;
58
+ }
59
+ return null;
60
+ }