@moor-sh/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.
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.4.1",
3
+ "version": "0.5.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
@@ -1042,6 +1042,150 @@ server.registerTool(
1042
1042
  },
1043
1043
  );
1044
1044
 
1045
+ // --- Async exec (#34 Phase B) ---
1046
+
1047
+ server.registerTool(
1048
+ "moor_exec_async",
1049
+ {
1050
+ title: "Start Async Exec",
1051
+ description:
1052
+ "Run a long-lived command inside a project's container, returning immediately with a run_id. Use moor_exec_status to poll for output and exit code; moor_exec_stop to terminate. Bounded by an optional timeout_ms (default 86400000 = 24h; min 60000 = 1 min; max 86400000). The recorded output is tail-truncated to the last 64 KiB per stream; stdout_total_bytes and stderr_total_bytes report the full pre-truncation byte count.",
1053
+ inputSchema: z.object({
1054
+ project: z.string().describe("Project name or ID"),
1055
+ command: z.string().min(1).describe("Shell command to execute"),
1056
+ timeout_ms: z
1057
+ .number()
1058
+ .int()
1059
+ .min(60_000)
1060
+ .max(86_400_000)
1061
+ .optional()
1062
+ .describe(
1063
+ "Safety timeout in milliseconds. When exceeded, the process tree is terminated and the run is marked timed_out. Default 86400000 (24h). Min 60000. Max 86400000.",
1064
+ ),
1065
+ }),
1066
+ },
1067
+ async ({ project, command, timeout_ms }) => {
1068
+ const p = await resolveProject(project);
1069
+ const body: Record<string, unknown> = { command };
1070
+ if (timeout_ms !== undefined) body.timeout_ms = timeout_ms;
1071
+ const res = await apiPost(`/api/projects/${p.id}/exec/async`, body);
1072
+ if (!res.ok) throw new Error(`Failed: ${await res.text()}`);
1073
+ const data = (await res.json()) as { run_id: number };
1074
+ return {
1075
+ content: [
1076
+ {
1077
+ type: "text",
1078
+ text: `Started async exec on ${p.name}. run_id=${data.run_id}. Use moor_exec_status to poll; moor_exec_stop to terminate.`,
1079
+ },
1080
+ ],
1081
+ };
1082
+ },
1083
+ );
1084
+
1085
+ server.registerTool(
1086
+ "moor_exec_status",
1087
+ {
1088
+ title: "Get Async Exec Status",
1089
+ description:
1090
+ "Return the current state of an async exec run: state, exit code (when finished), running tail of stdout/stderr (last 64 KiB each), total bytes seen, duration, and any error message. State is one of: running, exited, stopped, timed_out, error.",
1091
+ inputSchema: z.object({
1092
+ run_id: z.number().int().positive().describe("Run ID returned by moor_exec_async"),
1093
+ }),
1094
+ },
1095
+ async ({ run_id }) => {
1096
+ const res = await apiGet(`/api/exec/${run_id}`);
1097
+ if (res.status === 404) throw new Error(`run_id ${run_id} not found`);
1098
+ if (!res.ok) throw new Error(`Failed: ${await res.text()}`);
1099
+ const data = (await res.json()) as {
1100
+ id: number;
1101
+ state: string;
1102
+ exit_code: number | null;
1103
+ stdout: string;
1104
+ stderr: string;
1105
+ stdout_total_bytes: number;
1106
+ stderr_total_bytes: number;
1107
+ duration_ms: number;
1108
+ command: string;
1109
+ killed_pid: string | null;
1110
+ error_message: string | null;
1111
+ started_at: string;
1112
+ finished_at: string | null;
1113
+ };
1114
+ const lines: string[] = [];
1115
+ lines.push(
1116
+ `run_id=${data.id} state=${data.state} duration=${formatMs(data.duration_ms)}` +
1117
+ (data.exit_code !== null ? ` exit_code=${data.exit_code}` : ""),
1118
+ );
1119
+ lines.push(`command: ${data.command}`);
1120
+ if (data.killed_pid) lines.push(`killed_pid: ${data.killed_pid}`);
1121
+ if (data.error_message) lines.push(`error: ${data.error_message}`);
1122
+ if (data.stdout) {
1123
+ const note =
1124
+ data.stdout_total_bytes > data.stdout.length
1125
+ ? ` (tail of ${data.stdout_total_bytes} bytes)`
1126
+ : "";
1127
+ lines.push(`stdout${note}:`);
1128
+ lines.push(data.stdout);
1129
+ } else if (data.stdout_total_bytes > 0) {
1130
+ lines.push(`stdout_total_bytes=${data.stdout_total_bytes}`);
1131
+ }
1132
+ if (data.stderr) {
1133
+ const note =
1134
+ data.stderr_total_bytes > data.stderr.length
1135
+ ? ` (tail of ${data.stderr_total_bytes} bytes)`
1136
+ : "";
1137
+ lines.push(`stderr${note}:`);
1138
+ lines.push(data.stderr);
1139
+ } else if (data.stderr_total_bytes > 0) {
1140
+ lines.push(`stderr_total_bytes=${data.stderr_total_bytes}`);
1141
+ }
1142
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1143
+ },
1144
+ );
1145
+
1146
+ server.registerTool(
1147
+ "moor_exec_stop",
1148
+ {
1149
+ title: "Stop Async Exec",
1150
+ description:
1151
+ "Terminate a running async exec by run_id. Walks the descendant process tree inside the container and sends SIGTERM then SIGKILL. Always transitions the run to a terminal state: state=stopped on clean termination (all descendants gone), state=error if any descendant survived OR if the kill handle was lost (moor restart, missing pidfile). Stop is NOT retry-safe — the kill script removes the pidfile after every attempt, and reparented survivors are unreachable from the original PID.",
1152
+ inputSchema: z.object({
1153
+ run_id: z.number().int().positive().describe("Run ID returned by moor_exec_async"),
1154
+ }),
1155
+ },
1156
+ async ({ run_id }) => {
1157
+ const res = await apiPost(`/api/exec/${run_id}/stop`);
1158
+ if (res.status === 404) throw new Error(`run_id ${run_id} not found`);
1159
+ const data = (await res.json()) as {
1160
+ ok: boolean;
1161
+ state: string;
1162
+ killed_pid: string | null;
1163
+ live_remaining: number;
1164
+ message: string;
1165
+ };
1166
+ return {
1167
+ content: [
1168
+ {
1169
+ type: "text",
1170
+ text: `run_id=${run_id} state=${data.state} ${data.message}`,
1171
+ },
1172
+ ],
1173
+ };
1174
+ },
1175
+ );
1176
+
1177
+ function formatMs(ms: number): string {
1178
+ if (ms < 1000) return `${ms}ms`;
1179
+ const s = Math.floor(ms / 1000);
1180
+ if (s < 60) return `${s}s`;
1181
+ const m = Math.floor(s / 60);
1182
+ const rs = s % 60;
1183
+ if (m < 60) return `${m}m${rs}s`;
1184
+ const h = Math.floor(m / 60);
1185
+ const rm = m % 60;
1186
+ return `${h}h${rm}m${rs}s`;
1187
+ }
1188
+
1045
1189
  // --- Start ---
1046
1190
 
1047
1191
  const transport = new StdioServerTransport();