@barivia/barmesh-mcp 0.7.0 → 0.8.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.
@@ -2,8 +2,9 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { registerAuditedTool } from "../audit.js";
5
- import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, resetInlineAttachBudget, pollUntilComplete, POLL_STAGE_MAX_MS, } from "../shared.js";
5
+ import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, resetInlineAttachBudget, pollUntilComplete, POLL_STAGE_MAX_MS, suggestAfterRender, } from "../shared.js";
6
6
  import { formatConvergenceReading } from "../convergence_reading.js";
7
+ import { formatRichardsonResultsSummary, isRichardsonJob } from "../richardson_results.js";
7
8
  const MESH_DEFAULT_FIGURES = ["combined", "overview_distances", "plot_vol_all_meshes", "plot_vol_steps", "learning_curve"];
8
9
  function formatMeshResultsSummary(jobId, data, summary) {
9
10
  const label = data.label != null && data.label !== "" ? String(data.label) : null;
@@ -39,13 +40,14 @@ const TEXT_ARTIFACTS = [
39
40
  "distances_to_ref.txt",
40
41
  "emd_stepwise.txt",
41
42
  "cfd_metrics.json",
43
+ "richardson_gci.csv",
42
44
  ];
43
45
  export function registerResultsTool(server) {
44
- registerAuditedTool(server, "barmesh_results", `Fetch results of a completed CFD job: distances, convergence reading, and figures.
46
+ registerAuditedTool(server, "barmesh_results", `Fetch results of a completed CFD job: distances, convergence reading, and figures (mesh_convergence) or Richardson/GCI triplets (richardson).
45
47
 
46
48
  | Action | Use when |
47
49
  |--------|----------|
48
- | get | Read the summary (distances per mesh, stepwise, convergence reading) and inline key figures. |
50
+ | get | Read the summary. mesh_convergence: distances + inline figures. richardson: triplet p/GCI table + topology_warning; see richardson_gci.csv. |
49
51
  | image | Download one figure by filename (e.g. KL_ref.png, combined.pdf). |
50
52
  | render | Re-render the figures as publication PDFs (or SVG) on demand — PDFs are NOT generated by default. |
51
53
  | download | Save figures and metrics to a local folder (headless / agent path). |
@@ -54,6 +56,7 @@ BEST FOR: After barmesh_jobs(action=status) shows completed (and finalize finish
54
56
  FIGURES: Default bundle is combined.png, overview_distances.png (all KL/EMD panels), plot_vol_all_meshes.png, plot_vol_steps.png, and learning_curve.png — not redundant per-feature PNGs. action=get inlines the headline set; pass figures="all" for every artifact listed in summary.files, figures="none" for metrics only (recommended for agents).
55
57
  PDFs ON DEMAND: vector PDFs are not produced by default. Use action=render (format=pdf) once, then action=image or action=download.
56
58
  If GET /v1/results returns 404 shortly after status=completed, cfd_finalize may still be rendering — poll barmesh_jobs(status) until finalize_job_id completes.
59
+ UNAVAILABLE: If the mesh analysis API is temporarily unavailable, say so plainly and suggest retry later (~retry_after_sec). Figures already downloaded stay usable.
57
60
  NOT FOR: Submitting jobs.`, {
58
61
  action: z.enum(["get", "image", "render", "download"]).describe("get: summary + figures; image: one file; render: lazy PDF/SVG; download: save to disk"),
59
62
  job_id: z.string().describe("Job ID"),
@@ -84,7 +87,7 @@ NOT FOR: Submitting jobs.`, {
84
87
  status: "completed",
85
88
  render_job_id: renderId,
86
89
  format: fmt,
87
- suggested_next_step: `Figures rendered as ${fmt}. Download with barmesh_results(action=download, job_id="${job_id}") or fetch one file with action=image.`,
90
+ suggested_next_step: suggestAfterRender(job_id, fmt),
88
91
  });
89
92
  }
90
93
  if (action === "image") {
@@ -193,7 +196,10 @@ NOT FOR: Submitting jobs.`, {
193
196
  throw err;
194
197
  }
195
198
  const summary = (data.summary ?? {});
196
- const content = [{ type: "text", text: formatMeshResultsSummary(job_id, data, summary) }];
199
+ const summaryText = isRichardsonJob(summary)
200
+ ? formatRichardsonResultsSummary(job_id, data, summary)
201
+ : formatMeshResultsSummary(job_id, data, summary);
202
+ const content = [{ type: "text", text: summaryText }];
197
203
  if (figures === "none")
198
204
  return { content };
199
205
  const allFiles = summary.files ?? [];
@@ -1,13 +1,15 @@
1
1
  import { z } from "zod";
2
2
  import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
3
3
  import { runMcpToolAudit } from "../audit.js";
4
- import { apiCall, getClientSupportsMcpApps, getVizPort, structuredTextResult, } from "../shared.js";
4
+ import { apiCall, BARMESH_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, structuredTextResult, } from "../shared.js";
5
+ import { appendUiDeliveryContent, resolveUiDelivery, } from "../ui-delivery.js";
5
6
  import { enrichWithTrainingLog, needsTrainingLogEnrichment } from "../training_review.js";
6
7
  import { formatRunConfigTable } from "../run_config.js";
7
8
  export const TRAINING_MONITOR_URI = "ui://barmesh/training-monitor";
8
9
  export const TRAINING_MONITOR_REFRESH_MS = 5000;
9
- function buildStructuredContent(job_id, data) {
10
+ function buildStructuredContent(job_id, data, port) {
10
11
  const id = String(data.id ?? job_id);
12
+ const standaloneUrl = buildStandaloneVizPageUrl(port, BARMESH_VIZ_VIEWS.trainingMonitor, id);
11
13
  return {
12
14
  ...data,
13
15
  type: "barmesh-training-monitor",
@@ -15,6 +17,8 @@ function buildStructuredContent(job_id, data) {
15
17
  id,
16
18
  job_id: id,
17
19
  refresh_interval_ms: TRAINING_MONITOR_REFRESH_MS,
20
+ standaloneUrl,
21
+ relatedUrls: buildVizRelatedUrls(port, id, BARMESH_VIZ_VIEWS),
18
22
  };
19
23
  }
20
24
  export function registerTrainingMonitorTool(server) {
@@ -37,7 +41,7 @@ export function registerTrainingMonitorTool(server) {
37
41
  if (fetch_training_log || needsTrainingLogEnrichment(data)) {
38
42
  data = await enrichWithTrainingLog(job_id, data);
39
43
  }
40
- const structuredContent = buildStructuredContent(job_id, data);
44
+ const structuredContent = buildStructuredContent(job_id, data, getVizPort());
41
45
  const progress = (data.progress ?? 0) * 100;
42
46
  const etaSec = data.training_eta_sec != null ? Number(data.training_eta_sec) : null;
43
47
  const elapsedSec = data.training_elapsed_sec != null ? Number(data.training_elapsed_sec) : null;
@@ -56,24 +60,25 @@ export function registerTrainingMonitorTool(server) {
56
60
  const text = (runConfigNote ? `${runConfigNote}\n` : "") +
57
61
  `Training monitor (visual MCP App, refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s): job ${job_id} — ${jobStatus} (${progress.toFixed(1)}%).${modeNote}${timingNote}`;
58
62
  const content = [{ type: "text", text }];
59
- const port = getVizPort();
60
- const standaloneUrl = port
61
- ? `http://localhost:${port}/viz/barmesh-training-monitor?mode=standalone&job_id=${encodeURIComponent(job_id)}`
62
- : undefined;
63
- if (standaloneUrl) {
64
- content.push({
65
- type: "text",
66
- text: `Standalone training monitor URL (copy if Open is blocked):\n${standaloneUrl}\n` +
67
- `AGENT: surface this URL to the user — use a markdown link or tell them to copy/paste into a browser when the inline panel cannot open localhost. Re-post after the job completes so they can review curves.\n` +
68
- `This localhost port is per MCP session and goes stale if the proxy restarts re-run barmesh_training_monitor for a fresh URL, or set BARIVIA_VIZ_PORT for a persistent port. Health check: http://localhost:${port}/api/health?job_id=${encodeURIComponent(job_id)}`,
69
- });
70
- }
71
- if (!getClientSupportsMcpApps() && standaloneUrl) {
72
- content.push({
73
- type: "text",
74
- text: `Interactive training monitor: [Open training monitor](${standaloneUrl})`,
75
- });
63
+ const uiDelivery = resolveUiDelivery({
64
+ tool: "training_monitor",
65
+ jobId: job_id,
66
+ jobStatus,
67
+ });
68
+ appendUiDeliveryContent(content, uiDelivery, {
69
+ linkLabel: "Open training monitor",
70
+ primaryUrl: structuredContent.standaloneUrl,
71
+ });
72
+ if (jobStatus === "completed" && uiDelivery.urls?.length) {
73
+ const resultsUrl = uiDelivery.urls.find((u) => u.includes("results-explorer"));
74
+ if (resultsUrl) {
75
+ content.push({
76
+ type: "text",
77
+ text: `Job completed — browse figures: [Open results explorer](${resultsUrl})`,
78
+ });
79
+ }
76
80
  }
81
+ structuredContent.ui_delivery = uiDelivery;
77
82
  return {
78
83
  ...structuredTextResult(structuredContent, text, content),
79
84
  _meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
@@ -16,7 +16,7 @@ export function isKernelTrainingComplete(data, status) {
16
16
  return true;
17
17
  return status === "completed";
18
18
  }
19
- export function teCurveLabel(base, _kernelComplete = false) {
19
+ export function teCurveLabel(base) {
20
20
  return `${base} (panel)`;
21
21
  }
22
22
  export function formatCurveSourceNote(data) {
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Client-capability adaptation for barmesh MCP App tools.
3
+ * Mirrors barsom ui-delivery; uses barmesh viz paths via viz_links.
4
+ */
5
+ import { getClientSupportsMcpApps, getVizPort, } from "./shared.js";
6
+ import { buildBarmeshVizStandaloneUrl } from "./viz_links.js";
7
+ export function parseUiDeliveryOverride() {
8
+ const raw = (process.env.BARIVIA_UI_DELIVERY ?? "auto").trim().toLowerCase();
9
+ if (raw === "apps" || raw === "localhost" || raw === "inline" || raw === "auto") {
10
+ return raw;
11
+ }
12
+ return "auto";
13
+ }
14
+ function vizPortPinned() {
15
+ return Boolean(process.env.BARIVIA_VIZ_PORT?.trim());
16
+ }
17
+ function viewForTool(tool) {
18
+ return tool === "training_monitor" ? "barmesh-training-monitor" : "barmesh-results-explorer";
19
+ }
20
+ export function buildStandaloneVizUrl(view, jobId) {
21
+ return buildBarmeshVizStandaloneUrl(viewForTool(view), jobId);
22
+ }
23
+ export function buildHealthUrl(jobId) {
24
+ const port = getVizPort();
25
+ if (!port)
26
+ return undefined;
27
+ const base = `http://localhost:${port}/api/health`;
28
+ return jobId ? `${base}?job_id=${encodeURIComponent(jobId)}` : base;
29
+ }
30
+ function hintForTier(tier, tool, opts) {
31
+ const label = tool === "training_monitor" ? "training monitor" : "results explorer";
32
+ switch (tier) {
33
+ case "embedded":
34
+ return `The ${label} MCP App panel may embed. Always mention the standalone URL — some hosts list ui:// but never mount widgets.`;
35
+ case "localhost":
36
+ return opts.standaloneUrl
37
+ ? `AGENT: Post a clickable markdown link (e.g. "[Open ${label}](${opts.standaloneUrl})") — do not bury it after metrics.`
38
+ : `AGENT: Viz server unavailable — use barmesh_jobs(status) and barmesh_results(get).`;
39
+ case "inline_image":
40
+ return `AGENT: Inline raster figure(s) are attached. Summarize metrics; offer barmesh_results_explorer for more figures.`;
41
+ case "text_only":
42
+ return `AGENT: No embedded panel or localhost viz — use barmesh_jobs(status) and barmesh_results(get).`;
43
+ }
44
+ }
45
+ export function resolveUiDelivery(options) {
46
+ const override = parseUiDeliveryOverride();
47
+ const mcpApps = getClientSupportsMcpApps();
48
+ const port = getVizPort() || null;
49
+ const standaloneUrl = buildStandaloneVizUrl(options.tool, options.jobId);
50
+ let delivery;
51
+ if (override === "apps") {
52
+ delivery = mcpApps ? "embedded" : port ? "localhost" : "text_only";
53
+ }
54
+ else if (override === "localhost") {
55
+ delivery = port ? "localhost" : options.inlineImageAttached ? "inline_image" : "text_only";
56
+ }
57
+ else if (override === "inline") {
58
+ delivery = options.inlineImageAttached ? "inline_image" : "text_only";
59
+ }
60
+ else if (mcpApps) {
61
+ delivery = "embedded";
62
+ }
63
+ else if (port) {
64
+ delivery = "localhost";
65
+ }
66
+ else if (options.inlineImageAttached) {
67
+ delivery = "inline_image";
68
+ }
69
+ else {
70
+ delivery = "text_only";
71
+ }
72
+ const urls = [];
73
+ if (standaloneUrl && (delivery === "embedded" || delivery === "localhost")) {
74
+ urls.push(standaloneUrl);
75
+ }
76
+ const healthUrl = buildHealthUrl(options.jobId);
77
+ if (healthUrl && delivery === "localhost") {
78
+ urls.push(healthUrl);
79
+ }
80
+ const crossLink = options.tool === "training_monitor" && options.jobStatus === "completed"
81
+ ? buildStandaloneVizUrl("results_explorer", options.jobId)
82
+ : options.tool === "results_explorer"
83
+ ? buildStandaloneVizUrl("training_monitor", options.jobId)
84
+ : undefined;
85
+ if (crossLink && !urls.includes(crossLink)) {
86
+ urls.push(crossLink);
87
+ }
88
+ return {
89
+ delivery,
90
+ urls: urls.length > 0 ? urls : undefined,
91
+ hint_for_agent: hintForTier(delivery, options.tool, { standaloneUrl }),
92
+ mcp_apps_supported: mcpApps,
93
+ viz_port: port,
94
+ port_pinned: vizPortPinned(),
95
+ override,
96
+ };
97
+ }
98
+ /** Prepend prominent standalone URL + structured ui_delivery (Apps handshake may lie). */
99
+ export function appendUiDeliveryContent(content, block, options) {
100
+ const primary = options?.primaryUrl ?? block.urls?.[0];
101
+ const label = options?.linkLabel ?? "Open interactive view";
102
+ const prefix = [];
103
+ if (primary) {
104
+ const embedNote = block.delivery === "embedded"
105
+ ? "Embedded panel if your host supports MCP Apps — otherwise open the standalone URL below (expected on some Cursor builds)."
106
+ : "Open the standalone URL below when no inline panel appears.";
107
+ prefix.push({
108
+ type: "text",
109
+ text: `${embedNote}\n` +
110
+ `Standalone URL (copy if Open is blocked):\n${primary}\n` +
111
+ `[${label}](${primary})\n` +
112
+ `${block.hint_for_agent}`,
113
+ });
114
+ }
115
+ prefix.push({
116
+ type: "text",
117
+ text: "```json\n" + JSON.stringify({ ui_delivery: block }, null, 2) + "\n```",
118
+ });
119
+ if (primary && !block.port_pinned && block.delivery !== "text_only") {
120
+ prefix.push({
121
+ type: "text",
122
+ text: "Tip: set BARIVIA_VIZ_PORT in MCP env for a session-stable localhost port; re-run this tool if the page stops updating.",
123
+ });
124
+ }
125
+ content.unshift(...prefix);
126
+ }
127
+ export function getMcpClientDiagnostics() {
128
+ const override = parseUiDeliveryOverride();
129
+ const mcpApps = getClientSupportsMcpApps();
130
+ const port = getVizPort() || null;
131
+ let activeTier = "text_only";
132
+ if (override === "apps") {
133
+ activeTier = mcpApps ? "embedded" : port ? "localhost" : "text_only";
134
+ }
135
+ else if (override === "localhost") {
136
+ activeTier = port ? "localhost" : "text_only";
137
+ }
138
+ else if (override === "inline") {
139
+ activeTier = "inline_image";
140
+ }
141
+ else if (mcpApps) {
142
+ activeTier = "embedded";
143
+ }
144
+ else if (port) {
145
+ activeTier = "localhost";
146
+ }
147
+ return {
148
+ mcp_apps_supported: mcpApps,
149
+ viz_port: port,
150
+ viz_port_pinned: vizPortPinned(),
151
+ ui_delivery_override: override,
152
+ active_delivery_tier: activeTier,
153
+ health_url: buildHealthUrl(),
154
+ };
155
+ }