@moor-sh/mcp 0.10.0 → 0.12.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/package.json +1 -1
- package/src/index.ts +103 -13
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -166,9 +166,9 @@ function validateGithubRepoUrl(url: string): void {
|
|
|
166
166
|
} catch {
|
|
167
167
|
throw new Error(`github_url is not a valid URL: ${url}`);
|
|
168
168
|
}
|
|
169
|
-
// The downstream build path (apps/api/docker.ts:
|
|
170
|
-
// branch ref to whatever URL we forward, so a non-http protocol, query string,
|
|
171
|
-
// fragment quietly mangles the resulting git remote. Reject those up front.
|
|
169
|
+
// The downstream build path (apps/api/docker.ts:buildImageStreaming) appends ".git"
|
|
170
|
+
// and a branch ref to whatever URL we forward, so a non-http protocol, query string,
|
|
171
|
+
// or fragment quietly mangles the resulting git remote. Reject those up front.
|
|
172
172
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
173
173
|
throw new Error(`github_url must use http or https (got protocol "${parsed.protocol}")`);
|
|
174
174
|
}
|
|
@@ -326,7 +326,7 @@ server.registerTool(
|
|
|
326
326
|
{
|
|
327
327
|
title: "Rebuild Project",
|
|
328
328
|
description:
|
|
329
|
-
"Rebuild a project from source (git pull + docker build) and restart the container. Returns the build output.",
|
|
329
|
+
"Rebuild a project from source (git pull + docker build) and restart the container. Returns the build output when it finishes. While a build is in flight, the most recent moor_runs entry has finished_at=null — call moor_run_get on its id to tail the live output. Use moor_rebuild for code, Dockerfile, or base-image changes. For env vars / resource limits / port / volume / restart-policy changes, or to recover a crashed container from the existing image, use moor_restart — it skips the build and is much faster.",
|
|
330
330
|
inputSchema: z.object({
|
|
331
331
|
project: z.string().describe("Project name or ID"),
|
|
332
332
|
no_cache: z.boolean().optional().default(false).describe("Build without Docker cache"),
|
|
@@ -346,7 +346,8 @@ server.registerTool(
|
|
|
346
346
|
"moor_restart",
|
|
347
347
|
{
|
|
348
348
|
title: "Restart Project",
|
|
349
|
-
description:
|
|
349
|
+
description:
|
|
350
|
+
"Stop and recreate a project's container from its existing image. Does NOT pull from git or rebuild — uses the existing image_tag. Right tool for: applying changed env vars / resource limits / ports / volumes / restart policy, recovering a crashed container, or simply bouncing the process. Wrong tool for: code or Dockerfile changes (use moor_rebuild — those need a new image).",
|
|
350
351
|
inputSchema: z.object({
|
|
351
352
|
project: z.string().describe("Project name or ID"),
|
|
352
353
|
}),
|
|
@@ -660,6 +661,47 @@ server.registerTool(
|
|
|
660
661
|
},
|
|
661
662
|
);
|
|
662
663
|
|
|
664
|
+
server.registerTool(
|
|
665
|
+
"moor_project_stats",
|
|
666
|
+
{
|
|
667
|
+
title: "Project Container Stats (live)",
|
|
668
|
+
description:
|
|
669
|
+
"Live container stats for one project: CPU percent, memory (excluding page cache, same accounting as `docker stats`), network and block I/O totals, PID count. Single Docker stats snapshot — CPU uses the cpu_stats/precpu_stats delta the daemon already includes. Stopped or never-started projects return running=false with zeroed counters (no 404).",
|
|
670
|
+
inputSchema: z.object({
|
|
671
|
+
project: z.string().describe("Project name or ID"),
|
|
672
|
+
}),
|
|
673
|
+
},
|
|
674
|
+
async ({ project }) => {
|
|
675
|
+
const p = await resolveProject(project);
|
|
676
|
+
const res = await apiGet(`/api/projects/${p.id}/container-stats`);
|
|
677
|
+
if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
|
|
678
|
+
const s = (await res.json()) as {
|
|
679
|
+
running: boolean;
|
|
680
|
+
cpu_percent: number;
|
|
681
|
+
memory_bytes: number;
|
|
682
|
+
memory_limit_bytes: number;
|
|
683
|
+
memory_percent: number;
|
|
684
|
+
network_rx_bytes: number;
|
|
685
|
+
network_tx_bytes: number;
|
|
686
|
+
block_read_bytes: number;
|
|
687
|
+
block_write_bytes: number;
|
|
688
|
+
pids: number;
|
|
689
|
+
};
|
|
690
|
+
if (!s.running) {
|
|
691
|
+
return {
|
|
692
|
+
content: [{ type: "text", text: `${p.name}: not running (zeroed counters returned).` }],
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const memLimit = s.memory_limit_bytes > 0 ? formatBytes(s.memory_limit_bytes) : "unlimited";
|
|
696
|
+
const lines = [
|
|
697
|
+
`${p.name}: CPU ${s.cpu_percent}% | Memory ${formatBytes(s.memory_bytes)} / ${memLimit} (${s.memory_percent}%) | PIDs ${s.pids}`,
|
|
698
|
+
`Network: rx ${formatBytes(s.network_rx_bytes)} / tx ${formatBytes(s.network_tx_bytes)}`,
|
|
699
|
+
`Block I/O: read ${formatBytes(s.block_read_bytes)} / write ${formatBytes(s.block_write_bytes)}`,
|
|
700
|
+
];
|
|
701
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
702
|
+
},
|
|
703
|
+
);
|
|
704
|
+
|
|
663
705
|
function formatBytes(bytes: number): string {
|
|
664
706
|
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
665
707
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
@@ -1223,7 +1265,7 @@ server.registerTool(
|
|
|
1223
1265
|
{
|
|
1224
1266
|
title: "List Project Run History",
|
|
1225
1267
|
description:
|
|
1226
|
-
"Paginated list of cron runs and build runs for a project. Returns one compact line per run (id, type, status, exit code, duration, output byte counts, timestamps) — stdout/stderr bodies are NOT included to avoid blowing token budgets on large build outputs. Use moor_run_get(run_id) to fetch the
|
|
1268
|
+
"Paginated list of cron runs and build runs for a project. Returns one compact line per run (id, type, status, exit code, duration, output byte counts, timestamps) — stdout/stderr bodies are NOT included to avoid blowing token budgets on large build outputs. Use moor_run_get(run_id) to fetch the stored output for a single run (cron rows store full output; build/manual rows store at most a 64 KiB tail with the original total bytes recorded separately).",
|
|
1227
1269
|
inputSchema: z.object({
|
|
1228
1270
|
project: z.string().describe("Project name or ID"),
|
|
1229
1271
|
page: z
|
|
@@ -1251,6 +1293,8 @@ server.registerTool(
|
|
|
1251
1293
|
duration_ms: number | null;
|
|
1252
1294
|
stdout_bytes: number;
|
|
1253
1295
|
stderr_bytes: number;
|
|
1296
|
+
stdout_total_bytes?: number;
|
|
1297
|
+
stderr_total_bytes?: number;
|
|
1254
1298
|
}>;
|
|
1255
1299
|
total: number;
|
|
1256
1300
|
};
|
|
@@ -1261,15 +1305,22 @@ server.registerTool(
|
|
|
1261
1305
|
}
|
|
1262
1306
|
const lines: string[] = [];
|
|
1263
1307
|
lines.push(
|
|
1264
|
-
`${p.name}: ${data.runs.length} run(s) on page ${page}, ${data.total} total. Use moor_run_get(run_id) for
|
|
1308
|
+
`${p.name}: ${data.runs.length} run(s) on page ${page}, ${data.total} total. Use moor_run_get(run_id) for stored output (build/manual rows are tail-truncated; total bytes shown below).`,
|
|
1265
1309
|
);
|
|
1266
1310
|
for (const r of data.runs) {
|
|
1267
1311
|
const type = deriveRunType(r);
|
|
1268
1312
|
const status = deriveRunStatus(r);
|
|
1269
1313
|
const exit = r.exit_code != null ? ` exit=${r.exit_code}` : "";
|
|
1270
1314
|
const cmd = r.cron_command ? ` cmd="${r.cron_command}"` : "";
|
|
1315
|
+
// #65: surface "what was emitted" (total) per byte field. For live or
|
|
1316
|
+
// already-truncated build runs total > stored; for crons and historical
|
|
1317
|
+
// build rows they're equal. Showing total is the operationally useful
|
|
1318
|
+
// number — "what did Docker actually produce" — and stays accurate as a
|
|
1319
|
+
// build streams in. Fall back to stdout_bytes if the API is old.
|
|
1320
|
+
const outTotal = r.stdout_total_bytes ?? r.stdout_bytes;
|
|
1321
|
+
const errTotal = r.stderr_total_bytes ?? r.stderr_bytes;
|
|
1271
1322
|
lines.push(
|
|
1272
|
-
`id=${r.id} ${type} ${status}${exit} dur=${formatMsShort(r.duration_ms)} stdout=${
|
|
1323
|
+
`id=${r.id} ${type} ${status}${exit} dur=${formatMsShort(r.duration_ms)} stdout=${outTotal}B stderr=${errTotal}B started=${r.started_at}${cmd}`,
|
|
1273
1324
|
);
|
|
1274
1325
|
}
|
|
1275
1326
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
@@ -1311,6 +1362,8 @@ server.registerTool(
|
|
|
1311
1362
|
duration_ms: number | null;
|
|
1312
1363
|
stdout: string | null;
|
|
1313
1364
|
stderr: string | null;
|
|
1365
|
+
stdout_total_bytes?: number | null;
|
|
1366
|
+
stderr_total_bytes?: number | null;
|
|
1314
1367
|
};
|
|
1315
1368
|
const lines: string[] = [];
|
|
1316
1369
|
const type = deriveRunType(r);
|
|
@@ -1320,18 +1373,55 @@ server.registerTool(
|
|
|
1320
1373
|
if (r.cron_command) lines.push(`cron_command: ${r.cron_command}`);
|
|
1321
1374
|
lines.push(`started_at: ${r.started_at}`);
|
|
1322
1375
|
if (r.finished_at) lines.push(`finished_at: ${r.finished_at}`);
|
|
1323
|
-
// runs.stdout/stderr
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1376
|
+
// #65: runs.stdout/stderr for build runs is a server-side 64 KiB tail
|
|
1377
|
+
// (TAIL_CAP_BYTES). Use stdout_total_bytes / stderr_total_bytes when the
|
|
1378
|
+
// API provides them so appendStream can honestly report "last X of Y".
|
|
1379
|
+
// For cron rows the stored payload IS the full output, and total == stored.
|
|
1380
|
+
// Fall back to encoded length for older APIs that don't return the totals.
|
|
1326
1381
|
const stdoutStr = r.stdout ?? "";
|
|
1327
1382
|
const stderrStr = r.stderr ?? "";
|
|
1328
1383
|
const enc = new TextEncoder();
|
|
1329
|
-
|
|
1330
|
-
|
|
1384
|
+
const stdoutTotal = r.stdout_total_bytes ?? enc.encode(stdoutStr).length;
|
|
1385
|
+
const stderrTotal = r.stderr_total_bytes ?? enc.encode(stderrStr).length;
|
|
1386
|
+
appendStream(lines, "stdout", stdoutStr, stdoutTotal, cap);
|
|
1387
|
+
appendStream(lines, "stderr", stderrStr, stderrTotal, cap);
|
|
1331
1388
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1332
1389
|
},
|
|
1333
1390
|
);
|
|
1334
1391
|
|
|
1392
|
+
server.registerTool(
|
|
1393
|
+
"moor_run_stop",
|
|
1394
|
+
{
|
|
1395
|
+
title: "Stop or Cancel a Run",
|
|
1396
|
+
description:
|
|
1397
|
+
"Stops an active cron run or cancels an active build/pull run (from moor_rebuild / moor_deploy). Closing the connection to the Docker build/pull endpoint aborts the daemon-side job. Cancellation is only valid during the build/pull streaming phase — once the build finishes and container start has begun, the call returns not_cancellable. Returns one of: cancelled, cancelled_cron, not_cancellable, already_finished, not_active, not_found. These are all expected outcomes, not errors — the tool throws only on unexpected server failures.",
|
|
1398
|
+
inputSchema: z.object({
|
|
1399
|
+
run_id: z.number().int().positive().describe("Run ID from moor_runs"),
|
|
1400
|
+
}),
|
|
1401
|
+
},
|
|
1402
|
+
async ({ run_id }) => {
|
|
1403
|
+
const res = await apiPost(`/api/runs/${run_id}/stop`);
|
|
1404
|
+
// The /stop route returns 200 for cancelled/cancelled_cron and 4xx
|
|
1405
|
+
// for the rest of the known result categories (with a result field
|
|
1406
|
+
// either way). All of those are expected outcomes — render them as
|
|
1407
|
+
// content so the agent can react without try/catch. Only surface as
|
|
1408
|
+
// an error if the response doesn't fit the documented shape (server
|
|
1409
|
+
// error, parse failure, etc).
|
|
1410
|
+
let data: { ok?: boolean; result?: string; error?: string };
|
|
1411
|
+
try {
|
|
1412
|
+
data = (await res.json()) as { ok?: boolean; result?: string; error?: string };
|
|
1413
|
+
} catch {
|
|
1414
|
+
throw new Error(`run_id=${run_id} server error: ${res.status} ${res.statusText}`);
|
|
1415
|
+
}
|
|
1416
|
+
if (typeof data.result === "string") {
|
|
1417
|
+
return { content: [{ type: "text", text: `run_id=${run_id} ${data.result}` }] };
|
|
1418
|
+
}
|
|
1419
|
+
throw new Error(
|
|
1420
|
+
`run_id=${run_id} unexpected response: status=${res.status} body=${JSON.stringify(data)}`,
|
|
1421
|
+
);
|
|
1422
|
+
},
|
|
1423
|
+
);
|
|
1424
|
+
|
|
1335
1425
|
server.registerTool(
|
|
1336
1426
|
"moor_deploy",
|
|
1337
1427
|
{
|