@mgsoftwarebv/mg-dashboard-mcp 2.1.1 → 2.2.1
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/dist/index.js +423 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,410 @@ import { createClient } from '@supabase/supabase-js';
|
|
|
6
6
|
import { createHash, randomBytes, createCipheriv, createDecipheriv } from 'crypto';
|
|
7
7
|
import { Client } from 'ssh2';
|
|
8
8
|
|
|
9
|
+
// src/trigger-tools.ts
|
|
10
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
11
|
+
"COMPLETED",
|
|
12
|
+
"FAILED",
|
|
13
|
+
"CRASHED",
|
|
14
|
+
"CANCELED",
|
|
15
|
+
"SYSTEM_FAILURE",
|
|
16
|
+
"INTERRUPTED",
|
|
17
|
+
"EXPIRED"
|
|
18
|
+
]);
|
|
19
|
+
var DEFAULT_TRIGGER_SERVER_ID = "03659d55-e194-400d-b82a-bf6457371ded";
|
|
20
|
+
var TRIGGER_TOOLS = [
|
|
21
|
+
{
|
|
22
|
+
name: "trigger-list",
|
|
23
|
+
description: "List all Trigger.dev projects on the Trigger server. Returns the compose project name, webapp port, and status. Use the project name in other trigger-* tools.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "trigger-runs",
|
|
31
|
+
description: "List recent task runs for a Trigger.dev project. Returns run ID, task name, status, duration, and timestamps.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
project: { type: "string", description: "Compose project name (from trigger-list)" },
|
|
36
|
+
status: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Comma-separated status filter: QUEUED,EXECUTING,COMPLETED,FAILED,CRASHED,CANCELED,SYSTEM_FAILURE"
|
|
39
|
+
},
|
|
40
|
+
taskIdentifier: { type: "string", description: 'Filter by task identifier (e.g. "hello-world")' },
|
|
41
|
+
limit: { type: "number", description: "Max runs to return (default 20, max 100)" }
|
|
42
|
+
},
|
|
43
|
+
required: ["project"]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "trigger-run-detail",
|
|
48
|
+
description: "Get full details of a specific run: status, payload, output, and error stack traces. Use after trigger-runs or trigger-test-task to inspect results.",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
project: { type: "string", description: "Compose project name" },
|
|
53
|
+
runId: { type: "string", description: "Run ID (e.g. run_xxxxx)" }
|
|
54
|
+
},
|
|
55
|
+
required: ["project", "runId"]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "trigger-test-task",
|
|
60
|
+
description: "Trigger a task run and wait for it to complete. Returns the final status, output, or error with stack trace. The main tool for testing trigger tasks from Cursor.",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
project: { type: "string", description: "Compose project name" },
|
|
65
|
+
taskId: { type: "string", description: 'Task identifier (e.g. "hello-world", "execute-pipeline")' },
|
|
66
|
+
payload: { type: "string", description: "JSON payload string to pass to the task (optional)" },
|
|
67
|
+
waitSeconds: { type: "number", description: "Max seconds to wait for completion (default 60, max 300)" }
|
|
68
|
+
},
|
|
69
|
+
required: ["project", "taskId"]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "trigger-cancel-run",
|
|
74
|
+
description: "Cancel a running or queued Trigger.dev task run.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
project: { type: "string", description: "Compose project name" },
|
|
79
|
+
runId: { type: "string", description: "Run ID to cancel (e.g. run_xxxxx)" }
|
|
80
|
+
},
|
|
81
|
+
required: ["project", "runId"]
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "trigger-replay-run",
|
|
86
|
+
description: "Replay a previously failed or completed run with the same payload. Returns the new run ID.",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
project: { type: "string", description: "Compose project name" },
|
|
91
|
+
runId: { type: "string", description: "Run ID to replay (e.g. run_xxxxx)" }
|
|
92
|
+
},
|
|
93
|
+
required: ["project", "runId"]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
var TRIGGER_TOOL_NAMES = new Set(TRIGGER_TOOLS.map((t) => t.name));
|
|
98
|
+
var TRIGGER_TOOL_MODULE_MAP = {
|
|
99
|
+
"trigger-list": "ci_cd",
|
|
100
|
+
"trigger-runs": "ci_cd",
|
|
101
|
+
"trigger-run-detail": "ci_cd",
|
|
102
|
+
"trigger-test-task": "ci_cd",
|
|
103
|
+
"trigger-cancel-run": "ci_cd",
|
|
104
|
+
"trigger-replay-run": "ci_cd"
|
|
105
|
+
};
|
|
106
|
+
async function discoverInstance(project, conn, proxy, sshExec2) {
|
|
107
|
+
const pg = `${project}-postgres-1`;
|
|
108
|
+
const wa = `${project}-webapp-1`;
|
|
109
|
+
const cmd = [
|
|
110
|
+
`PORT=$(docker port "${wa}" 3000/tcp 2>/dev/null | head -1 | sed 's/.*://')`,
|
|
111
|
+
`KEY=$(docker exec "${pg}" psql -U postgres -d main -t -A -c "SELECT \\"apiKey\\" FROM \\"RuntimeEnvironment\\" WHERE slug='prod' LIMIT 1" 2>/dev/null | tr -d '[:space:]')`,
|
|
112
|
+
'echo "$PORT|$KEY"'
|
|
113
|
+
].join(" && ");
|
|
114
|
+
const result = await sshExec2(conn, cmd, proxy);
|
|
115
|
+
const output = result.stdout.trim();
|
|
116
|
+
const sepIdx = output.indexOf("|");
|
|
117
|
+
const port = sepIdx > 0 ? output.substring(0, sepIdx) : "";
|
|
118
|
+
const apiKey2 = sepIdx > 0 ? output.substring(sepIdx + 1) : "";
|
|
119
|
+
if (!port) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Could not find webapp port for ${wa}. Is the container running? Use trigger-list to see available instances.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (!apiKey2) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Could not get API key from ${pg}. Check if the Trigger.dev database is accessible and the RuntimeEnvironment table exists.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return { port, apiKey: apiKey2 };
|
|
130
|
+
}
|
|
131
|
+
async function triggerApi(conn, proxy, sshExec2, instance, method, path, body) {
|
|
132
|
+
const parts = ["curl", "-s", "--max-time", "30"];
|
|
133
|
+
if (method !== "GET") parts.push("-X", method);
|
|
134
|
+
parts.push(`"http://localhost:${instance.port}${path}"`);
|
|
135
|
+
parts.push(`-H "Authorization: Bearer ${instance.apiKey}"`);
|
|
136
|
+
if (body) {
|
|
137
|
+
parts.push('-H "Content-Type: application/json"');
|
|
138
|
+
parts.push(`-d '${body.replace(/'/g, "'\\''")}'`);
|
|
139
|
+
}
|
|
140
|
+
const result = await sshExec2(conn, parts.join(" "), proxy);
|
|
141
|
+
if (result.exitCode !== 0 && !result.stdout) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Trigger.dev API call failed (${method} ${path}): ${result.stderr || `exit code ${result.exitCode}`}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return result.stdout;
|
|
147
|
+
}
|
|
148
|
+
function formatRunsTable(runs) {
|
|
149
|
+
if (runs.length === 0) return "No runs found";
|
|
150
|
+
const lines = runs.map((r) => {
|
|
151
|
+
const duration = r.durationMs != null ? `${(r.durationMs / 1e3).toFixed(1)}s` : "-";
|
|
152
|
+
const test = r.isTest ? " [TEST]" : "";
|
|
153
|
+
const created = r.createdAt ? new Date(r.createdAt).toLocaleString("nl-NL", { timeZone: "Europe/Amsterdam" }) : "";
|
|
154
|
+
return `${r.id} ${r.taskIdentifier.padEnd(35)} ${r.status.padEnd(16)} ${duration.padStart(8)} ${created}${test}`;
|
|
155
|
+
});
|
|
156
|
+
return `${"ID".padEnd(20)} ${"TASK".padEnd(35)} ${"STATUS".padEnd(16)} ${"DURATION".padStart(8)} CREATED
|
|
157
|
+
` + "-".repeat(110) + "\n" + lines.join("\n");
|
|
158
|
+
}
|
|
159
|
+
function formatRunDetail(run) {
|
|
160
|
+
const sections = [];
|
|
161
|
+
sections.push(`=== Run ${run.id} ===`);
|
|
162
|
+
sections.push(`Task: ${run.taskIdentifier}`);
|
|
163
|
+
sections.push(`Status: ${run.status}`);
|
|
164
|
+
sections.push(`Version: ${run.version || "-"}`);
|
|
165
|
+
if (run.startedAt) sections.push(`Started: ${new Date(run.startedAt).toLocaleString("nl-NL", { timeZone: "Europe/Amsterdam" })}`);
|
|
166
|
+
if (run.finishedAt) sections.push(`Finished: ${new Date(run.finishedAt).toLocaleString("nl-NL", { timeZone: "Europe/Amsterdam" })}`);
|
|
167
|
+
if (run.durationMs != null) sections.push(`Duration: ${(run.durationMs / 1e3).toFixed(2)}s`);
|
|
168
|
+
if (run.payload !== void 0) {
|
|
169
|
+
const payloadStr = typeof run.payload === "string" ? run.payload : JSON.stringify(run.payload, null, 2);
|
|
170
|
+
sections.push("", "--- Payload ---", payloadStr);
|
|
171
|
+
}
|
|
172
|
+
if (run.output !== void 0) {
|
|
173
|
+
const outputStr = typeof run.output === "string" ? run.output : JSON.stringify(run.output, null, 2);
|
|
174
|
+
sections.push("", "--- Output ---", outputStr);
|
|
175
|
+
}
|
|
176
|
+
if (run.attempts && run.attempts.length > 0) {
|
|
177
|
+
for (const attempt of run.attempts) {
|
|
178
|
+
if (attempt.error) {
|
|
179
|
+
sections.push(
|
|
180
|
+
"",
|
|
181
|
+
`--- Error (${attempt.id}) ---`,
|
|
182
|
+
`${attempt.error.name || "Error"}: ${attempt.error.message}`
|
|
183
|
+
);
|
|
184
|
+
if (attempt.error.stackTrace) {
|
|
185
|
+
sections.push("", attempt.error.stackTrace);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return sections.join("\n");
|
|
191
|
+
}
|
|
192
|
+
async function handleTriggerTool(name, args2, deps) {
|
|
193
|
+
const { sshExec: sshExec2, getServerConnection: getServerConnection2 } = deps;
|
|
194
|
+
const serverId = DEFAULT_TRIGGER_SERVER_ID;
|
|
195
|
+
switch (name) {
|
|
196
|
+
// -----------------------------------------------------------------
|
|
197
|
+
case "trigger-list": {
|
|
198
|
+
const { conn, proxy } = await getServerConnection2(serverId);
|
|
199
|
+
const script = [
|
|
200
|
+
"FOUND=0",
|
|
201
|
+
"for PG in $(docker ps --format '{{.Names}}' | grep -E 'trigger.*-postgres-[0-9]+$'); do",
|
|
202
|
+
` PROJECT=$(echo "$PG" | sed 's/-postgres-[0-9]*$//')`,
|
|
203
|
+
' WA="${PROJECT}-webapp-1"',
|
|
204
|
+
` PORT=$(docker port "$WA" 3000/tcp 2>/dev/null | head -1 | sed 's/.*://')`,
|
|
205
|
+
` STATUS=$(docker inspect -f '{{.State.Status}}' "$WA" 2>/dev/null || echo 'not_found')`,
|
|
206
|
+
` RUNNING=$(docker ps --filter "label=com.docker.compose.project=\${PROJECT}" --format '{{.Names}}' 2>/dev/null | wc -l)`,
|
|
207
|
+
` TOTAL=$(docker ps -a --filter "label=com.docker.compose.project=\${PROJECT}" --format '{{.Names}}' 2>/dev/null | wc -l)`,
|
|
208
|
+
' echo "${PROJECT}|${PORT:-?}|${STATUS}|${RUNNING}/${TOTAL}"',
|
|
209
|
+
" FOUND=1",
|
|
210
|
+
"done",
|
|
211
|
+
'[ "$FOUND" = "0" ] && echo "NO_INSTANCES"'
|
|
212
|
+
].join("\n");
|
|
213
|
+
const result = await sshExec2(conn, script, proxy);
|
|
214
|
+
const output = result.stdout.trim();
|
|
215
|
+
if (!output || output === "NO_INSTANCES") {
|
|
216
|
+
return { content: [{ type: "text", text: "No Trigger.dev instances found on this server. Check if Docker containers are running." }] };
|
|
217
|
+
}
|
|
218
|
+
const header = `${"PROJECT".padEnd(45)} ${"PORT".padEnd(6)} ${"WEBAPP".padEnd(12)} CONTAINERS`;
|
|
219
|
+
const sep = "-".repeat(85);
|
|
220
|
+
const lines = output.split("\n").map((line) => {
|
|
221
|
+
const [project, port, status, containers] = line.split("|");
|
|
222
|
+
return `${(project || "").padEnd(45)} ${(port || "?").padEnd(6)} ${(status || "?").padEnd(12)} ${containers || "?"}`;
|
|
223
|
+
});
|
|
224
|
+
return { content: [{ type: "text", text: `${header}
|
|
225
|
+
${sep}
|
|
226
|
+
${lines.join("\n")}` }] };
|
|
227
|
+
}
|
|
228
|
+
// -----------------------------------------------------------------
|
|
229
|
+
case "trigger-runs": {
|
|
230
|
+
const project = String(args2.project);
|
|
231
|
+
const { conn, proxy } = await getServerConnection2(serverId);
|
|
232
|
+
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
233
|
+
const limit = Math.min(Math.max(Number(args2.limit) || 20, 1), 100);
|
|
234
|
+
const queryParts = [`page[size]=${limit}`];
|
|
235
|
+
if (args2.status) queryParts.push(`filter[status]=${String(args2.status)}`);
|
|
236
|
+
if (args2.taskIdentifier) queryParts.push(`filter[taskIdentifier]=${String(args2.taskIdentifier)}`);
|
|
237
|
+
const rawJson = await triggerApi(
|
|
238
|
+
conn,
|
|
239
|
+
proxy,
|
|
240
|
+
sshExec2,
|
|
241
|
+
instance,
|
|
242
|
+
"GET",
|
|
243
|
+
`/api/v1/runs?${queryParts.join("&")}`
|
|
244
|
+
);
|
|
245
|
+
let parsed;
|
|
246
|
+
try {
|
|
247
|
+
parsed = JSON.parse(rawJson);
|
|
248
|
+
} catch {
|
|
249
|
+
return { content: [{ type: "text", text: `Invalid API response:
|
|
250
|
+
${rawJson.substring(0, 500)}` }] };
|
|
251
|
+
}
|
|
252
|
+
const runs = parsed.data || [];
|
|
253
|
+
return { content: [{ type: "text", text: formatRunsTable(runs) }] };
|
|
254
|
+
}
|
|
255
|
+
// -----------------------------------------------------------------
|
|
256
|
+
case "trigger-run-detail": {
|
|
257
|
+
const project = String(args2.project);
|
|
258
|
+
const runId = String(args2.runId);
|
|
259
|
+
const { conn, proxy } = await getServerConnection2(serverId);
|
|
260
|
+
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
261
|
+
const rawJson = await triggerApi(
|
|
262
|
+
conn,
|
|
263
|
+
proxy,
|
|
264
|
+
sshExec2,
|
|
265
|
+
instance,
|
|
266
|
+
"GET",
|
|
267
|
+
`/api/v3/runs/${encodeURIComponent(runId)}`
|
|
268
|
+
);
|
|
269
|
+
let run;
|
|
270
|
+
try {
|
|
271
|
+
run = JSON.parse(rawJson);
|
|
272
|
+
} catch {
|
|
273
|
+
return { content: [{ type: "text", text: `Invalid API response:
|
|
274
|
+
${rawJson.substring(0, 500)}` }] };
|
|
275
|
+
}
|
|
276
|
+
return { content: [{ type: "text", text: formatRunDetail(run) }] };
|
|
277
|
+
}
|
|
278
|
+
// -----------------------------------------------------------------
|
|
279
|
+
case "trigger-test-task": {
|
|
280
|
+
const project = String(args2.project);
|
|
281
|
+
const taskId = String(args2.taskId);
|
|
282
|
+
const waitSeconds = Math.min(Math.max(Number(args2.waitSeconds) || 60, 5), 300);
|
|
283
|
+
const { conn, proxy } = await getServerConnection2(serverId);
|
|
284
|
+
conn.timeout = (waitSeconds + 30) * 1e3;
|
|
285
|
+
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
286
|
+
let payload = "{}";
|
|
287
|
+
if (args2.payload) {
|
|
288
|
+
try {
|
|
289
|
+
JSON.parse(String(args2.payload));
|
|
290
|
+
payload = String(args2.payload);
|
|
291
|
+
} catch {
|
|
292
|
+
throw new Error(`Invalid JSON payload: ${String(args2.payload).substring(0, 200)}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const triggerBody = JSON.stringify({
|
|
296
|
+
payload: JSON.parse(payload),
|
|
297
|
+
options: { tags: ["mcp-test"], test: true }
|
|
298
|
+
});
|
|
299
|
+
const triggerJson = await triggerApi(
|
|
300
|
+
conn,
|
|
301
|
+
proxy,
|
|
302
|
+
sshExec2,
|
|
303
|
+
instance,
|
|
304
|
+
"POST",
|
|
305
|
+
`/api/v1/tasks/${encodeURIComponent(taskId)}/trigger`,
|
|
306
|
+
triggerBody
|
|
307
|
+
);
|
|
308
|
+
let triggerResp;
|
|
309
|
+
try {
|
|
310
|
+
triggerResp = JSON.parse(triggerJson);
|
|
311
|
+
} catch {
|
|
312
|
+
return { content: [{ type: "text", text: `Failed to trigger task. API response:
|
|
313
|
+
${triggerJson.substring(0, 500)}` }] };
|
|
314
|
+
}
|
|
315
|
+
const runId = triggerResp.id;
|
|
316
|
+
if (!runId) {
|
|
317
|
+
return { content: [{ type: "text", text: `Trigger API did not return a run ID:
|
|
318
|
+
${triggerJson.substring(0, 500)}` }] };
|
|
319
|
+
}
|
|
320
|
+
const pollInterval = 3e3;
|
|
321
|
+
const maxPolls = Math.ceil(waitSeconds * 1e3 / pollInterval);
|
|
322
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
323
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
324
|
+
const pollJson = await triggerApi(
|
|
325
|
+
conn,
|
|
326
|
+
proxy,
|
|
327
|
+
sshExec2,
|
|
328
|
+
instance,
|
|
329
|
+
"GET",
|
|
330
|
+
`/api/v3/runs/${encodeURIComponent(runId)}`
|
|
331
|
+
);
|
|
332
|
+
let run;
|
|
333
|
+
try {
|
|
334
|
+
run = JSON.parse(pollJson);
|
|
335
|
+
} catch {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (TERMINAL_STATUSES.has(run.status)) {
|
|
339
|
+
return { content: [{ type: "text", text: formatRunDetail(run) }] };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
content: [{
|
|
344
|
+
type: "text",
|
|
345
|
+
text: `Run ${runId} did not complete within ${waitSeconds}s (still running). Use trigger-run-detail to check later.`
|
|
346
|
+
}]
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
// -----------------------------------------------------------------
|
|
350
|
+
case "trigger-cancel-run": {
|
|
351
|
+
const project = String(args2.project);
|
|
352
|
+
const runId = String(args2.runId);
|
|
353
|
+
const { conn, proxy } = await getServerConnection2(serverId);
|
|
354
|
+
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
355
|
+
const rawJson = await triggerApi(
|
|
356
|
+
conn,
|
|
357
|
+
proxy,
|
|
358
|
+
sshExec2,
|
|
359
|
+
instance,
|
|
360
|
+
"POST",
|
|
361
|
+
`/api/v3/runs/${encodeURIComponent(runId)}/cancel`
|
|
362
|
+
);
|
|
363
|
+
let result;
|
|
364
|
+
try {
|
|
365
|
+
result = JSON.parse(rawJson);
|
|
366
|
+
} catch {
|
|
367
|
+
return { content: [{ type: "text", text: `Cancel response:
|
|
368
|
+
${rawJson.substring(0, 500)}` }] };
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
content: [{
|
|
372
|
+
type: "text",
|
|
373
|
+
text: `Run ${result.id || runId} canceled (status: ${result.status || "unknown"})`
|
|
374
|
+
}]
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
// -----------------------------------------------------------------
|
|
378
|
+
case "trigger-replay-run": {
|
|
379
|
+
const project = String(args2.project);
|
|
380
|
+
const runId = String(args2.runId);
|
|
381
|
+
const { conn, proxy } = await getServerConnection2(serverId);
|
|
382
|
+
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
383
|
+
const rawJson = await triggerApi(
|
|
384
|
+
conn,
|
|
385
|
+
proxy,
|
|
386
|
+
sshExec2,
|
|
387
|
+
instance,
|
|
388
|
+
"POST",
|
|
389
|
+
`/api/v3/runs/${encodeURIComponent(runId)}/replay`
|
|
390
|
+
);
|
|
391
|
+
let result;
|
|
392
|
+
try {
|
|
393
|
+
result = JSON.parse(rawJson);
|
|
394
|
+
} catch {
|
|
395
|
+
return { content: [{ type: "text", text: `Replay response:
|
|
396
|
+
${rawJson.substring(0, 500)}` }] };
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
content: [{
|
|
400
|
+
type: "text",
|
|
401
|
+
text: result.id ? `Run replayed. New run ID: ${result.id}` : `Replay response:
|
|
402
|
+
${rawJson.substring(0, 500)}`
|
|
403
|
+
}]
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
// -----------------------------------------------------------------
|
|
407
|
+
default:
|
|
408
|
+
return { content: [{ type: "text", text: `Unknown trigger tool: ${name}` }] };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/index.ts
|
|
9
413
|
var args = process.argv.slice(2);
|
|
10
414
|
function getArg(name) {
|
|
11
415
|
return args.find((a) => a.startsWith(`--${name}=`))?.split("=").slice(1).join("=");
|
|
@@ -149,7 +553,8 @@ var TOOL_MODULE_MAP = {
|
|
|
149
553
|
"dns-list": "domains",
|
|
150
554
|
"dns-create": "domains",
|
|
151
555
|
"dns-update": "domains",
|
|
152
|
-
"dns-delete": "domains"
|
|
556
|
+
"dns-delete": "domains",
|
|
557
|
+
...TRIGGER_TOOL_MODULE_MAP
|
|
153
558
|
};
|
|
154
559
|
var authContext = null;
|
|
155
560
|
async function validateApiKey(key) {
|
|
@@ -1004,7 +1409,7 @@ async function mijnhostFetch(path, options = {}) {
|
|
|
1004
1409
|
"API-Key": key,
|
|
1005
1410
|
"Accept": "application/json",
|
|
1006
1411
|
"Content-Type": "application/json",
|
|
1007
|
-
"User-Agent": "mg-dashboard-mcp/
|
|
1412
|
+
"User-Agent": "mg-dashboard-mcp/2.2.0",
|
|
1008
1413
|
...options.headers || {}
|
|
1009
1414
|
}
|
|
1010
1415
|
});
|
|
@@ -1404,10 +1809,12 @@ var TOOLS = [
|
|
|
1404
1809
|
},
|
|
1405
1810
|
required: ["domain", "type", "name", "value"]
|
|
1406
1811
|
}
|
|
1407
|
-
}
|
|
1812
|
+
},
|
|
1813
|
+
// ----- Trigger.dev -----
|
|
1814
|
+
...TRIGGER_TOOLS
|
|
1408
1815
|
];
|
|
1409
1816
|
var server = new Server(
|
|
1410
|
-
{ name: "mg-dashboard-mcp", version: "
|
|
1817
|
+
{ name: "mg-dashboard-mcp", version: "2.2.0" },
|
|
1411
1818
|
{ capabilities: { tools: {} } }
|
|
1412
1819
|
);
|
|
1413
1820
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -1824,14 +2231,14 @@ ${cronD.stdout}`);
|
|
|
1824
2231
|
return { content: [{ type: "text", text: parts.join("\n\n") }] };
|
|
1825
2232
|
}
|
|
1826
2233
|
case "cron-add": {
|
|
2234
|
+
if (!a.schedule || !a.command) {
|
|
2235
|
+
throw new Error("Both schedule and command are required");
|
|
2236
|
+
}
|
|
1827
2237
|
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1828
2238
|
const user = a.user ? String(a.user) : "root";
|
|
1829
2239
|
const schedule = String(a.schedule).trim();
|
|
1830
2240
|
const command = String(a.command).trim();
|
|
1831
2241
|
const commentText = a.comment ? String(a.comment).trim() : "";
|
|
1832
|
-
if (!schedule || !command) {
|
|
1833
|
-
throw new Error("Both schedule and command are required");
|
|
1834
|
-
}
|
|
1835
2242
|
const entry = commentText ? `# ${commentText}
|
|
1836
2243
|
${schedule} ${command}` : `${schedule} ${command}`;
|
|
1837
2244
|
const result = await sshExec(conn, `(crontab -l -u ${user} 2>/dev/null; echo '${entry.replace(/'/g, "'\\''")}') | crontab -u ${user} -`, proxy);
|
|
@@ -1842,12 +2249,12 @@ ${schedule} ${command}` : `${schedule} ${command}`;
|
|
|
1842
2249
|
${schedule} ${command}` }] };
|
|
1843
2250
|
}
|
|
1844
2251
|
case "cron-remove": {
|
|
2252
|
+
if (!a.commandMatch) {
|
|
2253
|
+
throw new Error("commandMatch is required \u2013 pass a (partial) command string to identify the cron entry");
|
|
2254
|
+
}
|
|
1845
2255
|
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1846
2256
|
const user = a.user ? String(a.user) : "root";
|
|
1847
2257
|
const match = String(a.commandMatch).trim();
|
|
1848
|
-
if (!match) {
|
|
1849
|
-
throw new Error("commandMatch is required");
|
|
1850
|
-
}
|
|
1851
2258
|
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
|
|
1852
2259
|
const lines = current.stdout.split("\n");
|
|
1853
2260
|
const before = lines.length;
|
|
@@ -1867,13 +2274,13 @@ ${schedule} ${command}` }] };
|
|
|
1867
2274
|
return { content: [{ type: "text", text: `Removed ${removed} cron entry/entries matching "${match}" from ${user} crontab` }] };
|
|
1868
2275
|
}
|
|
1869
2276
|
case "cron-toggle": {
|
|
2277
|
+
if (!a.commandMatch) {
|
|
2278
|
+
throw new Error("commandMatch is required \u2013 pass a (partial) command string to identify the cron entry");
|
|
2279
|
+
}
|
|
1870
2280
|
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1871
2281
|
const user = a.user ? String(a.user) : "root";
|
|
1872
2282
|
const match = String(a.commandMatch).trim();
|
|
1873
2283
|
const enable = Boolean(a.enable);
|
|
1874
|
-
if (!match) {
|
|
1875
|
-
throw new Error("commandMatch is required");
|
|
1876
|
-
}
|
|
1877
2284
|
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
|
|
1878
2285
|
const lines = current.stdout.split("\n");
|
|
1879
2286
|
let toggled = 0;
|
|
@@ -2038,6 +2445,9 @@ ${lines.join("\n")}` }] };
|
|
|
2038
2445
|
return { content: [{ type: "text", text: `DNS record deleted: ${type} ${dnsName} = ${value} (${remaining.length} records remaining)` }] };
|
|
2039
2446
|
}
|
|
2040
2447
|
default:
|
|
2448
|
+
if (TRIGGER_TOOL_NAMES.has(name)) {
|
|
2449
|
+
return handleTriggerTool(name, a, { sshExec, getServerConnection });
|
|
2450
|
+
}
|
|
2041
2451
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
2042
2452
|
}
|
|
2043
2453
|
} catch (err) {
|