@barivia/barsom-mcp 0.23.12 → 0.23.14

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.
@@ -64,6 +64,10 @@ export function formatJobStatusText(job_id, data) {
64
64
  else if (prepareJobId) {
65
65
  parts.push(`phase: waiting_for_prepare (prepare_job_id=${prepareJobId})`);
66
66
  }
67
+ else if (progress >= 5) {
68
+ // Progress advanced but phase not yet stamped — do not imply early ingest.
69
+ parts.push(`phase: running`);
70
+ }
67
71
  else {
68
72
  parts.push(`phase: preprocessing (not set yet — often download/parse before first progress tick)`);
69
73
  }
package/dist/shared.js CHANGED
@@ -66,7 +66,7 @@ function newRequestId() {
66
66
  * X-Barsom-Client-Version so the server can annotate tool guidance with the
67
67
  * wrapper version each action requires. Keep in sync with package.json on bump.
68
68
  */
69
- export const CLIENT_VERSION = "0.23.12";
69
+ export const CLIENT_VERSION = "0.23.14";
70
70
  /** User-facing links; keep aligned with barivia.se / api.barivia.se. */
71
71
  export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
72
72
  /** Self-serve account dashboard (manage plan, billing, and API keys). */
@@ -1218,6 +1218,27 @@ export function coerceFiguresArg(v) {
1218
1218
  }
1219
1219
  return val;
1220
1220
  }
1221
+ /**
1222
+ * Images to inline after results(action=feature_divergences).
1223
+ * Default is text-only (omit / none / default / combined_only) so headless LLM hosts
1224
+ * (e.g. Ornith) are not rejected for image MCP content. Opt in with "all"/"images" or a filename array.
1225
+ */
1226
+ export function getFeatureDivergenceImagesToFetch(files, figures) {
1227
+ const isRasterOrSvg = (f) => /\.(png|svg)$/i.test(f);
1228
+ if (figures === "all" || figures === "images") {
1229
+ return files.filter(isRasterOrSvg);
1230
+ }
1231
+ if (Array.isArray(figures)) {
1232
+ return figures.map((name) => {
1233
+ const n = String(name);
1234
+ if (/\.(png|svg|pdf)$/i.test(n))
1235
+ return n;
1236
+ return `${n}.png`;
1237
+ });
1238
+ }
1239
+ // omit, "none", "default", "combined_only" → no inline images
1240
+ return [];
1241
+ }
1221
1242
  /** Attach result figures with captions; tracks inlined names for dedup. */
1222
1243
  export async function attachResultsImages(content, jobId, jobType, summary, figures, includeIndividual, inlinedImages) {
1223
1244
  for (const name of getResultsImagesToFetch(jobType, summary, figures, includeIndividual)) {
@@ -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, resetInlineAttachBudget, attachResultsImages, resolveOutputDpi, POLL_RECOLOR_MAX_MS, fmtDensityDiffNode, getCaptionForImage, shouldFetchAllRemainingFigures, mimeForFilename, structuredTextResult, coerceFiguresArg, } from "../shared.js";
5
+ import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, pollUntilComplete, tryAttachImage, resetInlineAttachBudget, attachResultsImages, resolveOutputDpi, POLL_RECOLOR_MAX_MS, fmtDensityDiffNode, getCaptionForImage, shouldFetchAllRemainingFigures, getFeatureDivergenceImagesToFetch, mimeForFilename, structuredTextResult, coerceFiguresArg, } 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
 
@@ -33,14 +33,14 @@ action=export: Structured data exports (not the same as download). Use export_ty
33
33
 
34
34
  action=download: Save figures/artifacts to disk (deduped file list). Use so user can open, share, or version files locally.
35
35
  - folder: e.g. "." or "./results". Relative to the client's working directory (or MCP workspace). Each job lands in a dedicated subfolder under folder: {job_type}_{label} when label was set at train time, otherwise {job_type}_{job_id_prefix} — so parallel downloads never overwrite summary.json or figure names.
36
- - figures: "all" (default) downloads published image files; OR a string array of exact artifact filenames from the job (e.g. ["predictions.csv"], ["combined.png","umatrix.png"]). Enum tokens like "none" are for action=get, not download.
36
+ - figures: "all" (default) downloads published image files; OR a string array of **exact** artifact filenames from summary.files / the Available list (e.g. ["combined.png","component_1_alcohol.png"]). Do not invent bare component_1.png — published planes are component_{i}_{feature}.png. Logical tokens like component_1 are for action=get/recolor only. Enum "none" is for get/feature_divergences, not download.
37
37
  - include_json: when true, also saves summary.json and other JSON artifacts (use this when you need CSVs listed in summary.files without naming each figure).
38
38
 
39
39
  action=recolor: Change colormap or output format — no retraining. Returns a new job_id; auto-polls 60s.
40
40
  AFTER: use results(action=get, job_id=NEW_JOB_ID).
41
41
  Colormaps: viridis, plasma, inferno, magma, cividis, turbo, coolwarm, balance, hsv, twilight, RdBu, Spectral. Also: thermal, hot, grays, RdYlBu, delta, curl, phase, rainbow, tol_bright, tol_muted.
42
42
 
43
- action=feature_divergences: Recompute component-plane divergences (min-shift sum-normalized denormalized planes). Finalize already writes symmetric KL + rankings; use this for Wasserstein or a refresh. metrics default ["symmetric_kl","directed_kl"]; add "wasserstein" for W1. Auto-polls then summarizes top pairs vs Pearson top_correlations.
43
+ action=feature_divergences: Recompute component-plane divergences (min-shift sum-normalized denormalized planes). Finalize already writes directed KL heatmap (divergence_kl: cell (i,j)=KL(pᵢ‖pⱼ); upper/lower triangles opposite directions) plus SKL pair tops and mean-outgoing ranking in JSON; use this for Wasserstein or a refresh. metrics default ["symmetric_kl","directed_kl"]; add "wasserstein" for W1. Auto-polls then returns text + structuredContent (tops/metrics/files). Default is **text-only** (no inline images) for headless LLM hosts; opt in with figures="all" or figures=["divergence_kl.png"]. Prefer results(download) for heatmaps on text-only clients.
44
44
 
45
45
  NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or temporal flow (use inference(action=transition_flow)).`, {
46
46
  action: z
@@ -50,7 +50,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
50
50
  figures: z.preprocess(coerceFiguresArg, z
51
51
  .union([z.enum(["default", "combined_only", "all", "images", "none"]), z.array(z.string())])
52
52
  .optional()
53
- .describe("Prefer string 'all'/'none'. action=get: omit=combined only; 'all'=all plots; 'none'=metrics text only; array=logical names (combined,umatrix,…). action=download: 'all'=image files; array=exact filenames (e.g. combined.png). Stringified JSON arrays from hosts are accepted.")),
53
+ .describe("Prefer string 'all'/'none'. action=get: omit=combined only; 'all'=all plots; 'none'=metrics text only; array=logical names (combined,umatrix,…). action=feature_divergences: omit/'none'=text-only (default); 'all'/'images'=inline heatmaps; array=exact heatmap filenames. action=download: 'all'=image files; array=exact filenames from summary.files (e.g. component_1_alcohol.png — not bare component_1.png). Stringified JSON arrays from hosts are accepted.")),
54
54
  include_individual: z
55
55
  .boolean()
56
56
  .optional()
@@ -419,7 +419,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
419
419
  ...(topCorrLine ? ["", topCorrLine] : []),
420
420
  ...(topDivLine ? [topDivLine] : []),
421
421
  ...(klComplexityLine ? [klComplexityLine] : []),
422
- ...(topDivLine || klComplexityLine ? ["(Compare Pearson vs KL: high |r| low KL; use results(action=feature_divergences, metrics=[\"wasserstein\"]) for W1.)"] : []),
422
+ ...(topDivLine || klComplexityLine ? ["(divergence_kl.png is directed KL: cell (i,j)=KL(pᵢ‖pⱼ), upper/lower triangles opposite directions. Compare Pearson vs SKL pair tops; use results(action=feature_divergences, metrics=[\"wasserstein\"]) for W1.)"] : []),
423
423
  ...(siom ? ["", "SIOM layer coverage (distinct from grid dead nodes):",
424
424
  ` Utilization: ${siom.utilization !== undefined ? Number(siom.utilization).toFixed(4) : "N/A"}`,
425
425
  ` SIOM dead fraction: ${siom.dead_fraction !== undefined ? Number(siom.dead_fraction).toFixed(4) : "N/A"}`,
@@ -664,12 +664,16 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
664
664
  }
665
665
  }
666
666
  const savedDir = path.join(folder, jobSubfolder);
667
+ const availCap = 40;
668
+ const availableNote = missing.length > 0 && files.length > 0
669
+ ? ` Available (${Math.min(files.length, availCap)}${files.length > availCap ? ` of ${files.length}` : ""}): ${files.slice(0, availCap).join(", ")}${files.length > availCap ? ", …" : ""}. Use exact names from this list (component planes are component_{i}_{feature}.png, not bare component_1.png).`
670
+ : "";
667
671
  if (saved.length === 0) {
668
672
  const missNote = missing.length > 0 ? ` Missing/failed: ${missing.join(", ")}.` : "";
669
673
  return {
670
674
  content: [{
671
675
  type: "text",
672
- text: `No files saved.${missNote} Check job_id and that the job is completed.`,
676
+ text: `No files saved.${missNote}${availableNote} Check job_id and that the job is completed.`,
673
677
  }],
674
678
  };
675
679
  }
@@ -679,7 +683,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
679
683
  return {
680
684
  content: [{
681
685
  type: "text",
682
- text: `Saved ${saved.length} file(s) to ${savedDir}: ${saved.join(", ")}.${missNote}`,
686
+ text: `Saved ${saved.length} file(s) to ${savedDir}: ${saved.join(", ")}.${missNote}${availableNote}`,
683
687
  }],
684
688
  };
685
689
  }
@@ -707,6 +711,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
707
711
  return { content: [{ type: "text", text: `Recolor job ${newJobId} submitted. Poll with jobs(action=status, job_id="${newJobId}"), then results(action=get, job_id="${newJobId}").` }] };
708
712
  }
709
713
  if (action === "feature_divergences") {
714
+ resetInlineAttachBudget();
710
715
  const body = {
711
716
  metrics: divergenceMetrics ?? ["symmetric_kl", "directed_kl"],
712
717
  output_format: output_format ?? "png",
@@ -721,7 +726,7 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
721
726
  return {
722
727
  content: [{
723
728
  type: "text",
724
- text: `Feature divergences job ${newJobId} submitted (parent ${job_id}). Poll with jobs(action=status, job_id="${newJobId}"), then results(action=get, job_id="${newJobId}").`,
729
+ text: `Feature divergences job ${newJobId} submitted (parent ${job_id}). Poll with jobs(action=status, job_id="${newJobId}"), then results(action=get, job_id="${newJobId}", figures="none").`,
725
730
  }],
726
731
  };
727
732
  }
@@ -730,28 +735,47 @@ NOT FOR: Jobs that haven't completed (use jobs(action=status) first), or tempora
730
735
  const topDivs = sum.top_divergences ?? [];
731
736
  const klComp = sum.feature_kl_complexity ?? [];
732
737
  const topW1 = sum.top_wasserstein ?? null;
738
+ const files = sum.files ?? [];
739
+ const toInline = getFeatureDivergenceImagesToFetch(files, figuresArg);
733
740
  const lines = [
734
741
  `Feature divergences complete — job_id: ${newJobId} (parent training: ${job_id})`,
735
- `Method: ${sum.divergence_method ?? "symmetric_kl_min_shift_sum_norm_denormalized_component_planes"}`,
742
+ `Method: ${sum.divergence_method ?? "kl_min_shift_sum_norm_denormalized_component_planes"}`,
736
743
  `Weight space: ${sum.weight_space ?? "denormalized"}`,
737
744
  `Metrics: ${JSON.stringify(sum.metrics ?? body.metrics)}`,
738
745
  topDivs.length
739
746
  ? `Top symmetric KL pairs: ${topDivs.map((t) => `${t.f1}&${t.f2}=${typeof t.symmetric_kl === "number" ? Number(t.symmetric_kl).toFixed(3) : t.symmetric_kl}`).join(", ")}`
740
747
  : "",
741
748
  klComp.length
742
- ? `KL complexity ranking: ${klComp.map((t) => `${t.feature}=${typeof t.mean_outgoing_kl === "number" ? Number(t.mean_outgoing_kl).toFixed(3) : t.mean_outgoing_kl}`).join(", ")}`
749
+ ? `KL complexity ranking (mean outgoing directed KL): ${klComp.map((t) => `${t.feature}=${typeof t.mean_outgoing_kl === "number" ? Number(t.mean_outgoing_kl).toFixed(3) : t.mean_outgoing_kl}`).join(", ")}`
743
750
  : "",
744
751
  topW1 && topW1.length
745
752
  ? `Top Wasserstein pairs: ${topW1.map((t) => `${t.f1}&${t.f2}=${typeof t.wasserstein === "number" ? Number(t.wasserstein).toFixed(3) : t.wasserstein}`).join(", ")}`
746
753
  : "",
747
- `Artifacts refreshed on parent result (divergence_kl / feature_divergences.json). Compare with Pearson top_correlations from results(get) on the training job.`,
754
+ files.length ? `Artifacts: ${files.join(", ")}` : "",
755
+ toInline.length === 0
756
+ ? `Text-only response (default). Heatmaps: results(download, job_id="${job_id}" or "${newJobId}") or re-call with figures="all" on vision-capable hosts.`
757
+ : "",
758
+ `Artifacts refreshed on parent result (divergence_kl = directed KL heatmap, upper/lower triangles opposite directions; feature_divergences.json has both directed_kl and symmetric_kl). Compare with Pearson top_correlations from results(get) on the training job.`,
748
759
  ].filter(Boolean);
749
760
  const content = [{ type: "text", text: lines.join("\n") }];
750
- const files = sum.files ?? [];
751
- for (const f of files.filter((x) => /\.(png|svg)$/i.test(x))) {
761
+ const inlinedImages = new Set();
762
+ for (const f of toInline) {
752
763
  await tryAttachImage(content, newJobId, f);
764
+ inlinedImages.add(f);
753
765
  }
754
- return { content };
766
+ const structuredPayload = {
767
+ job_id: newJobId,
768
+ parent_job_id: job_id,
769
+ job_type: "feature_divergences",
770
+ metrics: sum.metrics ?? body.metrics,
771
+ top_divergences: topDivs,
772
+ feature_kl_complexity: klComp,
773
+ top_wasserstein: topW1,
774
+ files,
775
+ figures_inlined: [...inlinedImages],
776
+ };
777
+ const summaryText = content.filter((c) => c.type === "text").map((c) => c.text).join("\n\n");
778
+ return structuredTextResult(structuredPayload, summaryText, content);
755
779
  }
756
780
  throw new Error("Invalid action");
757
781
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barivia/barsom-mcp",
3
- "version": "0.23.12",
3
+ "version": "0.23.14",
4
4
  "description": "barSOM MCP proxy — connect any MCP client to the barSOM cloud API for Self-Organizing Map analytics",
5
5
  "keywords": [
6
6
  "mcp",