@moor-sh/mcp 0.11.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +62 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moor-sh/mcp",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "MCP server for moor - lets AI agents (Claude Code, Cursor, etc.) manage your moor projects via the moor HTTP API.",
5
5
  "license": "MIT",
6
6
  "repository": {
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:buildImage) appends ".git" and a
170
- // branch ref to whatever URL we forward, so a non-http protocol, query string, or
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: "Stop and start a project's container.",
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
  }),
@@ -1264,7 +1265,7 @@ server.registerTool(
1264
1265
  {
1265
1266
  title: "List Project Run History",
1266
1267
  description:
1267
- "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 full output for a single run.",
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).",
1268
1269
  inputSchema: z.object({
1269
1270
  project: z.string().describe("Project name or ID"),
1270
1271
  page: z
@@ -1292,6 +1293,8 @@ server.registerTool(
1292
1293
  duration_ms: number | null;
1293
1294
  stdout_bytes: number;
1294
1295
  stderr_bytes: number;
1296
+ stdout_total_bytes?: number;
1297
+ stderr_total_bytes?: number;
1295
1298
  }>;
1296
1299
  total: number;
1297
1300
  };
@@ -1302,15 +1305,22 @@ server.registerTool(
1302
1305
  }
1303
1306
  const lines: string[] = [];
1304
1307
  lines.push(
1305
- `${p.name}: ${data.runs.length} run(s) on page ${page}, ${data.total} total. Use moor_run_get(run_id) for full output.`,
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).`,
1306
1309
  );
1307
1310
  for (const r of data.runs) {
1308
1311
  const type = deriveRunType(r);
1309
1312
  const status = deriveRunStatus(r);
1310
1313
  const exit = r.exit_code != null ? ` exit=${r.exit_code}` : "";
1311
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;
1312
1322
  lines.push(
1313
- `id=${r.id} ${type} ${status}${exit} dur=${formatMsShort(r.duration_ms)} stdout=${r.stdout_bytes}B stderr=${r.stderr_bytes}B started=${r.started_at}${cmd}`,
1323
+ `id=${r.id} ${type} ${status}${exit} dur=${formatMsShort(r.duration_ms)} stdout=${outTotal}B stderr=${errTotal}B started=${r.started_at}${cmd}`,
1314
1324
  );
1315
1325
  }
1316
1326
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -1352,6 +1362,8 @@ server.registerTool(
1352
1362
  duration_ms: number | null;
1353
1363
  stdout: string | null;
1354
1364
  stderr: string | null;
1365
+ stdout_total_bytes?: number | null;
1366
+ stderr_total_bytes?: number | null;
1355
1367
  };
1356
1368
  const lines: string[] = [];
1357
1369
  const type = deriveRunType(r);
@@ -1361,18 +1373,55 @@ server.registerTool(
1361
1373
  if (r.cron_command) lines.push(`cron_command: ${r.cron_command}`);
1362
1374
  lines.push(`started_at: ${r.started_at}`);
1363
1375
  if (r.finished_at) lines.push(`finished_at: ${r.finished_at}`);
1364
- // runs.stdout/stderr is the full payload (no API-side truncation), so
1365
- // total bytes == stored bytes here. Using TextEncoder for byte length
1366
- // since JS String.length is UTF-16 code units, not UTF-8 bytes.
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.
1367
1381
  const stdoutStr = r.stdout ?? "";
1368
1382
  const stderrStr = r.stderr ?? "";
1369
1383
  const enc = new TextEncoder();
1370
- appendStream(lines, "stdout", stdoutStr, enc.encode(stdoutStr).length, cap);
1371
- appendStream(lines, "stderr", stderrStr, enc.encode(stderrStr).length, cap);
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);
1372
1388
  return { content: [{ type: "text", text: lines.join("\n") }] };
1373
1389
  },
1374
1390
  );
1375
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
+
1376
1425
  server.registerTool(
1377
1426
  "moor_deploy",
1378
1427
  {