@barivia/barmesh-mcp 0.7.1 → 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.
- package/dist/audit.js +3 -1
- package/dist/deferred_feedback.js +88 -0
- package/dist/index.js +1 -1
- package/dist/inventory_format.js +69 -0
- package/dist/richardson_results.js +35 -0
- package/dist/shared.js +275 -38
- package/dist/tools/barmesh_results_explorer.js +35 -29
- package/dist/tools/cfd.js +17 -5
- package/dist/tools/datasets.js +50 -18
- package/dist/tools/feedback.js +71 -6
- package/dist/tools/guide.js +33 -9
- package/dist/tools/jobs.js +117 -11
- package/dist/tools/results.js +9 -3
- package/dist/tools/training_monitor.js +25 -20
- package/dist/ui-delivery.js +155 -0
- package/dist/views/src/views/barmesh-results-explorer/index.html +18 -16
- package/dist/views/src/views/barmesh-training-monitor/index.html +36 -24
- package/dist/viz_links.js +55 -0
- package/package.json +1 -1
package/dist/tools/jobs.js
CHANGED
|
@@ -5,17 +5,28 @@ import { pollCfdFinalizeIfPresent, refreshJobAfterFinalize } from "../cfd_finali
|
|
|
5
5
|
import { formatJobStatusText } from "../job_status_format.js";
|
|
6
6
|
import { DEFAULT_BLOCK_UNTIL_SEC, DEFAULT_POLL_INTERVAL_SEC } from "../job_monitor.js";
|
|
7
7
|
import { runBlockingMonitor } from "../blocking_monitor.js";
|
|
8
|
+
import { buildJobsListQuery, extractJobsList, formatDatasetInventoryLine, formatJobInventoryLine, } from "../inventory_format.js";
|
|
8
9
|
export function registerJobsTool(server) {
|
|
9
|
-
registerAuditedTool(server, "barmesh_jobs", `Check job status, block until terminal, or
|
|
10
|
+
registerAuditedTool(server, "barmesh_jobs", `Check job status, block until terminal, list/update jobs, or rebuild an org inventory.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
| Action | Use when |
|
|
13
|
+
|--------|----------|
|
|
14
|
+
| monitor | After async submit — blocks with snapshots until terminal (preferred for agents) |
|
|
15
|
+
| status | One-shot progress check |
|
|
16
|
+
| list | Slim recent jobs (filter/page with limit/cursor/status/job_type/has_results) |
|
|
17
|
+
| update | Edit label / description / tags |
|
|
18
|
+
| inventory | Markdown overview of datasets + recent jobs |
|
|
19
|
+
|
|
20
|
+
BEST FOR: action=monitor after submit (one call, throttled snapshots — preferred for agents). action=status for a single one-shot check. action=inventory for a durable org catalog.
|
|
12
21
|
MONITOR MODES: barmesh_training_monitor — default visual MCP App (live curves, post-hoc review on completed jobs). barmesh_jobs(action=monitor) — headless blocking snapshots for agents (live or post-hoc review; attaches learning_curve.png when already completed).
|
|
13
22
|
ASYNC PROTOCOL: monitor blocks server-side until completed/failed or block_until timeout (default ${DEFAULT_BLOCK_UNTIL_SEC}s, poll every ${DEFAULT_POLL_INTERVAL_SEC}s). status is one-shot; poll every 10-20s manually if not using monitor. When status=completed, call barmesh_results(action=get, job_id=...) then barmesh_results_explorer(job_id=...).
|
|
14
|
-
|
|
23
|
+
LIST: slim by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). has_results=true is the results catalog.
|
|
24
|
+
ESCALATION: status=failed returns an error message and (when available) a failure_stage; read it before retrying.
|
|
25
|
+
UNAVAILABLE: If the mesh analysis API is temporarily unavailable, say so plainly, pause (proxy already burst-retried; wait ~retry_after_sec), and keep reviewing the last good distances/figures — do not treat the demo as failed. Suggest retry later.`, {
|
|
15
26
|
action: z
|
|
16
|
-
.enum(["status", "monitor", "list"])
|
|
17
|
-
.describe("status: one-shot check; monitor: block until terminal
|
|
18
|
-
job_id: z.string().optional().describe("Job ID (required for status
|
|
27
|
+
.enum(["status", "monitor", "list", "update", "inventory"])
|
|
28
|
+
.describe("status: one-shot check; monitor: block until terminal; list: slim paginated jobs; update: metadata; inventory: datasets+jobs overview"),
|
|
29
|
+
job_id: z.string().optional().describe("Job ID (required for status, monitor, update)"),
|
|
19
30
|
block_until_sec: z
|
|
20
31
|
.number()
|
|
21
32
|
.int()
|
|
@@ -32,8 +43,33 @@ ESCALATION: status=failed returns an error message and (when available) a failur
|
|
|
32
43
|
.boolean()
|
|
33
44
|
.optional()
|
|
34
45
|
.describe("action=monitor only: wait for cfd_finalize (default true)"),
|
|
46
|
+
dataset_id: z.string().optional().describe("action=list: filter by dataset ID"),
|
|
47
|
+
status: z
|
|
48
|
+
.enum(["pending", "running", "completed", "failed", "cancelled"])
|
|
49
|
+
.optional()
|
|
50
|
+
.describe("action=list: filter by status"),
|
|
51
|
+
job_type: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("action=list: MCP job_type label (e.g. cfd_mesh_convergence, cfd_richardson)"),
|
|
55
|
+
has_results: z
|
|
56
|
+
.boolean()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("action=list: true = completed jobs with result_ref"),
|
|
59
|
+
limit: z
|
|
60
|
+
.number()
|
|
61
|
+
.int()
|
|
62
|
+
.min(1)
|
|
63
|
+
.max(100)
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("action=list/inventory: page size (default 50)"),
|
|
66
|
+
cursor: z.string().optional().describe("action=list: next_cursor from a prior page"),
|
|
67
|
+
fields: z.enum(["slim", "full"]).optional().describe("action=list: slim (default) or full"),
|
|
68
|
+
label: z.string().optional().describe("action=update: new label"),
|
|
69
|
+
description: z.string().optional().describe("action=update: free-text description"),
|
|
70
|
+
tags: z.array(z.string()).optional().describe("action=update: replace tags"),
|
|
35
71
|
}, async (args) => {
|
|
36
|
-
const { action, job_id, block_until_sec, poll_interval_sec, wait_finalize } = args;
|
|
72
|
+
const { action, job_id, block_until_sec, poll_interval_sec, wait_finalize, dataset_id, status, job_type, has_results, limit, cursor, fields, label, description, tags, } = args;
|
|
37
73
|
if (action === "monitor") {
|
|
38
74
|
if (!job_id)
|
|
39
75
|
throw new Error("barmesh_jobs(monitor) requires job_id.");
|
|
@@ -43,16 +79,86 @@ ESCALATION: status=failed returns an error message and (when available) a failur
|
|
|
43
79
|
if (!job_id)
|
|
44
80
|
throw new Error("barmesh_jobs(status) requires job_id.");
|
|
45
81
|
let data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
46
|
-
const
|
|
82
|
+
const statusVal = String(data.status ?? "");
|
|
47
83
|
let note = null;
|
|
48
|
-
if (
|
|
84
|
+
if (statusVal === "completed" && data.finalize_job_id) {
|
|
49
85
|
({ note } = await pollCfdFinalizeIfPresent(job_id, data));
|
|
50
86
|
data = await refreshJobAfterFinalize(job_id);
|
|
51
87
|
}
|
|
52
88
|
const statusText = await formatJobStatusText(job_id, data);
|
|
53
89
|
return textResult({ ...data, status_text: note ? `${statusText}\n${note}` : statusText });
|
|
54
90
|
}
|
|
55
|
-
|
|
56
|
-
|
|
91
|
+
if (action === "list") {
|
|
92
|
+
const listPath = buildJobsListQuery({
|
|
93
|
+
dataset_id, status, job_type, has_results, limit, cursor, fields,
|
|
94
|
+
});
|
|
95
|
+
const data = await apiCall("GET", listPath);
|
|
96
|
+
const { jobs, next_cursor } = extractJobsList(data);
|
|
97
|
+
const lines = jobs.map((job) => formatJobInventoryLine(job));
|
|
98
|
+
if (next_cursor) {
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push(`next_cursor: ${next_cursor}`);
|
|
101
|
+
lines.push(`Tip: barmesh_jobs(action=list, cursor="${next_cursor}", limit=${limit ?? 50}) for the next page.`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: lines.length > 0 ? lines.join("\n") : "No jobs found.",
|
|
107
|
+
}],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (action === "update") {
|
|
111
|
+
if (!job_id)
|
|
112
|
+
throw new Error("barmesh_jobs(update) requires job_id.");
|
|
113
|
+
const body = {};
|
|
114
|
+
if (label !== undefined)
|
|
115
|
+
body.label = label;
|
|
116
|
+
if (description !== undefined)
|
|
117
|
+
body.description = description;
|
|
118
|
+
if (tags !== undefined)
|
|
119
|
+
body.tags = tags;
|
|
120
|
+
if (Object.keys(body).length === 0) {
|
|
121
|
+
throw new Error("barmesh_jobs(update) requires at least one of label, description, tags");
|
|
122
|
+
}
|
|
123
|
+
const data = await apiCall("PATCH", `/v1/jobs/${job_id}`, body);
|
|
124
|
+
return textResult(data);
|
|
125
|
+
}
|
|
126
|
+
if (action === "inventory") {
|
|
127
|
+
const jobLimit = limit && limit > 0 ? Math.min(Math.floor(limit), 100) : 50;
|
|
128
|
+
const [datasetsRaw, jobsRaw] = await Promise.all([
|
|
129
|
+
apiCall("GET", "/v1/datasets"),
|
|
130
|
+
apiCall("GET", buildJobsListQuery({ limit: jobLimit })),
|
|
131
|
+
]);
|
|
132
|
+
const datasets = Array.isArray(datasetsRaw) ? datasetsRaw : [];
|
|
133
|
+
const { jobs, next_cursor } = extractJobsList(jobsRaw);
|
|
134
|
+
const lines = [
|
|
135
|
+
`# Org inventory (barmesh)`,
|
|
136
|
+
``,
|
|
137
|
+
`## Datasets (${datasets.length})`,
|
|
138
|
+
];
|
|
139
|
+
if (datasets.length === 0) {
|
|
140
|
+
lines.push("(none)");
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
for (const ds of datasets)
|
|
144
|
+
lines.push(`- ${formatDatasetInventoryLine(ds)}`);
|
|
145
|
+
}
|
|
146
|
+
lines.push(``, `## Jobs (latest ${jobs.length}${next_cursor ? ", more available" : ""})`);
|
|
147
|
+
if (jobs.length === 0) {
|
|
148
|
+
lines.push("(none)");
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
for (const job of jobs)
|
|
152
|
+
lines.push(`- ${formatJobInventoryLine(job)}`);
|
|
153
|
+
}
|
|
154
|
+
if (next_cursor) {
|
|
155
|
+
lines.push(``);
|
|
156
|
+
lines.push(`More jobs: barmesh_jobs(action=list, cursor="${next_cursor}", limit=${jobLimit})`);
|
|
157
|
+
}
|
|
158
|
+
lines.push(``);
|
|
159
|
+
lines.push(`Tips: set description/tags on barmesh_datasets(upload|update) and barmesh_mesh_convergence / barmesh_jobs(update).`);
|
|
160
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
161
|
+
}
|
|
162
|
+
throw new Error("Invalid action");
|
|
57
163
|
});
|
|
58
164
|
}
|
package/dist/tools/results.js
CHANGED
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { registerAuditedTool } from "../audit.js";
|
|
5
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
|
|
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"),
|
|
@@ -193,7 +196,10 @@ NOT FOR: Submitting jobs.`, {
|
|
|
193
196
|
throw err;
|
|
194
197
|
}
|
|
195
198
|
const summary = (data.summary ?? {});
|
|
196
|
-
const
|
|
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,
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 } },
|
|
@@ -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
|
+
}
|