@barivia/barmesh-mcp 0.4.1 → 0.5.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/README.md +21 -2
- package/dist/index.js +1 -1
- package/dist/job_status_format.js +89 -0
- package/dist/shared.js +45 -1
- package/dist/tools/barmesh_results_explorer.js +158 -0
- package/dist/tools/jobs.js +4 -9
- package/dist/tools/results.js +32 -7
- package/dist/views/src/views/barmesh-results-explorer/index.html +180 -0
- package/dist/viz-server.js +124 -0
- package/package.json +9 -4
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local HTTP server for the barmesh results-explorer view when the MCP client
|
|
3
|
+
* does not support MCP Apps. Serves built HTML at /viz/:viewName and proxies API
|
|
4
|
+
* data at /api/results/:jobId and /api/results/:jobId/image/:filename.
|
|
5
|
+
* Bind to 127.0.0.1 only; port from BARIVIA_VIZ_PORT or OS-assigned.
|
|
6
|
+
*/
|
|
7
|
+
import http from "node:http";
|
|
8
|
+
const ID_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
9
|
+
const FILENAME_REGEX = /^[a-zA-Z0-9_.-]+$/;
|
|
10
|
+
const ALLOWED_VIEWS = new Set(["barmesh-results-explorer"]);
|
|
11
|
+
const CORS_HEADERS = {
|
|
12
|
+
"Access-Control-Allow-Origin": "*",
|
|
13
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
14
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
15
|
+
};
|
|
16
|
+
/** Start the local viz HTTP server. Returns the port (from BARIVIA_VIZ_PORT or OS-assigned). */
|
|
17
|
+
export function startVizServer(apiCall, apiRawCall, loadViewHtml) {
|
|
18
|
+
const portOverride = process.env.BARIVIA_VIZ_PORT;
|
|
19
|
+
const port = portOverride ? parseInt(portOverride, 10) : 0;
|
|
20
|
+
if (portOverride && (Number.isNaN(port) || port < 0 || port > 65535)) {
|
|
21
|
+
return Promise.reject(new Error(`Invalid BARIVIA_VIZ_PORT: ${portOverride}`));
|
|
22
|
+
}
|
|
23
|
+
let assignedPort = port;
|
|
24
|
+
const portIsPinned = Boolean(portOverride);
|
|
25
|
+
const server = http.createServer(async (req, res) => {
|
|
26
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1`);
|
|
27
|
+
const pathname = url.pathname;
|
|
28
|
+
const send = (status, body, contentType) => {
|
|
29
|
+
res.writeHead(status, { "Content-Type": contentType, ...CORS_HEADERS });
|
|
30
|
+
res.end(body);
|
|
31
|
+
};
|
|
32
|
+
const sendJson = (status, data) => {
|
|
33
|
+
res.writeHead(status, { "Content-Type": "application/json", ...CORS_HEADERS });
|
|
34
|
+
res.end(JSON.stringify(data));
|
|
35
|
+
};
|
|
36
|
+
if (req.method === "OPTIONS") {
|
|
37
|
+
res.writeHead(204, CORS_HEADERS);
|
|
38
|
+
res.end();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const vizMatch = pathname.match(/^\/viz\/([^/]+)$/);
|
|
42
|
+
if (req.method === "GET" && vizMatch) {
|
|
43
|
+
const viewName = vizMatch[1];
|
|
44
|
+
if (!ALLOWED_VIEWS.has(viewName)) {
|
|
45
|
+
send(404, "Not found", "text/plain");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const html = await loadViewHtml(viewName);
|
|
49
|
+
if (!html) {
|
|
50
|
+
send(404, "View not built. Run: npm run build:views", "text/plain");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
send(200, html, "text/html; charset=utf-8");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (req.method === "GET" && pathname === "/api/health") {
|
|
57
|
+
const out = { ok: true, viz_port: assignedPort, port_pinned: portIsPinned };
|
|
58
|
+
const jobId = url.searchParams.get("job_id");
|
|
59
|
+
if (jobId) {
|
|
60
|
+
if (!ID_REGEX.test(jobId)) {
|
|
61
|
+
out.api_reachable = false;
|
|
62
|
+
out.error = "Invalid job_id";
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
try {
|
|
66
|
+
const data = (await apiCall("GET", `/v1/jobs/${jobId}`));
|
|
67
|
+
out.api_reachable = true;
|
|
68
|
+
out.job_status = data.status ?? null;
|
|
69
|
+
out.job_progress = data.progress ?? null;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
out.api_reachable = false;
|
|
73
|
+
out.error = err instanceof Error ? err.message : "Request failed";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
sendJson(200, out);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const resultsMatch = pathname.match(/^\/api\/results\/([^/]+)$/);
|
|
81
|
+
if (req.method === "GET" && resultsMatch) {
|
|
82
|
+
const jobId = resultsMatch[1];
|
|
83
|
+
if (!ID_REGEX.test(jobId)) {
|
|
84
|
+
sendJson(400, { error: "Invalid job_id" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const data = await apiCall("GET", `/v1/results/${jobId}`);
|
|
89
|
+
sendJson(200, data);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
const status = err?.httpStatus ?? 500;
|
|
93
|
+
sendJson(status, { error: err instanceof Error ? err.message : "Request failed" });
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const imageMatch = pathname.match(/^\/api\/results\/([^/]+)\/image\/([^/]+)$/);
|
|
98
|
+
if (req.method === "GET" && imageMatch) {
|
|
99
|
+
const [, jobId, filename] = imageMatch;
|
|
100
|
+
if (!ID_REGEX.test(jobId) || !FILENAME_REGEX.test(filename)) {
|
|
101
|
+
sendJson(400, { error: "Invalid job_id or filename" });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const { data, contentType } = await apiRawCall(`/v1/results/${jobId}/image/${filename}`);
|
|
106
|
+
res.writeHead(200, { "Content-Type": contentType, ...CORS_HEADERS });
|
|
107
|
+
res.end(data);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
sendJson(404, { error: "Image not found" });
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
send(404, "Not found", "text/plain");
|
|
115
|
+
});
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
server.listen(port, "127.0.0.1", () => {
|
|
118
|
+
const assigned = server.address()?.port;
|
|
119
|
+
assignedPort = assigned ?? port;
|
|
120
|
+
resolve(assignedPort);
|
|
121
|
+
});
|
|
122
|
+
server.on("error", reject);
|
|
123
|
+
});
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barivia/barmesh-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "barmesh MCP proxy — SOM-based CFD mesh-convergence and Richardson/GCI analysis on the Barivia cloud API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -30,17 +30,20 @@
|
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"LICENSE",
|
|
33
|
-
"dist/**/*.js"
|
|
33
|
+
"dist/**/*.js",
|
|
34
|
+
"dist/views/**/*.html"
|
|
34
35
|
],
|
|
35
36
|
"scripts": {
|
|
36
37
|
"minify": "terser dist/index.js -o dist/index.js -c -m --toplevel",
|
|
37
|
-
"build": "tsc && npm run minify",
|
|
38
|
-
"build:publish": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.publish.json && npm run minify",
|
|
38
|
+
"build": "tsc && npm run build:views && npm run minify",
|
|
39
|
+
"build:publish": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.publish.json && npm run build:views && npm run minify",
|
|
40
|
+
"build:views": "INPUT=src/views/barmesh-results-explorer/index.html vite build",
|
|
39
41
|
"dev": "tsx src/index.ts",
|
|
40
42
|
"test": "vitest run --config vitest.config.ts",
|
|
41
43
|
"prepublishOnly": "npm run build:publish"
|
|
42
44
|
},
|
|
43
45
|
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/ext-apps": "^1.0.1",
|
|
44
47
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
45
48
|
"zod": "^3.23.0"
|
|
46
49
|
},
|
|
@@ -49,6 +52,8 @@
|
|
|
49
52
|
"terser": "^5.46.0",
|
|
50
53
|
"tsx": "^4.19.0",
|
|
51
54
|
"typescript": "^5.5.0",
|
|
55
|
+
"vite": "^7.3.1",
|
|
56
|
+
"vite-plugin-singlefile": "^2.3.0",
|
|
52
57
|
"vitest": "^4.0.18"
|
|
53
58
|
},
|
|
54
59
|
"engines": {
|