@moor-sh/mcp 0.7.0 → 0.8.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 +144 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moor-sh/mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.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
@@ -1038,6 +1038,150 @@ server.registerTool(
1038
1038
  },
1039
1039
  );
1040
1040
 
1041
+ // --- Runs history (#37) ---
1042
+
1043
+ // A runs row can be a cron run, a build/manual run, OR a cron run whose cron
1044
+ // was deleted (cron_id was SET NULL by the FK). The list alone can't tell the
1045
+ // latter two apart, so labels are honest about ambiguity instead of confidently
1046
+ // calling NULL cron_id "build."
1047
+ function deriveRunStatus(row: {
1048
+ finished_at: string | null;
1049
+ exit_code: number | null;
1050
+ }): "running" | "success" | "failed" {
1051
+ if (!row.finished_at) return "running";
1052
+ return row.exit_code === 0 ? "success" : "failed";
1053
+ }
1054
+
1055
+ function deriveRunType(row: { cron_id: number | null; cron_name: string | null }): string {
1056
+ if (row.cron_name) return `cron(${row.cron_name})`;
1057
+ // cron_id IS NULL — could be a genuine build/manual run, or a cron run
1058
+ // whose cron has since been deleted (ON DELETE SET NULL on the FK).
1059
+ return "build_or_manual";
1060
+ }
1061
+
1062
+ function formatMsShort(ms: number | null | undefined): string {
1063
+ if (ms == null) return "—";
1064
+ if (ms < 1000) return `${ms}ms`;
1065
+ const s = Math.floor(ms / 1000);
1066
+ if (s < 60) return `${s}s`;
1067
+ const m = Math.floor(s / 60);
1068
+ return `${m}m${s % 60}s`;
1069
+ }
1070
+
1071
+ server.registerTool(
1072
+ "moor_runs",
1073
+ {
1074
+ title: "List Project Run History",
1075
+ description:
1076
+ "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.",
1077
+ inputSchema: z.object({
1078
+ project: z.string().describe("Project name or ID"),
1079
+ page: z
1080
+ .number()
1081
+ .int()
1082
+ .positive()
1083
+ .optional()
1084
+ .default(1)
1085
+ .describe("Page number (20 runs per page). Default 1."),
1086
+ }),
1087
+ },
1088
+ async ({ project, page }) => {
1089
+ const p = await resolveProject(project);
1090
+ const res = await apiGet(`/api/projects/${p.id}/runs?include_output=false&page=${page}`);
1091
+ if (!res.ok) throw new Error(`Failed: ${await res.text()}`);
1092
+ const data = (await res.json()) as {
1093
+ runs: Array<{
1094
+ id: number;
1095
+ cron_id: number | null;
1096
+ cron_name: string | null;
1097
+ cron_command: string | null;
1098
+ started_at: string;
1099
+ finished_at: string | null;
1100
+ exit_code: number | null;
1101
+ duration_ms: number | null;
1102
+ stdout_bytes: number;
1103
+ stderr_bytes: number;
1104
+ }>;
1105
+ total: number;
1106
+ };
1107
+ if (data.runs.length === 0) {
1108
+ return {
1109
+ content: [{ type: "text", text: `No runs recorded for ${p.name}.` }],
1110
+ };
1111
+ }
1112
+ const lines: string[] = [];
1113
+ lines.push(
1114
+ `${p.name}: ${data.runs.length} run(s) on page ${page}, ${data.total} total. Use moor_run_get(run_id) for full output.`,
1115
+ );
1116
+ for (const r of data.runs) {
1117
+ const type = deriveRunType(r);
1118
+ const status = deriveRunStatus(r);
1119
+ const exit = r.exit_code != null ? ` exit=${r.exit_code}` : "";
1120
+ const cmd = r.cron_command ? ` cmd="${r.cron_command}"` : "";
1121
+ lines.push(
1122
+ `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}`,
1123
+ );
1124
+ }
1125
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1126
+ },
1127
+ );
1128
+
1129
+ server.registerTool(
1130
+ "moor_run_get",
1131
+ {
1132
+ title: "Get Run Detail",
1133
+ description:
1134
+ "Fetch one cron or build run with its stdout and stderr. Output is tail-truncated (default 8 KiB per stream; max 65536) to keep responses under typical agent token limits. Use tail_bytes=0 for metadata-only.",
1135
+ inputSchema: z.object({
1136
+ run_id: z.number().int().positive().describe("Run ID returned by moor_runs"),
1137
+ tail_bytes: z
1138
+ .number()
1139
+ .int()
1140
+ .min(0)
1141
+ .max(65_536)
1142
+ .optional()
1143
+ .describe(
1144
+ "Max bytes of each stream returned inline. Default 8192. Max 65536. Set to 0 for metadata-only.",
1145
+ ),
1146
+ }),
1147
+ },
1148
+ async ({ run_id, tail_bytes }) => {
1149
+ const cap = tail_bytes ?? 8192;
1150
+ const res = await apiGet(`/api/runs/${run_id}`);
1151
+ if (res.status === 404) throw new Error(`run_id ${run_id} not found`);
1152
+ if (!res.ok) throw new Error(`Failed: ${await res.text()}`);
1153
+ const r = (await res.json()) as {
1154
+ id: number;
1155
+ cron_id: number | null;
1156
+ cron_name: string | null;
1157
+ cron_command: string | null;
1158
+ started_at: string;
1159
+ finished_at: string | null;
1160
+ exit_code: number | null;
1161
+ duration_ms: number | null;
1162
+ stdout: string | null;
1163
+ stderr: string | null;
1164
+ };
1165
+ const lines: string[] = [];
1166
+ const type = deriveRunType(r);
1167
+ const status = deriveRunStatus(r);
1168
+ const exit = r.exit_code != null ? ` exit_code=${r.exit_code}` : "";
1169
+ lines.push(`run_id=${r.id} ${type} ${status}${exit} duration=${formatMsShort(r.duration_ms)}`);
1170
+ if (r.cron_command) lines.push(`cron_command: ${r.cron_command}`);
1171
+ lines.push(`started_at: ${r.started_at}`);
1172
+ if (r.finished_at) lines.push(`finished_at: ${r.finished_at}`);
1173
+ // runs.stdout/stderr is the full payload (no API-side truncation), so
1174
+ // total bytes == stored bytes here. Using TextEncoder for byte length
1175
+ // since JS String.length is UTF-16 code units, not UTF-8 bytes.
1176
+ const stdoutStr = r.stdout ?? "";
1177
+ const stderrStr = r.stderr ?? "";
1178
+ const enc = new TextEncoder();
1179
+ appendStream(lines, "stdout", stdoutStr, enc.encode(stdoutStr).length, cap);
1180
+ appendStream(lines, "stderr", stderrStr, enc.encode(stderrStr).length, cap);
1181
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1182
+ },
1183
+ );
1184
+
1041
1185
  server.registerTool(
1042
1186
  "moor_deploy",
1043
1187
  {