@moor-sh/mcp 0.23.0 → 0.24.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 +72 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -395,6 +395,13 @@ server.registerTool(
|
|
|
395
395
|
const p = await resolveProject(project);
|
|
396
396
|
const query = no_cache ? "?nocache=true" : "";
|
|
397
397
|
const res = await apiPost(`/api/projects/${p.id}/run${query}`);
|
|
398
|
+
// /run can fail BEFORE opening the SSE stream — resolver validation,
|
|
399
|
+
// drain mode, invalid URL, credential_not_active. Those land as a
|
|
400
|
+
// plain JSON or text body that readSSE walks without matching any
|
|
401
|
+
// event:/data: lines, returning empty everything. Without this guard
|
|
402
|
+
// the tool would silently report "Rebuild complete." on a failed build.
|
|
403
|
+
// Mirrors the existing moor_deploy guard at the /run call site.
|
|
404
|
+
if (!res.ok) throw new Error(`[run] ${await res.text()}`);
|
|
398
405
|
const { logs, error, structuredError } = await readSSE(res);
|
|
399
406
|
// #119: a classified failure (today: source_credential_required) gets
|
|
400
407
|
// returned as isError with a structured payload the agent can branch
|
|
@@ -1116,6 +1123,71 @@ function formatBytes(bytes: number): string {
|
|
|
1116
1123
|
return `${val.toFixed(val < 10 ? 1 : 0)} ${units[i]}`;
|
|
1117
1124
|
}
|
|
1118
1125
|
|
|
1126
|
+
server.registerTool(
|
|
1127
|
+
"moor_project_history",
|
|
1128
|
+
{
|
|
1129
|
+
title: "Project History (stored)",
|
|
1130
|
+
description:
|
|
1131
|
+
"Stored resource history + lifecycle events for one project over a time window — answers 'what was going on with this project around this case?' (NOT live: use moor_project_stats for a current snapshot). Resource samples are taken ~every minute; CPU is averaged across each interval and network/block reported as rates, both computed from raw counters and reset-aware. Events come from the Docker event stream (start/die/oom/kill/restart) and moor's own state changes. Window defaults to the last `hours` (24); pass from_ms/to_ms (epoch ms) for an exact window. A gap warning means events may be incomplete in that window.",
|
|
1132
|
+
inputSchema: z.object({
|
|
1133
|
+
project: z.string().describe("Project name or ID"),
|
|
1134
|
+
hours: z
|
|
1135
|
+
.number()
|
|
1136
|
+
.optional()
|
|
1137
|
+
.describe("Lookback window in hours (default 24). Ignored if from_ms/to_ms are given."),
|
|
1138
|
+
from_ms: z.number().optional().describe("Window start, epoch milliseconds"),
|
|
1139
|
+
to_ms: z.number().optional().describe("Window end, epoch milliseconds"),
|
|
1140
|
+
}),
|
|
1141
|
+
},
|
|
1142
|
+
async ({ project, hours, from_ms, to_ms }) => {
|
|
1143
|
+
const p = await resolveProject(project);
|
|
1144
|
+
const to = to_ms ?? Date.now();
|
|
1145
|
+
const from = from_ms ?? to - (hours ?? 24) * 3_600_000;
|
|
1146
|
+
const res = await apiGet(`/api/projects/${p.id}/stats/history?from=${from}&to=${to}`);
|
|
1147
|
+
if (!res.ok) throw new Error(`Failed: ${res.status} ${await res.text()}`);
|
|
1148
|
+
const h = (await res.json()) as {
|
|
1149
|
+
from_ms: number;
|
|
1150
|
+
to_ms: number;
|
|
1151
|
+
events: Array<{ occurred_at_ms: number; source: string; action: string }>;
|
|
1152
|
+
summary: {
|
|
1153
|
+
sample_count: number;
|
|
1154
|
+
running_sample_count: number;
|
|
1155
|
+
cpu_percent_avg: number | null;
|
|
1156
|
+
cpu_percent_max: number | null;
|
|
1157
|
+
mem_bytes_max: number | null;
|
|
1158
|
+
net_rx_bytes_total: number;
|
|
1159
|
+
net_tx_bytes_total: number;
|
|
1160
|
+
event_counts: Record<string, number>;
|
|
1161
|
+
has_gap: boolean;
|
|
1162
|
+
};
|
|
1163
|
+
};
|
|
1164
|
+
const s = h.summary;
|
|
1165
|
+
const windowH = Math.round(((h.to_ms - h.from_ms) / 3_600_000) * 10) / 10;
|
|
1166
|
+
const lines = [
|
|
1167
|
+
`${p.name} history — window ~${windowH}h${s.has_gap ? " [⚠ event gap recorded: events may be incomplete]" : ""}`,
|
|
1168
|
+
`Samples: ${s.sample_count} total, ${s.running_sample_count} running`,
|
|
1169
|
+
`CPU: avg ${s.cpu_percent_avg ?? "n/a"}% / max ${s.cpu_percent_max ?? "n/a"}%`,
|
|
1170
|
+
`Memory: max ${s.mem_bytes_max !== null ? formatBytes(s.mem_bytes_max) : "n/a"}`,
|
|
1171
|
+
`Network: in ${formatBytes(s.net_rx_bytes_total)} / out ${formatBytes(s.net_tx_bytes_total)}`,
|
|
1172
|
+
];
|
|
1173
|
+
const counts = Object.entries(s.event_counts);
|
|
1174
|
+
if (counts.length > 0) {
|
|
1175
|
+
lines.push(`Events: ${counts.map(([a, n]) => `${a} ${n}`).join(", ")}`);
|
|
1176
|
+
}
|
|
1177
|
+
const recent = h.events.slice(-8);
|
|
1178
|
+
if (recent.length > 0) {
|
|
1179
|
+
lines.push("Recent events:");
|
|
1180
|
+
for (const e of recent) {
|
|
1181
|
+
lines.push(` ${new Date(e.occurred_at_ms).toISOString()} ${e.action} (${e.source})`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (s.sample_count === 0 && h.events.length === 0) {
|
|
1185
|
+
lines.push("(no stored history in this window)");
|
|
1186
|
+
}
|
|
1187
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1188
|
+
},
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1119
1191
|
server.registerTool(
|
|
1120
1192
|
"moor_project_get",
|
|
1121
1193
|
{
|