@moor-sh/mcp 0.13.0 → 0.15.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 +112 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moor-sh/mcp",
3
- "version": "0.13.0",
3
+ "version": "0.15.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
@@ -320,7 +320,8 @@ server.registerTool(
320
320
  "moor_logs",
321
321
  {
322
322
  title: "Get Container Logs",
323
- description: "Get recent logs from a project's container.",
323
+ description:
324
+ "Get recent logs from a project's container. Annotates output with state: ok (container running), exited (container is stopped but Docker still has logs), no_container (project never started), or missing (container_id is set but Docker doesn't have it). Throws only on docker_error (Docker daemon 5xx / unreachable) so an operator can distinguish infrastructure failure from app silence — pre-#74 the tool returned empty logs for all of these.",
324
325
  inputSchema: z.object({
325
326
  project: z.string().describe("Project name or ID"),
326
327
  lines: z.number().optional().default(100).describe("Number of log lines to retrieve"),
@@ -329,11 +330,43 @@ server.registerTool(
329
330
  async ({ project, lines }) => {
330
331
  const p = await resolveProject(project);
331
332
  const res = await apiGet(`/api/projects/${p.id}/logs?tail=${lines}`);
332
- if (!res.ok) throw new Error(`Failed: ${res.status}`);
333
- const data = (await res.json()) as { logs: string };
334
- return {
335
- content: [{ type: "text", text: data.logs || "(no logs)" }],
336
- };
333
+ // 502 = API surfaced a Docker daemon failure. Throw so the agent
334
+ // gets a tool error, not silent empty logs.
335
+ if (res.status === 502) {
336
+ const data = (await res.json().catch(() => ({}))) as { error?: string };
337
+ throw new Error(`Docker error: ${data.error ?? "unknown"}`);
338
+ }
339
+ if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
340
+ const data = (await res.json()) as { logs: string; state?: string };
341
+ switch (data.state) {
342
+ case "no_container":
343
+ return {
344
+ content: [{ type: "text", text: "(project hasn't been started yet — no container)" }],
345
+ };
346
+ case "missing":
347
+ return {
348
+ content: [
349
+ {
350
+ type: "text",
351
+ text: "(container_id was recorded but Docker doesn't have it; moor may need to recreate the project)",
352
+ },
353
+ ],
354
+ };
355
+ case "exited":
356
+ return {
357
+ content: [
358
+ {
359
+ type: "text",
360
+ text: `${data.logs || "(no logs captured)"}\n\n(container is exited; logs above are from before)`,
361
+ },
362
+ ],
363
+ };
364
+ default:
365
+ // "ok" or undefined (older API) — render raw.
366
+ return {
367
+ content: [{ type: "text", text: data.logs || "(no logs)" }],
368
+ };
369
+ }
337
370
  },
338
371
  );
339
372
 
@@ -565,6 +598,79 @@ server.registerTool(
565
598
  },
566
599
  );
567
600
 
601
+ server.registerTool(
602
+ "moor_update_status",
603
+ {
604
+ title: "Update status / preflight",
605
+ description:
606
+ "Report moor's current version + image digest, the latest available digest on GHCR, active in-flight work counts, DB backup recency, and a safe_to_update boolean. update_available is null (not false) when either the local repo_digest or the registry digest is unknown — never lies by comparing across identifier spaces. unsafe_reasons is a human-readable array; render inline rather than re-deriving from booleans. Read-only diagnostic — does NOT perform any update.",
607
+ },
608
+ async () => {
609
+ const res = await apiGet("/api/server/update-status");
610
+ if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
611
+ const s = (await res.json()) as {
612
+ current: {
613
+ version: string;
614
+ image_id: string | null;
615
+ repo_digest: string | null;
616
+ started_at: string;
617
+ };
618
+ available: {
619
+ latest_tag: string;
620
+ latest_digest: string | null;
621
+ update_available: boolean | null;
622
+ registry_error: string | null;
623
+ };
624
+ active_work: {
625
+ builds_in_flight: number;
626
+ execs_in_flight: number;
627
+ crons_in_flight: number;
628
+ terminals_open: number;
629
+ };
630
+ db_backup: {
631
+ last_backup_at: string | null;
632
+ age_seconds: number | null;
633
+ location: string | null;
634
+ };
635
+ safe_to_update: boolean;
636
+ unsafe_reasons: string[];
637
+ recommended_command: string;
638
+ };
639
+ const lines: string[] = [];
640
+ lines.push(`moor ${s.current.version} (image_id: ${s.current.image_id ?? "unknown"})`);
641
+ lines.push(
642
+ `repo_digest: ${s.current.repo_digest ?? "(none — locally built or stale inspect)"}`,
643
+ );
644
+
645
+ if (s.available.update_available === true) {
646
+ lines.push(`update AVAILABLE → latest: ${s.available.latest_digest}`);
647
+ } else if (s.available.update_available === false) {
648
+ lines.push(`up to date (latest: ${s.available.latest_digest})`);
649
+ } else {
650
+ // null — explain WHICH side is unknown.
651
+ const why = s.available.registry_error
652
+ ? `registry unreachable: ${s.available.registry_error}`
653
+ : s.current.repo_digest === null
654
+ ? "no local repo_digest (built locally?)"
655
+ : "comparison unavailable";
656
+ lines.push(`update availability unknown — ${why}`);
657
+ }
658
+
659
+ lines.push(
660
+ `active: builds=${s.active_work.builds_in_flight} execs=${s.active_work.execs_in_flight} crons=${s.active_work.crons_in_flight} terminals=${s.active_work.terminals_open}`,
661
+ );
662
+
663
+ if (s.safe_to_update) {
664
+ lines.push("safe_to_update: YES");
665
+ } else {
666
+ lines.push("safe_to_update: NO");
667
+ for (const r of s.unsafe_reasons) lines.push(` - ${r}`);
668
+ }
669
+ lines.push(`recommended: ${s.recommended_command}`);
670
+ return { content: [{ type: "text", text: lines.join("\n") }] };
671
+ },
672
+ );
673
+
568
674
  server.registerTool(
569
675
  "moor_cleanup_plan",
570
676
  {