@moor-sh/mcp 0.7.0 → 0.9.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 +188 -6
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -491,7 +491,8 @@ server.registerTool(
|
|
|
491
491
|
"moor_stats",
|
|
492
492
|
{
|
|
493
493
|
title: "Server Stats",
|
|
494
|
-
description:
|
|
494
|
+
description:
|
|
495
|
+
"Get server resource usage: load, memory, root disk, Docker disk by category (images/containers/volumes/build cache) with reclaimable bytes, and container counts. Note: cpu.percent is load-derived (load avg ÷ cores), not instantaneous CPU; use the `load` field for the same signal with explicit naming.",
|
|
495
496
|
},
|
|
496
497
|
async () => {
|
|
497
498
|
const res = await apiGet("/api/server/stats");
|
|
@@ -501,23 +502,60 @@ server.registerTool(
|
|
|
501
502
|
os: string;
|
|
502
503
|
uptime: string;
|
|
503
504
|
cpu: { percent: number; cores: number };
|
|
505
|
+
load?: { one_min: number; cores: number; normalized_percent: number };
|
|
504
506
|
memory: { total: string; used: string; percent: number };
|
|
505
507
|
disk: { total: string; used: string; percent: number };
|
|
506
508
|
containers: { running: number; total: number };
|
|
509
|
+
docker?: {
|
|
510
|
+
images: { bytes: number; reclaimable_bytes: number; count: number; unused_count: number };
|
|
511
|
+
containers: {
|
|
512
|
+
bytes: number;
|
|
513
|
+
reclaimable_bytes: number;
|
|
514
|
+
count: number;
|
|
515
|
+
stopped_count: number;
|
|
516
|
+
};
|
|
517
|
+
volumes: { bytes: number; reclaimable_bytes: number; count: number; unused_count: number };
|
|
518
|
+
build_cache: { bytes: number; reclaimable_bytes: number; count: number };
|
|
519
|
+
} | null;
|
|
507
520
|
};
|
|
508
|
-
const
|
|
521
|
+
const lines = [
|
|
509
522
|
`Host: ${s.hostname}`,
|
|
510
523
|
`OS: ${s.os}`,
|
|
511
524
|
`Uptime: ${s.uptime}`,
|
|
512
|
-
`CPU: ${s.cpu.percent}% (${s.cpu.cores} cores)`,
|
|
525
|
+
`CPU: ${s.cpu.percent}% (${s.cpu.cores} cores) — load-derived, not instantaneous`,
|
|
526
|
+
];
|
|
527
|
+
if (s.load) {
|
|
528
|
+
lines.push(
|
|
529
|
+
`Load (1m): ${s.load.one_min.toFixed(2)} on ${s.load.cores} cores (${s.load.normalized_percent}%)`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
lines.push(
|
|
513
533
|
`Memory: ${s.memory.used} / ${s.memory.total} (${s.memory.percent}%)`,
|
|
514
|
-
`Disk: ${s.disk.used} / ${s.disk.total} (${s.disk.percent}%)`,
|
|
534
|
+
`Disk (root /): ${s.disk.used} / ${s.disk.total} (${s.disk.percent}%)`,
|
|
515
535
|
`Containers: ${s.containers.running} running / ${s.containers.total} total`,
|
|
516
|
-
|
|
517
|
-
|
|
536
|
+
);
|
|
537
|
+
if (s.docker) {
|
|
538
|
+
const d = s.docker;
|
|
539
|
+
lines.push(
|
|
540
|
+
"Docker disk:",
|
|
541
|
+
` Images: ${formatBytes(d.images.bytes)} (${formatBytes(d.images.reclaimable_bytes)} reclaimable, ${d.images.unused_count}/${d.images.count} unused)`,
|
|
542
|
+
` Containers: ${formatBytes(d.containers.bytes)} (${formatBytes(d.containers.reclaimable_bytes)} reclaimable, ${d.containers.stopped_count}/${d.containers.count} stopped)`,
|
|
543
|
+
` Volumes: ${formatBytes(d.volumes.bytes)} (${formatBytes(d.volumes.reclaimable_bytes)} reclaimable, ${d.volumes.unused_count}/${d.volumes.count} unused)`,
|
|
544
|
+
` Build cache: ${formatBytes(d.build_cache.bytes)} (${formatBytes(d.build_cache.reclaimable_bytes)} reclaimable, ${d.build_cache.count} entries)`,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
518
548
|
},
|
|
519
549
|
);
|
|
520
550
|
|
|
551
|
+
function formatBytes(bytes: number): string {
|
|
552
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
553
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
554
|
+
const i = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
555
|
+
const val = bytes / 1024 ** i;
|
|
556
|
+
return `${val.toFixed(val < 10 ? 1 : 0)} ${units[i]}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
521
559
|
server.registerTool(
|
|
522
560
|
"moor_project_get",
|
|
523
561
|
{
|
|
@@ -1038,6 +1076,150 @@ server.registerTool(
|
|
|
1038
1076
|
},
|
|
1039
1077
|
);
|
|
1040
1078
|
|
|
1079
|
+
// --- Runs history (#37) ---
|
|
1080
|
+
|
|
1081
|
+
// A runs row can be a cron run, a build/manual run, OR a cron run whose cron
|
|
1082
|
+
// was deleted (cron_id was SET NULL by the FK). The list alone can't tell the
|
|
1083
|
+
// latter two apart, so labels are honest about ambiguity instead of confidently
|
|
1084
|
+
// calling NULL cron_id "build."
|
|
1085
|
+
function deriveRunStatus(row: {
|
|
1086
|
+
finished_at: string | null;
|
|
1087
|
+
exit_code: number | null;
|
|
1088
|
+
}): "running" | "success" | "failed" {
|
|
1089
|
+
if (!row.finished_at) return "running";
|
|
1090
|
+
return row.exit_code === 0 ? "success" : "failed";
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function deriveRunType(row: { cron_id: number | null; cron_name: string | null }): string {
|
|
1094
|
+
if (row.cron_name) return `cron(${row.cron_name})`;
|
|
1095
|
+
// cron_id IS NULL — could be a genuine build/manual run, or a cron run
|
|
1096
|
+
// whose cron has since been deleted (ON DELETE SET NULL on the FK).
|
|
1097
|
+
return "build_or_manual";
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function formatMsShort(ms: number | null | undefined): string {
|
|
1101
|
+
if (ms == null) return "—";
|
|
1102
|
+
if (ms < 1000) return `${ms}ms`;
|
|
1103
|
+
const s = Math.floor(ms / 1000);
|
|
1104
|
+
if (s < 60) return `${s}s`;
|
|
1105
|
+
const m = Math.floor(s / 60);
|
|
1106
|
+
return `${m}m${s % 60}s`;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
server.registerTool(
|
|
1110
|
+
"moor_runs",
|
|
1111
|
+
{
|
|
1112
|
+
title: "List Project Run History",
|
|
1113
|
+
description:
|
|
1114
|
+
"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.",
|
|
1115
|
+
inputSchema: z.object({
|
|
1116
|
+
project: z.string().describe("Project name or ID"),
|
|
1117
|
+
page: z
|
|
1118
|
+
.number()
|
|
1119
|
+
.int()
|
|
1120
|
+
.positive()
|
|
1121
|
+
.optional()
|
|
1122
|
+
.default(1)
|
|
1123
|
+
.describe("Page number (20 runs per page). Default 1."),
|
|
1124
|
+
}),
|
|
1125
|
+
},
|
|
1126
|
+
async ({ project, page }) => {
|
|
1127
|
+
const p = await resolveProject(project);
|
|
1128
|
+
const res = await apiGet(`/api/projects/${p.id}/runs?include_output=false&page=${page}`);
|
|
1129
|
+
if (!res.ok) throw new Error(`Failed: ${await res.text()}`);
|
|
1130
|
+
const data = (await res.json()) as {
|
|
1131
|
+
runs: Array<{
|
|
1132
|
+
id: number;
|
|
1133
|
+
cron_id: number | null;
|
|
1134
|
+
cron_name: string | null;
|
|
1135
|
+
cron_command: string | null;
|
|
1136
|
+
started_at: string;
|
|
1137
|
+
finished_at: string | null;
|
|
1138
|
+
exit_code: number | null;
|
|
1139
|
+
duration_ms: number | null;
|
|
1140
|
+
stdout_bytes: number;
|
|
1141
|
+
stderr_bytes: number;
|
|
1142
|
+
}>;
|
|
1143
|
+
total: number;
|
|
1144
|
+
};
|
|
1145
|
+
if (data.runs.length === 0) {
|
|
1146
|
+
return {
|
|
1147
|
+
content: [{ type: "text", text: `No runs recorded for ${p.name}.` }],
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
const lines: string[] = [];
|
|
1151
|
+
lines.push(
|
|
1152
|
+
`${p.name}: ${data.runs.length} run(s) on page ${page}, ${data.total} total. Use moor_run_get(run_id) for full output.`,
|
|
1153
|
+
);
|
|
1154
|
+
for (const r of data.runs) {
|
|
1155
|
+
const type = deriveRunType(r);
|
|
1156
|
+
const status = deriveRunStatus(r);
|
|
1157
|
+
const exit = r.exit_code != null ? ` exit=${r.exit_code}` : "";
|
|
1158
|
+
const cmd = r.cron_command ? ` cmd="${r.cron_command}"` : "";
|
|
1159
|
+
lines.push(
|
|
1160
|
+
`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}`,
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1164
|
+
},
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
server.registerTool(
|
|
1168
|
+
"moor_run_get",
|
|
1169
|
+
{
|
|
1170
|
+
title: "Get Run Detail",
|
|
1171
|
+
description:
|
|
1172
|
+
"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.",
|
|
1173
|
+
inputSchema: z.object({
|
|
1174
|
+
run_id: z.number().int().positive().describe("Run ID returned by moor_runs"),
|
|
1175
|
+
tail_bytes: z
|
|
1176
|
+
.number()
|
|
1177
|
+
.int()
|
|
1178
|
+
.min(0)
|
|
1179
|
+
.max(65_536)
|
|
1180
|
+
.optional()
|
|
1181
|
+
.describe(
|
|
1182
|
+
"Max bytes of each stream returned inline. Default 8192. Max 65536. Set to 0 for metadata-only.",
|
|
1183
|
+
),
|
|
1184
|
+
}),
|
|
1185
|
+
},
|
|
1186
|
+
async ({ run_id, tail_bytes }) => {
|
|
1187
|
+
const cap = tail_bytes ?? 8192;
|
|
1188
|
+
const res = await apiGet(`/api/runs/${run_id}`);
|
|
1189
|
+
if (res.status === 404) throw new Error(`run_id ${run_id} not found`);
|
|
1190
|
+
if (!res.ok) throw new Error(`Failed: ${await res.text()}`);
|
|
1191
|
+
const r = (await res.json()) as {
|
|
1192
|
+
id: number;
|
|
1193
|
+
cron_id: number | null;
|
|
1194
|
+
cron_name: string | null;
|
|
1195
|
+
cron_command: string | null;
|
|
1196
|
+
started_at: string;
|
|
1197
|
+
finished_at: string | null;
|
|
1198
|
+
exit_code: number | null;
|
|
1199
|
+
duration_ms: number | null;
|
|
1200
|
+
stdout: string | null;
|
|
1201
|
+
stderr: string | null;
|
|
1202
|
+
};
|
|
1203
|
+
const lines: string[] = [];
|
|
1204
|
+
const type = deriveRunType(r);
|
|
1205
|
+
const status = deriveRunStatus(r);
|
|
1206
|
+
const exit = r.exit_code != null ? ` exit_code=${r.exit_code}` : "";
|
|
1207
|
+
lines.push(`run_id=${r.id} ${type} ${status}${exit} duration=${formatMsShort(r.duration_ms)}`);
|
|
1208
|
+
if (r.cron_command) lines.push(`cron_command: ${r.cron_command}`);
|
|
1209
|
+
lines.push(`started_at: ${r.started_at}`);
|
|
1210
|
+
if (r.finished_at) lines.push(`finished_at: ${r.finished_at}`);
|
|
1211
|
+
// runs.stdout/stderr is the full payload (no API-side truncation), so
|
|
1212
|
+
// total bytes == stored bytes here. Using TextEncoder for byte length
|
|
1213
|
+
// since JS String.length is UTF-16 code units, not UTF-8 bytes.
|
|
1214
|
+
const stdoutStr = r.stdout ?? "";
|
|
1215
|
+
const stderrStr = r.stderr ?? "";
|
|
1216
|
+
const enc = new TextEncoder();
|
|
1217
|
+
appendStream(lines, "stdout", stdoutStr, enc.encode(stdoutStr).length, cap);
|
|
1218
|
+
appendStream(lines, "stderr", stderrStr, enc.encode(stderrStr).length, cap);
|
|
1219
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1220
|
+
},
|
|
1221
|
+
);
|
|
1222
|
+
|
|
1041
1223
|
server.registerTool(
|
|
1042
1224
|
"moor_deploy",
|
|
1043
1225
|
{
|