@barivia/barmesh-mcp 0.7.1 → 0.8.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/README.md +21 -13
- package/dist/audit.js +3 -1
- package/dist/deferred_feedback.js +88 -0
- package/dist/figure_sections.js +11 -3
- package/dist/index.js +1 -1
- package/dist/inventory_format.js +69 -0
- package/dist/richardson_results.js +35 -0
- package/dist/shared.js +281 -38
- package/dist/tools/barmesh_results_explorer.js +41 -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 +26 -21
- package/dist/training_monitor_curve.js +42 -0
- 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 +48 -26
- package/dist/viz_links.js +55 -0
- package/package.json +1 -1
package/dist/tools/datasets.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import { registerAuditedTool } from "../audit.js";
|
|
7
7
|
import { apiCall, getWorkspaceRootAsync, resolveFilePathForUpload, textResult, pollUntilComplete, UPLOAD_DATASET_TIMEOUT_MS, LARGE_UPLOAD_BYTES, PRESIGNED_PUT_TIMEOUT_MS, POLL_STAGE_MAX_MS, streamFileSha256, putPresignedStream, resolveUploadContentType, suggestMeshDatasetPreview, } from "../shared.js";
|
|
8
8
|
import { GZIP_UPLOAD_HINT } from "../upload_hints.js";
|
|
9
|
+
import { formatDatasetInventoryLine, formatTags } from "../inventory_format.js";
|
|
9
10
|
/**
|
|
10
11
|
* Normalize a nullable string field from the API. Returns "" for absent values,
|
|
11
12
|
* empty strings, and the literal SQL/serialization sentinels "missing"/"null".
|
|
@@ -23,12 +24,14 @@ Formats: plain CSV/TSV or gzip (.csv.gz / .tsv.gz). For files above ~64 MB, pref
|
|
|
23
24
|
|
|
24
25
|
| Action | Use when |
|
|
25
26
|
|--------|----------|
|
|
26
|
-
| upload | You have prepared a combined per-cell CSV (mesh_id + feature columns + cell volume V). Do this first. |
|
|
27
|
+
| upload | You have prepared a combined per-cell CSV (mesh_id + feature columns + cell volume V). Do this first. Optional description/tags help inventory. |
|
|
27
28
|
| preview | After upload — verify the mesh column, feature columns, and volume column are present and numeric. |
|
|
28
|
-
| get | Fetch one dataset by id — status, staging fields, ingest_error (use after upload or when staging is slow). |
|
|
29
|
-
| list | Find dataset IDs for analysis. |
|
|
29
|
+
| get | Fetch one dataset by id — status, staging fields, ingest_error, description/tags (use after upload or when staging is slow). |
|
|
30
|
+
| list | Find dataset IDs for analysis (includes description/tags). |
|
|
31
|
+
| update | Edit name / description / tags on an existing dataset. |
|
|
30
32
|
| subset | Shrink a huge per-cell table server-side (row_range, filters, or sample_n). |
|
|
31
33
|
| delete | Remove a dataset permanently. |
|
|
34
|
+
| inventory | Prefer barmesh_jobs(action=inventory) for a full datasets+jobs markdown overview. |
|
|
32
35
|
|
|
33
36
|
action=upload: PREFER file_path — server reads from workspace root (token-efficient). Accepts .csv, .tsv, .csv.gz, .tsv.gz. Use csv_data only for small inline pastes (<10KB). If plain CSV exceeds the 5 GB upload cap, gzip it first (.csv.gz).
|
|
34
37
|
|
|
@@ -37,9 +40,11 @@ NOT FOR: Raw OpenFOAM case directories — extract a per-cell CSV first (see bar
|
|
|
37
40
|
COMMON MISTAKES: omitting the cell-volume column (defaults to equal weights, which weakens the fingerprint); inconsistent feature columns across meshes.
|
|
38
41
|
ESCALATION: If preview shows a feature column as non-numeric, fix the extraction and re-upload.`, {
|
|
39
42
|
action: z
|
|
40
|
-
.enum(["upload", "preview", "list", "get", "subset", "delete"])
|
|
41
|
-
.describe("upload: add CSV or .csv.gz; preview: inspect columns; list: see all datasets; get: fetch one dataset metadata (status/staging); subset: create filtered subset; delete: remove dataset"),
|
|
42
|
-
name: z.string().optional().describe("Dataset name (required for upload and subset)"),
|
|
43
|
+
.enum(["upload", "preview", "list", "get", "update", "subset", "delete"])
|
|
44
|
+
.describe("upload: add CSV or .csv.gz; preview: inspect columns; list: see all datasets; get: fetch one dataset metadata (status/staging); update: edit name/description/tags; subset: create filtered subset; delete: remove dataset"),
|
|
45
|
+
name: z.string().optional().describe("Dataset name (required for upload and subset; optional for update)"),
|
|
46
|
+
description: z.string().optional().describe("Optional free-text description (upload/update)"),
|
|
47
|
+
tags: z.array(z.string()).optional().describe("Optional tags for inventory/navigation (upload/update)"),
|
|
43
48
|
file_path: z
|
|
44
49
|
.string()
|
|
45
50
|
.optional()
|
|
@@ -86,7 +91,7 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
86
91
|
.int()
|
|
87
92
|
.optional()
|
|
88
93
|
.describe("action=subset: RNG seed for sample_n (default 42)."),
|
|
89
|
-
}, async ({ action, name, file_path, csv_data, dataset_id, n_rows, row_range, filters, filter, sample_n, sample_seed, }) => {
|
|
94
|
+
}, async ({ action, name, description, tags, file_path, csv_data, dataset_id, n_rows, row_range, filters, filter, sample_n, sample_seed, }) => {
|
|
90
95
|
if (action === "upload") {
|
|
91
96
|
if (!name)
|
|
92
97
|
throw new Error("barmesh_datasets(upload) requires name.");
|
|
@@ -121,7 +126,12 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
121
126
|
const idem = await streamFileSha256(resolved);
|
|
122
127
|
let init;
|
|
123
128
|
try {
|
|
124
|
-
|
|
129
|
+
const uploadUrlBody = { name, size_bytes: stat.size };
|
|
130
|
+
if (description !== undefined)
|
|
131
|
+
uploadUrlBody.description = description;
|
|
132
|
+
if (tags !== undefined)
|
|
133
|
+
uploadUrlBody.tags = tags;
|
|
134
|
+
init = (await apiCall("POST", "/v1/datasets/upload-url", uploadUrlBody, { "Idempotency-Key": idem }));
|
|
125
135
|
}
|
|
126
136
|
catch (e) {
|
|
127
137
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -158,12 +168,17 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
158
168
|
}
|
|
159
169
|
if (isGzipInput) {
|
|
160
170
|
const gzBytes = await fs.readFile(resolved);
|
|
161
|
-
const
|
|
171
|
+
const gzHeaders = {
|
|
162
172
|
"X-Dataset-Name": name,
|
|
163
173
|
"Content-Type": uploadContentType,
|
|
164
174
|
"Content-Encoding": "gzip",
|
|
165
175
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(gzBytes).digest("hex"),
|
|
166
|
-
}
|
|
176
|
+
};
|
|
177
|
+
if (description !== undefined)
|
|
178
|
+
gzHeaders["X-Dataset-Description"] = description;
|
|
179
|
+
if (tags !== undefined)
|
|
180
|
+
gzHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
|
|
181
|
+
const data = (await apiCall("POST", "/v1/datasets", gzBytes, gzHeaders, UPLOAD_DATASET_TIMEOUT_MS));
|
|
167
182
|
const gid = data.id ?? data.dataset_id;
|
|
168
183
|
if (gid != null) {
|
|
169
184
|
data.suggested_next_step = suggestMeshDatasetPreview(String(gid));
|
|
@@ -184,6 +199,10 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
184
199
|
"Content-Type": uploadContentType,
|
|
185
200
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(body).digest("hex"),
|
|
186
201
|
};
|
|
202
|
+
if (description !== undefined)
|
|
203
|
+
uploadHeaders["X-Dataset-Description"] = description;
|
|
204
|
+
if (tags !== undefined)
|
|
205
|
+
uploadHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
|
|
187
206
|
let uploadBody = body;
|
|
188
207
|
if (Buffer.byteLength(body, "utf-8") > GZIP_THRESHOLD) {
|
|
189
208
|
uploadBody = gzipSync(Buffer.from(body, "utf-8"));
|
|
@@ -227,15 +246,9 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
227
246
|
const data = (await apiCall("GET", "/v1/datasets"));
|
|
228
247
|
if (Array.isArray(data)) {
|
|
229
248
|
const lines = data.map((ds) => {
|
|
230
|
-
const
|
|
231
|
-
const dsName = String(ds.name ?? "");
|
|
232
|
-
const rows = ds.rows != null ? Number(ds.rows) : "?";
|
|
233
|
-
const cols = ds.cols != null ? Number(ds.cols) : "?";
|
|
234
|
-
const st = ds.status != null ? String(ds.status) : "ready";
|
|
235
|
-
const statusBit = st !== "ready" ? ` | status=${st}` : "";
|
|
249
|
+
const base = formatDatasetInventoryLine(ds);
|
|
236
250
|
const ingestErr = cleanNullable(ds.ingest_error);
|
|
237
|
-
|
|
238
|
-
return `${dsName} (${id}) — ${rows}×${cols}${statusBit}${err}`;
|
|
251
|
+
return ingestErr ? `${base} | ingest_error=${ingestErr}` : base;
|
|
239
252
|
});
|
|
240
253
|
return { content: [{ type: "text", text: lines.length > 0 ? lines.join("\n") : "No datasets." }] };
|
|
241
254
|
}
|
|
@@ -245,11 +258,14 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
245
258
|
if (!dataset_id)
|
|
246
259
|
throw new Error("barmesh_datasets(get) requires dataset_id.");
|
|
247
260
|
const ds = (await apiCall("GET", `/v1/datasets/${dataset_id}`));
|
|
261
|
+
const tagStr = formatTags(ds.tags);
|
|
248
262
|
const lines = [
|
|
249
263
|
`Dataset: ${ds.name ?? "?"} (${ds.id ?? dataset_id})`,
|
|
250
264
|
`Status: ${ds.status ?? "ready"}`,
|
|
251
265
|
`Rows × cols: ${ds.rows ?? "?"} × ${ds.cols ?? "?"}`,
|
|
252
266
|
ds.size_bytes != null ? `Size: ${Number(ds.size_bytes).toLocaleString()} bytes` : "",
|
|
267
|
+
ds.description != null && String(ds.description).trim() !== "" ? `Description: ${String(ds.description)}` : "",
|
|
268
|
+
tagStr ? `Tags: ${tagStr}` : "",
|
|
253
269
|
ds.staged_prefix != null ? `Staged prefix: ${String(ds.staged_prefix)}` : "",
|
|
254
270
|
ds.staged_version != null ? `Staged version: ${String(ds.staged_version)}` : "",
|
|
255
271
|
ds.stage_job_id != null ? `Stage job: ${String(ds.stage_job_id)} (poll barmesh_jobs(action=status))` : "",
|
|
@@ -258,6 +274,22 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
258
274
|
].filter(Boolean);
|
|
259
275
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
260
276
|
}
|
|
277
|
+
if (action === "update") {
|
|
278
|
+
if (!dataset_id)
|
|
279
|
+
throw new Error("barmesh_datasets(update) requires dataset_id.");
|
|
280
|
+
const body = {};
|
|
281
|
+
if (name !== undefined)
|
|
282
|
+
body.name = name;
|
|
283
|
+
if (description !== undefined)
|
|
284
|
+
body.description = description;
|
|
285
|
+
if (tags !== undefined)
|
|
286
|
+
body.tags = tags;
|
|
287
|
+
if (Object.keys(body).length === 0) {
|
|
288
|
+
throw new Error("barmesh_datasets(update) requires at least one of name, description, tags");
|
|
289
|
+
}
|
|
290
|
+
const data = await apiCall("PATCH", `/v1/datasets/${dataset_id}`, body);
|
|
291
|
+
return textResult(data);
|
|
292
|
+
}
|
|
261
293
|
if (action === "delete") {
|
|
262
294
|
if (!dataset_id)
|
|
263
295
|
throw new Error("barmesh_datasets(delete) requires dataset_id.");
|
package/dist/tools/feedback.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAuditedTool } from "../audit.js";
|
|
3
|
-
import { apiCall, API_URL, fetchWithTimeout,
|
|
3
|
+
import { apiCall, API_URL, fetchWithTimeout, probeApiHealth, structuredTextResult, } from "../shared.js";
|
|
4
|
+
import { enqueueDeferredFeedback, flushDeferredFeedback, listDeferredFeedback, } from "../deferred_feedback.js";
|
|
5
|
+
function isDeferworthy(err) {
|
|
6
|
+
const e = err;
|
|
7
|
+
if (e?.retryable)
|
|
8
|
+
return true;
|
|
9
|
+
if (e?.cause === "api_unreachable" || e?.cause === "storage" || e?.cause === "network")
|
|
10
|
+
return true;
|
|
11
|
+
const status = e?.httpStatus;
|
|
12
|
+
if (status === 502 || status === 503 || status === 504)
|
|
13
|
+
return true;
|
|
14
|
+
if (err instanceof TypeError)
|
|
15
|
+
return true;
|
|
16
|
+
if (err instanceof Error && /fetch failed|ECONNREFUSED|ENOTFOUND|ECONNRESET|network|timed out/i.test(err.message)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
4
21
|
export function registerFeedbackTool(server) {
|
|
5
22
|
registerAuditedTool(server, "barmesh_send_feedback", `Send feedback or feature requests to Barivia developers (CFD mesh-convergence / barmesh workflow). Use when the user has suggestions, ran into issues, or wants something improved. Do NOT call without asking the user first — but after a mesh-convergence or Richardson session, you SHOULD prepare feedback based on the user's workflow or errors encountered, show it to them, and ask for permission to send it. Once they accept, call this tool.
|
|
6
23
|
|
|
7
|
-
For a substantial issue, prefer feedback_items: submit several focused instances (each max 1400 chars) covering, e.g., symptoms, exact reproduction steps, environment, and concrete asks — they are stored together as one batch so developers see the full picture. Use the single feedback field for a short one-off note
|
|
24
|
+
For a substantial issue, prefer feedback_items: submit several focused instances (each max 1400 chars) covering, e.g., symptoms, exact reproduction steps, environment, and concrete asks — they are stored together as one batch so developers see the full picture. Use the single feedback field for a short one-off note.
|
|
25
|
+
|
|
26
|
+
If the API or object storage is unavailable (e.g. gateway HTML 502 mid-demo), this tool queues feedback locally (~/.barivia/deferred-feedback, shared with barsom send_feedback) and returns deferred=true with request_id + api_health from GET /health (no R2). Call again later to flush.`, {
|
|
8
27
|
feedback: z.string().max(1400).optional().describe("Single feedback note (max 1400 characters). Use feedback_items instead for a multi-part report."),
|
|
9
28
|
feedback_items: z.array(z.string().max(1400)).max(10).optional().describe("Several feedback instances (each max 1400 characters, up to 10), stored together as one batch. Prefer this for a detailed issue: split it into focused parts (symptoms, reproduction, environment, asks)."),
|
|
10
29
|
}, async ({ feedback, feedback_items }) => {
|
|
@@ -15,9 +34,19 @@ For a substantial issue, prefer feedback_items: submit several focused instances
|
|
|
15
34
|
throw new Error("barmesh_send_feedback requires feedback or feedback_items (at least one non-empty entry).");
|
|
16
35
|
}
|
|
17
36
|
const body = items.length === 1 ? { feedback: items[0] } : { feedback_items: items };
|
|
37
|
+
const submit = async (b) => apiCall("POST", "/v1/feedback", b);
|
|
38
|
+
const flushed = await flushDeferredFeedback("barmesh", submit).catch(() => null);
|
|
18
39
|
try {
|
|
19
|
-
const data = await
|
|
20
|
-
return
|
|
40
|
+
const data = (await submit(body));
|
|
41
|
+
return structuredTextResult({
|
|
42
|
+
...data,
|
|
43
|
+
deferred: false,
|
|
44
|
+
...(flushed && flushed.flushed > 0
|
|
45
|
+
? { flushed_deferred: flushed.flushed, deferred_remaining: flushed.remaining }
|
|
46
|
+
: {}),
|
|
47
|
+
}, typeof data.message === "string"
|
|
48
|
+
? data.message
|
|
49
|
+
: "Feedback received. Thank you.");
|
|
21
50
|
}
|
|
22
51
|
catch (err) {
|
|
23
52
|
if (err?.httpStatus === 401) {
|
|
@@ -31,10 +60,46 @@ For a substantial issue, prefer feedback_items: submit several focused instances
|
|
|
31
60
|
const text = await resp.text();
|
|
32
61
|
if (resp.ok) {
|
|
33
62
|
const parsed = JSON.parse(text);
|
|
34
|
-
return
|
|
63
|
+
return structuredTextResult({ ...parsed, deferred: false, note: "Feedback sent anonymously (API key invalid or expired)." }, "Feedback sent anonymously (API key invalid or expired).");
|
|
35
64
|
}
|
|
36
65
|
}
|
|
37
|
-
catch { /* fall through
|
|
66
|
+
catch { /* fall through */ }
|
|
67
|
+
}
|
|
68
|
+
if (isDeferworthy(err)) {
|
|
69
|
+
const e = err;
|
|
70
|
+
const queued = await enqueueDeferredFeedback("barmesh", body, {
|
|
71
|
+
status: e.httpStatus,
|
|
72
|
+
error_code: e.errorCode,
|
|
73
|
+
request_id: e.requestId,
|
|
74
|
+
message: e.message,
|
|
75
|
+
});
|
|
76
|
+
const api_health = await probeApiHealth().catch(() => ({
|
|
77
|
+
liveness: "down",
|
|
78
|
+
readiness: "unknown",
|
|
79
|
+
storage: "not_probed",
|
|
80
|
+
}));
|
|
81
|
+
const pending = await listDeferredFeedback("barmesh");
|
|
82
|
+
return structuredTextResult({
|
|
83
|
+
ok: true,
|
|
84
|
+
deferred: true,
|
|
85
|
+
deferred_id: queued.id,
|
|
86
|
+
deferred_path: queued.path,
|
|
87
|
+
deferred_pending: pending.length,
|
|
88
|
+
error_code: e.errorCode ?? "api_unreachable",
|
|
89
|
+
cause: e.cause ?? "api_unreachable",
|
|
90
|
+
request_id: e.requestId ?? null,
|
|
91
|
+
api_health,
|
|
92
|
+
message: "API/storage unavailable — feedback saved locally and will be sent on the next successful barmesh_send_feedback call.",
|
|
93
|
+
}, [
|
|
94
|
+
"Feedback deferred (API/storage unavailable).",
|
|
95
|
+
`Saved to ${queued.path}`,
|
|
96
|
+
e.requestId ? `request_id=${e.requestId}` : null,
|
|
97
|
+
e.errorCode ? `error_code=${e.errorCode}` : null,
|
|
98
|
+
`api_health.liveness=${api_health.liveness} (storage not_probed)`,
|
|
99
|
+
"Retry barmesh_send_feedback later to flush the local queue.",
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(" | "));
|
|
38
103
|
}
|
|
39
104
|
throw err;
|
|
40
105
|
}
|
package/dist/tools/guide.js
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import { registerAuditedTool } from "../audit.js";
|
|
2
|
-
import { apiCall, textResult } from "../shared.js";
|
|
3
|
-
|
|
2
|
+
import { apiCall, probeApiHealth, structuredTextResult, textResult } from "../shared.js";
|
|
3
|
+
import { listDeferredFeedback } from "../deferred_feedback.js";
|
|
4
|
+
const OFFLINE_GUIDE = `Mesh analysis API temporarily unavailable — figures already on disk stay available; new jobs/feedback will wait.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
barmesh offline summary (demo-safe): keep reviewing the last good distances, GCI CSV, and downloaded figures. Say the API is unavailable plainly; the proxy already burst-retried (~3×2s) — wait ~30s before another round. Do not treat the session as failed.
|
|
7
|
+
|
|
8
|
+
Two tracks (when API returns):
|
|
6
9
|
- barmesh_mesh_convergence — SOM volume-fingerprint distances (symmetric KL + Wasserstein-1 vs reference and stepwise)
|
|
7
|
-
- barmesh_richardson — classical Richardson / GCI on scalar QoIs
|
|
10
|
+
- barmesh_richardson — classical Richardson / GCI on scalar QoIs (one mesh family per CSV: uniform OR graded, not mixed)
|
|
8
11
|
|
|
9
|
-
Workflow:
|
|
12
|
+
Workflow (when API returns):
|
|
10
13
|
1. barmesh_prepare_mesh_data — recipe for the combined per-cell CSV (mesh_id + features + cell volume V)
|
|
11
14
|
2. barmesh_datasets(upload) → barmesh_datasets(preview)
|
|
12
15
|
3. barmesh_mesh_convergence and/or barmesh_richardson — returns job_id
|
|
16
|
+
- Multi-topology cavity studies: pass mesh_order interleaved (U20,G20,U40,G40,…) or preset=cavity
|
|
13
17
|
4. barmesh_training_monitor (visual MCP App) or barmesh_jobs(action=monitor) headless — poll until complete
|
|
14
|
-
5. barmesh_results(action=get) → barmesh_results_explorer for figures
|
|
18
|
+
5. barmesh_results(action=get) → barmesh_results_explorer for figures (mesh_convergence) or richardson_gci.csv summary (Richardson)
|
|
15
19
|
|
|
16
|
-
(
|
|
20
|
+
(Set BARIVIA_API_KEY / BARIVIA_API_URL when reconnecting.)`;
|
|
17
21
|
const OFFLINE_PREP = `barmesh mesh-data prep (offline summary; API unreachable):
|
|
18
22
|
Build ONE combined per-cell CSV across all meshes of the refinement study:
|
|
19
23
|
- one row per cell; a mesh label column (mesh_id); the physical channels you want compared (e.g. p, U_mag, k, log_epsilon — log-compress turbulence quantities); and a cell-volume column (V) for fingerprint weighting.
|
|
20
24
|
- keep the SAME feature columns across every mesh; pick the finest mesh as the reference.
|
|
25
|
+
- multi-topology (U + G): pass mesh_order coarse→fine interleaved by refinement (U20,G20,U40,G40,U80,G80,U160,G160) or use preset=cavity.
|
|
21
26
|
Then: barmesh_datasets(upload) -> barmesh_mesh_convergence. (Set BARIVIA_API_KEY / BARIVIA_API_URL.)`;
|
|
22
27
|
export function registerGuideTool(server) {
|
|
23
28
|
registerAuditedTool(server, "barmesh_guide_workflow", `Get the barmesh CFD mesh-convergence workflow and tool map from the API (tier-scoped).
|
|
24
29
|
|
|
25
30
|
BEST FOR: First call in a session — orients you on the two tracks (SOM fingerprint distances and Richardson/GCI), the upload->submit->poll->results flow, and what your plan allows.
|
|
26
31
|
NOT FOR: Step-by-step mesh-data preparation — use barmesh_prepare_mesh_data for that.
|
|
27
|
-
ESCALATION: If the response says your plan does not include CFD tools, the analysis tools will return 403; contact Barivia to enable the cfd entitlement
|
|
32
|
+
ESCALATION: If the response says your plan does not include CFD tools, the analysis tools will return 403; contact Barivia to enable the cfd entitlement.
|
|
33
|
+
UNAVAILABLE: If the API is down, this tool returns a calm offline summary — say so plainly, keep reviewing last good results, and suggest retry later (~30s).`, {}, async () => {
|
|
28
34
|
try {
|
|
29
35
|
const data = (await apiCall("GET", "/v1/cfd/guide"));
|
|
30
36
|
return textResult({
|
|
@@ -43,7 +49,7 @@ ESCALATION: If the response says your plan does not include CFD tools, the analy
|
|
|
43
49
|
|
|
44
50
|
BEST FOR: Before barmesh_datasets(upload) — tells you which physical channels to extract, how to label meshes (mesh_id), the cell-volume column (V), and how to pick the reference mesh.
|
|
45
51
|
NOT FOR: Submitting jobs — after preparing the CSV, use barmesh_datasets(upload) then barmesh_mesh_convergence.
|
|
46
|
-
COMMON MISTAKES: forgetting the per-cell cell-volume column; using different channels across meshes; not log-compressing turbulence quantities (k, epsilon, omega).`, {}, async () => {
|
|
52
|
+
COMMON MISTAKES: forgetting the per-cell cell-volume column; using different channels across meshes; not log-compressing turbulence quantities (k, epsilon, omega); multi-topology U+G studies without mesh_order (use interleaved U20,G20,U40,G40,… or preset=cavity).`, {}, async () => {
|
|
47
53
|
try {
|
|
48
54
|
const data = (await apiCall("GET", "/v1/cfd/prep"));
|
|
49
55
|
return textResult({ recipe: data.recipe, entitled: data.entitled });
|
|
@@ -54,4 +60,22 @@ COMMON MISTAKES: forgetting the per-cell cell-volume column; using different cha
|
|
|
54
60
|
return textResult(OFFLINE_PREP);
|
|
55
61
|
}
|
|
56
62
|
});
|
|
63
|
+
registerAuditedTool(server, "barmesh_api_health", `Probe Barivia API liveness/readiness without touching object storage (R2).
|
|
64
|
+
|
|
65
|
+
BEST FOR: Mid-demo outages — distinguish "API process up" from storage/gateway failures when barmesh_send_feedback or uploads fail.
|
|
66
|
+
Uses GET /health (always 200 if process alive) and GET /ready (DB; Redis best-effort). storage is always not_probed.
|
|
67
|
+
Also reports how many deferred barmesh feedback drafts are queued locally.`, {}, async () => {
|
|
68
|
+
const health = await probeApiHealth();
|
|
69
|
+
const deferred = await listDeferredFeedback("barmesh").catch(() => []);
|
|
70
|
+
const lines = [
|
|
71
|
+
`API liveness: ${health.liveness}`,
|
|
72
|
+
`API readiness: ${health.readiness}`,
|
|
73
|
+
health.db ? `DB: ${health.db}` : null,
|
|
74
|
+
health.redis ? `Redis: ${health.redis}` : null,
|
|
75
|
+
`Object storage (R2): ${health.storage} (intentionally not checked)`,
|
|
76
|
+
health.detail ? `Detail: ${health.detail}` : null,
|
|
77
|
+
`Deferred feedback drafts (barmesh): ${deferred.length}`,
|
|
78
|
+
].filter(Boolean);
|
|
79
|
+
return structuredTextResult({ ...health, deferred_feedback_pending: deferred.length }, lines.join("\n"));
|
|
80
|
+
});
|
|
57
81
|
}
|
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,12 +17,14 @@ 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) {
|
|
21
25
|
registerAppTool(server, "barmesh_training_monitor", {
|
|
22
26
|
title: "Mesh Convergence Training Monitor",
|
|
23
|
-
description: "Default visual monitor for mesh_convergence jobs after submit. Embedded MCP App auto-refreshes every 5s: epoch progress bar, phase, ETA, live QE/TE curves (uniformly subsampled to ≤1000 batch samples per phase), and a per-epoch hit-grid heatmap when available. On already-completed jobs, replays training-log curves in review mode (same entry point). Also exposes a standalone localhost URL — copy it if window.open is blocked in your MCP host. Headless fallback: barmesh_jobs(action=monitor) blocks with compact text snapshots (live or post-hoc review). barmesh_jobs(action=status) remains a one-shot poll. After completion, use barmesh_results_explorer for figures.",
|
|
27
|
+
description: "Default visual monitor for mesh_convergence jobs after submit. Embedded MCP App auto-refreshes every 5s: epoch progress bar, phase, ETA, live QE/TE curves (uniformly subsampled to ≤1000 batch samples per phase; SOM batch-step labels — not FLooP unless job_type is FLooP), panel fingerprint ΔKL curve (mean/max SKL→ref on the TE panel), and a per-epoch hit-grid heatmap when available. On already-completed jobs, replays training-log curves in review mode (same entry point). Also exposes a standalone localhost URL — copy it if window.open is blocked in your MCP host. In MCP App context, use Copy results explorer link when Open results explorer cannot navigate. Headless fallback: barmesh_jobs(action=monitor) blocks with compact text snapshots (live or post-hoc review). barmesh_jobs(action=status) remains a one-shot poll. After completion, use barmesh_results_explorer for figures.",
|
|
24
28
|
inputSchema: {
|
|
25
29
|
job_id: z.string().describe("Job ID from barmesh_mesh_convergence or barmesh_richardson"),
|
|
26
30
|
fetch_training_log: z
|
|
@@ -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 } },
|