@barivia/barmesh-mcp 0.8.7 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/shared.js +1 -1
- package/dist/tools/datasets.js +10 -6
- package/dist/tools/jobs.js +11 -4
- package/dist/tools/results.js +2 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as
|
|
2
|
+
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as s}from"@modelcontextprotocol/sdk/server/stdio.js";import{getUiCapability as r,registerAppResource as t,RESOURCE_MIME_TYPE as o}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as n}from"./viz-server.js";import{API_KEY as a,CLIENT_VERSION as i,apiCall as l,apiRawCall as c,loadViewHtml as m,setVizPort as d,setClientSupportsMcpApps as h}from"./shared.js";import{registerGuideTool as u}from"./tools/guide.js";import{registerDatasetsTool as p}from"./tools/datasets.js";import{registerCfdTools as b}from"./tools/cfd.js";import{registerJobsTool as f}from"./tools/jobs.js";import{registerResultsTool as _}from"./tools/results.js";import{registerResultsExplorerTool as y,RESULTS_EXPLORER_URI as v}from"./tools/barmesh_results_explorer.js";import{registerTrainingMonitorTool as g,TRAINING_MONITOR_URI as w}from"./tools/training_monitor.js";import{registerFeedbackTool as j}from"./tools/feedback.js";a||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));(async function(){const a=new e({name:"barmesh",version:i},{instructions:"# Barivia barmesh — CFD mesh-convergence analytics\n\nSOM-based mesh-convergence verification: compare CFD meshes of a refinement study by the\nvolume-weighted distribution their cells form on a shared self-organizing map, plus\nclassical Richardson/GCI on scalar quantities.\n\n## Two tracks\n- barmesh_mesh_convergence: high-dimensional field comparison. Symmetric KL and\n Wasserstein-1 (EMD) distances between each mesh's SOM fingerprint and a reference,\n and between consecutive meshes. Decreasing, plateauing distances toward the finest\n mesh indicate sufficiency. Complements (does not replace) numerical uncertainty analysis.\n- barmesh_richardson: classical grid-convergence index on scalar QoIs.\n\n## Workflow (read-only first)\n1. barmesh_guide_workflow — orient and confirm your plan includes CFD tools.\n2. barmesh_prepare_mesh_data — recipe for the combined per-cell CSV (mesh_id + features + cell volume V).\n3. barmesh_datasets(action=upload) then preview (optional description/tags for inventory).\n4. barmesh_mesh_convergence (and/or barmesh_richardson) — returns a job id.\n5. barmesh_training_monitor(job_id) — visual MCP App with live curves (default); barmesh_jobs(action=monitor) — headless blocking snapshots; or barmesh_jobs(status) for one-shot polls every 10-20s.\n6. barmesh_results(action=get) — distances, convergence reading, and figures; then\n barmesh_results_explorer(job_id) to browse every figure interactively.\n\n## Org inventory recipe\n- Prefer `barmesh_jobs(action=inventory)` for a markdown datasets+jobs overview.\n- Or compose `barmesh_datasets(list)` + `barmesh_jobs(action=list, limit=50)` (page with `cursor`; filter with `status`/`job_type`/`has_results=true` for a results catalog).\n- Cleanup: `barmesh_jobs(action=delete, job_id=…)` + `barmesh_datasets(action=delete)` for known IDs — do not deep-page list to find them.\n- Set `description`+`tags` on upload/submit/update so duplicates stay navigable. List is slim by default (no fat params).\n\n## UI delivery (barmesh_training_monitor, barmesh_results_explorer)\n\nHosts vary: some embed MCP Apps; others list ui:// but never mount them (expected on some Cursor builds). Tool results always lead with a standalone localhost URL and a structured ui_delivery block (delivery, urls, hint_for_agent) — follow hint_for_agent without user prompting.\n\n- Surface the standalone URL as a clickable markdown link. Do not bury it after metrics.\n- Env: BARIVIA_UI_DELIVERY=auto|apps|localhost|inline; BARIVIA_VIZ_PORT pins the viz port across restarts.\n- Standalone pages cross-link training monitor ↔ results explorer for the same job_id.\n\nThese tools are gated by the 'cfd' entitlement; analysis calls return 403 if your plan\ndoes not include it.\n\n## API briefly unavailable (demos)\nIf barmesh_jobs / barmesh_results / barmesh_send_feedback (or any tool) reports the mesh analysis\nAPI temporarily unavailable: say so in one calm line, do not paste HTML/502 pages, and pause\nafter the proxy's burst retries (3 tries ~2s apart; then retry_after_sec≈30). Figures already\ndownloaded stay usable. Keep talking about the last good distances, GCI CSV, and figures —\ndegrade to \"review what we have\", not \"everything failed\". Suggest a later retry for new jobs\nor feedback.\n\n## Outages / feedback\n- barmesh_api_health probes GET /health + /ready (no R2). Use when tools fail with api_unreachable.\n- barmesh_send_feedback queues drafts under ~/.barivia/deferred-feedback when the API/gateway\n is down and flushes them on the next successful call (shared directory with barsom send_feedback).\n- API errors are slim structured lines (error_code, cause, request_id, retry_after_sec) — never raw Cloudflare HTML."});t(a,v,v,{mimeType:o},async()=>{const e=await m("barmesh-results-explorer");return{contents:[{uri:v,mimeType:o,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),t(a,w,w,{mimeType:o},async()=>{const e=await m("barmesh-training-monitor");return{contents:[{uri:w,mimeType:o,text:e??"<html><body>Training Monitor view not built yet. Run: npm run build:views</body></html>"}]}}),u(a),y(a),p(a),b(a),f(a),g(a),_(a),j(a);try{const e=await n(l,c,m);d(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("barmesh viz server failed to start:",e)}const I=a.server;I.oninitialized=()=>{const e=I.getClientCapabilities(),s=r(e);h(!!s?.mimeTypes?.includes(o))};const k=new s;await a.connect(k),console.error(`barmesh-mcp ${i} ready (API: ${process.env.BARIVIA_API_URL??"https://api.barivia.se"})`)})().catch(e=>{console.error("Fatal error starting barmesh-mcp:",e),process.exit(1)});
|
package/dist/shared.js
CHANGED
|
@@ -35,7 +35,7 @@ function newRequestId() {
|
|
|
35
35
|
return randomUUID();
|
|
36
36
|
}
|
|
37
37
|
/** Single source of truth for the proxy version. Keep in sync with package.json on bump. */
|
|
38
|
-
export const CLIENT_VERSION = "0.8.
|
|
38
|
+
export const CLIENT_VERSION = "0.8.8";
|
|
39
39
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
40
40
|
/** Large per-cell CSV uploads may exceed the default fetch timeout. */
|
|
41
41
|
export const UPLOAD_DATASET_TIMEOUT_MS = 180_000;
|
package/dist/tools/datasets.js
CHANGED
|
@@ -7,6 +7,10 @@ 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, awaitDatasetStageIfNeeded, streamFileSha256, putPresignedStream, resolveUploadContentType, suggestMeshDatasetPreview, } from "../shared.js";
|
|
8
8
|
import { GZIP_UPLOAD_HINT } from "../upload_hints.js";
|
|
9
9
|
import { formatDatasetInventoryLine, formatTags } from "../inventory_format.js";
|
|
10
|
+
/** Percent-encode header values so Node fetch accepts Unicode (ByteString limit). */
|
|
11
|
+
function encodeHeaderValue(s) {
|
|
12
|
+
return encodeURIComponent(s);
|
|
13
|
+
}
|
|
10
14
|
/**
|
|
11
15
|
* Normalize a nullable string field from the API. Returns "" for absent values,
|
|
12
16
|
* empty strings, and the literal SQL/serialization sentinels "missing"/"null".
|
|
@@ -169,15 +173,15 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
169
173
|
if (isGzipInput) {
|
|
170
174
|
const gzBytes = await fs.readFile(resolved);
|
|
171
175
|
const gzHeaders = {
|
|
172
|
-
"X-Dataset-Name": name,
|
|
176
|
+
"X-Dataset-Name": encodeHeaderValue(name),
|
|
173
177
|
"Content-Type": uploadContentType,
|
|
174
178
|
"Content-Encoding": "gzip",
|
|
175
179
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(gzBytes).digest("hex"),
|
|
176
180
|
};
|
|
177
181
|
if (description !== undefined)
|
|
178
|
-
gzHeaders["X-Dataset-Description"] = description;
|
|
182
|
+
gzHeaders["X-Dataset-Description"] = encodeHeaderValue(description);
|
|
179
183
|
if (tags !== undefined)
|
|
180
|
-
gzHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
|
|
184
|
+
gzHeaders["X-Dataset-Tags"] = encodeHeaderValue(JSON.stringify(tags));
|
|
181
185
|
const data = await awaitDatasetStageIfNeeded((await apiCall("POST", "/v1/datasets", gzBytes, gzHeaders, UPLOAD_DATASET_TIMEOUT_MS)));
|
|
182
186
|
const gid = data.id ?? data.dataset_id;
|
|
183
187
|
if (gid != null) {
|
|
@@ -195,14 +199,14 @@ ESCALATION: If preview shows a feature column as non-numeric, fix the extraction
|
|
|
195
199
|
}
|
|
196
200
|
const GZIP_THRESHOLD = 1024 * 1024;
|
|
197
201
|
const uploadHeaders = {
|
|
198
|
-
"X-Dataset-Name": name,
|
|
202
|
+
"X-Dataset-Name": encodeHeaderValue(name),
|
|
199
203
|
"Content-Type": uploadContentType,
|
|
200
204
|
"Idempotency-Key": createHash("sha256").update(`${name}\n`).update(body).digest("hex"),
|
|
201
205
|
};
|
|
202
206
|
if (description !== undefined)
|
|
203
|
-
uploadHeaders["X-Dataset-Description"] = description;
|
|
207
|
+
uploadHeaders["X-Dataset-Description"] = encodeHeaderValue(description);
|
|
204
208
|
if (tags !== undefined)
|
|
205
|
-
uploadHeaders["X-Dataset-Tags"] = JSON.stringify(tags);
|
|
209
|
+
uploadHeaders["X-Dataset-Tags"] = encodeHeaderValue(JSON.stringify(tags));
|
|
206
210
|
let uploadBody = body;
|
|
207
211
|
if (Buffer.byteLength(body, "utf-8") > GZIP_THRESHOLD) {
|
|
208
212
|
uploadBody = gzipSync(Buffer.from(body, "utf-8"));
|
package/dist/tools/jobs.js
CHANGED
|
@@ -15,18 +15,19 @@ export function registerJobsTool(server) {
|
|
|
15
15
|
| status | One-shot progress check |
|
|
16
16
|
| list | Slim recent jobs (filter/page with limit/cursor/status/job_type/has_results) |
|
|
17
17
|
| update | Edit label / description / tags |
|
|
18
|
+
| delete | Permanently remove a job and its result files |
|
|
18
19
|
| inventory | Markdown overview of datasets + recent jobs |
|
|
19
20
|
|
|
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.
|
|
21
|
+
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. action=delete for cleanup by known job_id (do not deep-page list to find IDs).
|
|
21
22
|
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).
|
|
22
23
|
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=...).
|
|
23
24
|
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
25
|
ESCALATION: status=failed returns an error message and (when available) a failure_stage; read it before retrying.
|
|
25
26
|
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.`, {
|
|
26
27
|
action: z
|
|
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)"),
|
|
28
|
+
.enum(["status", "monitor", "list", "update", "delete", "inventory"])
|
|
29
|
+
.describe("status: one-shot check; monitor: block until terminal; list: slim paginated jobs; update: metadata; delete: remove job+results; inventory: datasets+jobs overview"),
|
|
30
|
+
job_id: z.string().optional().describe("Job ID (required for status, monitor, update, delete)"),
|
|
30
31
|
block_until_sec: z
|
|
31
32
|
.number()
|
|
32
33
|
.int()
|
|
@@ -123,6 +124,12 @@ UNAVAILABLE: If the mesh analysis API is temporarily unavailable, say so plainly
|
|
|
123
124
|
const data = await apiCall("PATCH", `/v1/jobs/${job_id}`, body);
|
|
124
125
|
return textResult(data);
|
|
125
126
|
}
|
|
127
|
+
if (action === "delete") {
|
|
128
|
+
if (!job_id)
|
|
129
|
+
throw new Error("barmesh_jobs(delete) requires job_id.");
|
|
130
|
+
const data = await apiCall("DELETE", `/v1/jobs/${job_id}`);
|
|
131
|
+
return textResult(data);
|
|
132
|
+
}
|
|
126
133
|
if (action === "inventory") {
|
|
127
134
|
const jobLimit = limit && limit > 0 ? Math.min(Math.floor(limit), 100) : 50;
|
|
128
135
|
const [datasetsRaw, jobsRaw] = await Promise.all([
|
package/dist/tools/results.js
CHANGED
|
@@ -235,7 +235,8 @@ NOT FOR: Submitting jobs.`, {
|
|
|
235
235
|
}
|
|
236
236
|
toDownload = [...new Set(toDownload)];
|
|
237
237
|
let resolvedDir = sandboxPath(folder, await getWorkspaceRootAsync(server));
|
|
238
|
-
const
|
|
238
|
+
const prefix = isRichardsonJob(summary) ? "cfd_richardson" : "cfd_mesh_convergence";
|
|
239
|
+
const jobSubfolder = `${prefix}_${jobLabel ?? job_id}`.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
239
240
|
resolvedDir = path.join(resolvedDir, jobSubfolder);
|
|
240
241
|
await fs.mkdir(resolvedDir, { recursive: true });
|
|
241
242
|
const saved = [];
|