@mgsoftwarebv/mg-dashboard-mcp 2.1.0 → 2.2.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/dist/index.js +598 -68
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,416 @@ 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 TRIGGER_TOOLS = [
|
|
20
|
+
{
|
|
21
|
+
name: "trigger-list",
|
|
22
|
+
description: "List all Trigger.dev instances (Docker Compose projects) on a server. Returns the compose project name, webapp port, and status for each instance. Use the project name in other trigger-* tools.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
serverId: { type: "string", description: "UUID of the SSH server running Trigger.dev" }
|
|
27
|
+
},
|
|
28
|
+
required: ["serverId"]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "trigger-runs",
|
|
33
|
+
description: "List recent task runs for a Trigger.dev instance. Returns run ID, task name, status, duration, and timestamps.",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
38
|
+
project: { type: "string", description: "Compose project name (from trigger-list)" },
|
|
39
|
+
status: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Comma-separated status filter: QUEUED,EXECUTING,COMPLETED,FAILED,CRASHED,CANCELED,SYSTEM_FAILURE"
|
|
42
|
+
},
|
|
43
|
+
taskIdentifier: { type: "string", description: 'Filter by task identifier (e.g. "hello-world")' },
|
|
44
|
+
limit: { type: "number", description: "Max runs to return (default 20, max 100)" }
|
|
45
|
+
},
|
|
46
|
+
required: ["serverId", "project"]
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "trigger-run-detail",
|
|
51
|
+
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.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
56
|
+
project: { type: "string", description: "Compose project name" },
|
|
57
|
+
runId: { type: "string", description: "Run ID (e.g. run_xxxxx)" }
|
|
58
|
+
},
|
|
59
|
+
required: ["serverId", "project", "runId"]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "trigger-test-task",
|
|
64
|
+
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.",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
69
|
+
project: { type: "string", description: "Compose project name" },
|
|
70
|
+
taskId: { type: "string", description: 'Task identifier (e.g. "hello-world", "execute-pipeline")' },
|
|
71
|
+
payload: { type: "string", description: "JSON payload string to pass to the task (optional)" },
|
|
72
|
+
waitSeconds: { type: "number", description: "Max seconds to wait for completion (default 60, max 300)" }
|
|
73
|
+
},
|
|
74
|
+
required: ["serverId", "project", "taskId"]
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "trigger-cancel-run",
|
|
79
|
+
description: "Cancel a running or queued Trigger.dev task run.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
84
|
+
project: { type: "string", description: "Compose project name" },
|
|
85
|
+
runId: { type: "string", description: "Run ID to cancel (e.g. run_xxxxx)" }
|
|
86
|
+
},
|
|
87
|
+
required: ["serverId", "project", "runId"]
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "trigger-replay-run",
|
|
92
|
+
description: "Replay a previously failed or completed run with the same payload. Returns the new run ID.",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: {
|
|
96
|
+
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
97
|
+
project: { type: "string", description: "Compose project name" },
|
|
98
|
+
runId: { type: "string", description: "Run ID to replay (e.g. run_xxxxx)" }
|
|
99
|
+
},
|
|
100
|
+
required: ["serverId", "project", "runId"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
var TRIGGER_TOOL_NAMES = new Set(TRIGGER_TOOLS.map((t) => t.name));
|
|
105
|
+
var TRIGGER_TOOL_MODULE_MAP = {
|
|
106
|
+
"trigger-list": "ci_cd",
|
|
107
|
+
"trigger-runs": "ci_cd",
|
|
108
|
+
"trigger-run-detail": "ci_cd",
|
|
109
|
+
"trigger-test-task": "ci_cd",
|
|
110
|
+
"trigger-cancel-run": "ci_cd",
|
|
111
|
+
"trigger-replay-run": "ci_cd"
|
|
112
|
+
};
|
|
113
|
+
async function discoverInstance(project, conn, proxy, sshExec2) {
|
|
114
|
+
const pg = `${project}-postgres-1`;
|
|
115
|
+
const wa = `${project}-webapp-1`;
|
|
116
|
+
const cmd = [
|
|
117
|
+
`PORT=$(docker port "${wa}" 3000/tcp 2>/dev/null | head -1 | sed 's/.*://')`,
|
|
118
|
+
`KEY=$(docker exec "${pg}" psql -U postgres -d main -t -A -c "SELECT \\"apiKey\\" FROM \\"RuntimeEnvironment\\" WHERE slug='production' LIMIT 1" 2>/dev/null | tr -d '[:space:]')`,
|
|
119
|
+
'echo "$PORT|$KEY"'
|
|
120
|
+
].join(" && ");
|
|
121
|
+
const result = await sshExec2(conn, cmd, proxy);
|
|
122
|
+
const output = result.stdout.trim();
|
|
123
|
+
const sepIdx = output.indexOf("|");
|
|
124
|
+
const port = sepIdx > 0 ? output.substring(0, sepIdx) : "";
|
|
125
|
+
const apiKey2 = sepIdx > 0 ? output.substring(sepIdx + 1) : "";
|
|
126
|
+
if (!port) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Could not find webapp port for ${wa}. Is the container running? Use trigger-list to see available instances.`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (!apiKey2) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Could not get API key from ${pg}. Check if the Trigger.dev database is accessible and the RuntimeEnvironment table exists.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return { port, apiKey: apiKey2 };
|
|
137
|
+
}
|
|
138
|
+
async function triggerApi(conn, proxy, sshExec2, instance, method, path, body) {
|
|
139
|
+
const parts = ["curl", "-s", "--max-time", "30"];
|
|
140
|
+
if (method !== "GET") parts.push("-X", method);
|
|
141
|
+
parts.push(`"http://localhost:${instance.port}${path}"`);
|
|
142
|
+
parts.push(`-H "Authorization: Bearer ${instance.apiKey}"`);
|
|
143
|
+
if (body) {
|
|
144
|
+
parts.push('-H "Content-Type: application/json"');
|
|
145
|
+
parts.push(`-d '${body.replace(/'/g, "'\\''")}'`);
|
|
146
|
+
}
|
|
147
|
+
const result = await sshExec2(conn, parts.join(" "), proxy);
|
|
148
|
+
if (result.exitCode !== 0 && !result.stdout) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Trigger.dev API call failed (${method} ${path}): ${result.stderr || `exit code ${result.exitCode}`}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return result.stdout;
|
|
154
|
+
}
|
|
155
|
+
function formatRunsTable(runs) {
|
|
156
|
+
if (runs.length === 0) return "No runs found";
|
|
157
|
+
const lines = runs.map((r) => {
|
|
158
|
+
const duration = r.durationMs != null ? `${(r.durationMs / 1e3).toFixed(1)}s` : "-";
|
|
159
|
+
const test = r.isTest ? " [TEST]" : "";
|
|
160
|
+
const created = r.createdAt ? new Date(r.createdAt).toLocaleString("nl-NL", { timeZone: "Europe/Amsterdam" }) : "";
|
|
161
|
+
return `${r.id} ${r.taskIdentifier.padEnd(35)} ${r.status.padEnd(16)} ${duration.padStart(8)} ${created}${test}`;
|
|
162
|
+
});
|
|
163
|
+
return `${"ID".padEnd(20)} ${"TASK".padEnd(35)} ${"STATUS".padEnd(16)} ${"DURATION".padStart(8)} CREATED
|
|
164
|
+
` + "-".repeat(110) + "\n" + lines.join("\n");
|
|
165
|
+
}
|
|
166
|
+
function formatRunDetail(run) {
|
|
167
|
+
const sections = [];
|
|
168
|
+
sections.push(`=== Run ${run.id} ===`);
|
|
169
|
+
sections.push(`Task: ${run.taskIdentifier}`);
|
|
170
|
+
sections.push(`Status: ${run.status}`);
|
|
171
|
+
sections.push(`Version: ${run.version || "-"}`);
|
|
172
|
+
if (run.startedAt) sections.push(`Started: ${new Date(run.startedAt).toLocaleString("nl-NL", { timeZone: "Europe/Amsterdam" })}`);
|
|
173
|
+
if (run.finishedAt) sections.push(`Finished: ${new Date(run.finishedAt).toLocaleString("nl-NL", { timeZone: "Europe/Amsterdam" })}`);
|
|
174
|
+
if (run.durationMs != null) sections.push(`Duration: ${(run.durationMs / 1e3).toFixed(2)}s`);
|
|
175
|
+
if (run.payload !== void 0) {
|
|
176
|
+
const payloadStr = typeof run.payload === "string" ? run.payload : JSON.stringify(run.payload, null, 2);
|
|
177
|
+
sections.push("", "--- Payload ---", payloadStr);
|
|
178
|
+
}
|
|
179
|
+
if (run.output !== void 0) {
|
|
180
|
+
const outputStr = typeof run.output === "string" ? run.output : JSON.stringify(run.output, null, 2);
|
|
181
|
+
sections.push("", "--- Output ---", outputStr);
|
|
182
|
+
}
|
|
183
|
+
if (run.attempts && run.attempts.length > 0) {
|
|
184
|
+
for (const attempt of run.attempts) {
|
|
185
|
+
if (attempt.error) {
|
|
186
|
+
sections.push(
|
|
187
|
+
"",
|
|
188
|
+
`--- Error (${attempt.id}) ---`,
|
|
189
|
+
`${attempt.error.name || "Error"}: ${attempt.error.message}`
|
|
190
|
+
);
|
|
191
|
+
if (attempt.error.stackTrace) {
|
|
192
|
+
sections.push("", attempt.error.stackTrace);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return sections.join("\n");
|
|
198
|
+
}
|
|
199
|
+
async function handleTriggerTool(name, args2, deps) {
|
|
200
|
+
const { sshExec: sshExec2, getServerConnection: getServerConnection2 } = deps;
|
|
201
|
+
switch (name) {
|
|
202
|
+
// -----------------------------------------------------------------
|
|
203
|
+
case "trigger-list": {
|
|
204
|
+
const { conn, proxy } = await getServerConnection2(String(args2.serverId));
|
|
205
|
+
const script = [
|
|
206
|
+
"FOUND=0",
|
|
207
|
+
"for PG in $(docker ps --format '{{.Names}}' | grep -E 'trigger.*-postgres-[0-9]+$'); do",
|
|
208
|
+
` PROJECT=$(echo "$PG" | sed 's/-postgres-[0-9]*$//')`,
|
|
209
|
+
' WA="${PROJECT}-webapp-1"',
|
|
210
|
+
` PORT=$(docker port "$WA" 3000/tcp 2>/dev/null | head -1 | sed 's/.*://')`,
|
|
211
|
+
` STATUS=$(docker inspect -f '{{.State.Status}}' "$WA" 2>/dev/null || echo 'not_found')`,
|
|
212
|
+
` RUNNING=$(docker ps --filter "label=com.docker.compose.project=\${PROJECT}" --format '{{.Names}}' 2>/dev/null | wc -l)`,
|
|
213
|
+
` TOTAL=$(docker ps -a --filter "label=com.docker.compose.project=\${PROJECT}" --format '{{.Names}}' 2>/dev/null | wc -l)`,
|
|
214
|
+
' echo "${PROJECT}|${PORT:-?}|${STATUS}|${RUNNING}/${TOTAL}"',
|
|
215
|
+
" FOUND=1",
|
|
216
|
+
"done",
|
|
217
|
+
'[ "$FOUND" = "0" ] && echo "NO_INSTANCES"'
|
|
218
|
+
].join("\n");
|
|
219
|
+
const result = await sshExec2(conn, script, proxy);
|
|
220
|
+
const output = result.stdout.trim();
|
|
221
|
+
if (!output || output === "NO_INSTANCES") {
|
|
222
|
+
return { content: [{ type: "text", text: "No Trigger.dev instances found on this server. Check if Docker containers are running." }] };
|
|
223
|
+
}
|
|
224
|
+
const header = `${"PROJECT".padEnd(45)} ${"PORT".padEnd(6)} ${"WEBAPP".padEnd(12)} CONTAINERS`;
|
|
225
|
+
const sep = "-".repeat(85);
|
|
226
|
+
const lines = output.split("\n").map((line) => {
|
|
227
|
+
const [project, port, status, containers] = line.split("|");
|
|
228
|
+
return `${(project || "").padEnd(45)} ${(port || "?").padEnd(6)} ${(status || "?").padEnd(12)} ${containers || "?"}`;
|
|
229
|
+
});
|
|
230
|
+
return { content: [{ type: "text", text: `${header}
|
|
231
|
+
${sep}
|
|
232
|
+
${lines.join("\n")}` }] };
|
|
233
|
+
}
|
|
234
|
+
// -----------------------------------------------------------------
|
|
235
|
+
case "trigger-runs": {
|
|
236
|
+
const project = String(args2.project);
|
|
237
|
+
const { conn, proxy } = await getServerConnection2(String(args2.serverId));
|
|
238
|
+
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
239
|
+
const limit = Math.min(Math.max(Number(args2.limit) || 20, 1), 100);
|
|
240
|
+
const queryParts = [`page[size]=${limit}`];
|
|
241
|
+
if (args2.status) queryParts.push(`filter[status]=${String(args2.status)}`);
|
|
242
|
+
if (args2.taskIdentifier) queryParts.push(`filter[taskIdentifier]=${String(args2.taskIdentifier)}`);
|
|
243
|
+
const rawJson = await triggerApi(
|
|
244
|
+
conn,
|
|
245
|
+
proxy,
|
|
246
|
+
sshExec2,
|
|
247
|
+
instance,
|
|
248
|
+
"GET",
|
|
249
|
+
`/api/v1/runs?${queryParts.join("&")}`
|
|
250
|
+
);
|
|
251
|
+
let parsed;
|
|
252
|
+
try {
|
|
253
|
+
parsed = JSON.parse(rawJson);
|
|
254
|
+
} catch {
|
|
255
|
+
return { content: [{ type: "text", text: `Invalid API response:
|
|
256
|
+
${rawJson.substring(0, 500)}` }] };
|
|
257
|
+
}
|
|
258
|
+
const runs = parsed.data || [];
|
|
259
|
+
return { content: [{ type: "text", text: formatRunsTable(runs) }] };
|
|
260
|
+
}
|
|
261
|
+
// -----------------------------------------------------------------
|
|
262
|
+
case "trigger-run-detail": {
|
|
263
|
+
const project = String(args2.project);
|
|
264
|
+
const runId = String(args2.runId);
|
|
265
|
+
const { conn, proxy } = await getServerConnection2(String(args2.serverId));
|
|
266
|
+
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
267
|
+
const rawJson = await triggerApi(
|
|
268
|
+
conn,
|
|
269
|
+
proxy,
|
|
270
|
+
sshExec2,
|
|
271
|
+
instance,
|
|
272
|
+
"GET",
|
|
273
|
+
`/api/v3/runs/${encodeURIComponent(runId)}`
|
|
274
|
+
);
|
|
275
|
+
let run;
|
|
276
|
+
try {
|
|
277
|
+
run = JSON.parse(rawJson);
|
|
278
|
+
} catch {
|
|
279
|
+
return { content: [{ type: "text", text: `Invalid API response:
|
|
280
|
+
${rawJson.substring(0, 500)}` }] };
|
|
281
|
+
}
|
|
282
|
+
return { content: [{ type: "text", text: formatRunDetail(run) }] };
|
|
283
|
+
}
|
|
284
|
+
// -----------------------------------------------------------------
|
|
285
|
+
case "trigger-test-task": {
|
|
286
|
+
const project = String(args2.project);
|
|
287
|
+
const taskId = String(args2.taskId);
|
|
288
|
+
const waitSeconds = Math.min(Math.max(Number(args2.waitSeconds) || 60, 5), 300);
|
|
289
|
+
const { conn, proxy } = await getServerConnection2(String(args2.serverId));
|
|
290
|
+
conn.timeout = (waitSeconds + 30) * 1e3;
|
|
291
|
+
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
292
|
+
let payload = "{}";
|
|
293
|
+
if (args2.payload) {
|
|
294
|
+
try {
|
|
295
|
+
JSON.parse(String(args2.payload));
|
|
296
|
+
payload = String(args2.payload);
|
|
297
|
+
} catch {
|
|
298
|
+
throw new Error(`Invalid JSON payload: ${String(args2.payload).substring(0, 200)}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const triggerBody = JSON.stringify({
|
|
302
|
+
payload: JSON.parse(payload),
|
|
303
|
+
options: { tags: ["mcp-test"], test: true }
|
|
304
|
+
});
|
|
305
|
+
const triggerJson = await triggerApi(
|
|
306
|
+
conn,
|
|
307
|
+
proxy,
|
|
308
|
+
sshExec2,
|
|
309
|
+
instance,
|
|
310
|
+
"POST",
|
|
311
|
+
`/api/v1/tasks/${encodeURIComponent(taskId)}/trigger`,
|
|
312
|
+
triggerBody
|
|
313
|
+
);
|
|
314
|
+
let triggerResp;
|
|
315
|
+
try {
|
|
316
|
+
triggerResp = JSON.parse(triggerJson);
|
|
317
|
+
} catch {
|
|
318
|
+
return { content: [{ type: "text", text: `Failed to trigger task. API response:
|
|
319
|
+
${triggerJson.substring(0, 500)}` }] };
|
|
320
|
+
}
|
|
321
|
+
const runId = triggerResp.id;
|
|
322
|
+
if (!runId) {
|
|
323
|
+
return { content: [{ type: "text", text: `Trigger API did not return a run ID:
|
|
324
|
+
${triggerJson.substring(0, 500)}` }] };
|
|
325
|
+
}
|
|
326
|
+
const pollInterval = 3e3;
|
|
327
|
+
const maxPolls = Math.ceil(waitSeconds * 1e3 / pollInterval);
|
|
328
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
329
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
330
|
+
const pollJson = await triggerApi(
|
|
331
|
+
conn,
|
|
332
|
+
proxy,
|
|
333
|
+
sshExec2,
|
|
334
|
+
instance,
|
|
335
|
+
"GET",
|
|
336
|
+
`/api/v3/runs/${encodeURIComponent(runId)}`
|
|
337
|
+
);
|
|
338
|
+
let run;
|
|
339
|
+
try {
|
|
340
|
+
run = JSON.parse(pollJson);
|
|
341
|
+
} catch {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (TERMINAL_STATUSES.has(run.status)) {
|
|
345
|
+
return { content: [{ type: "text", text: formatRunDetail(run) }] };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
content: [{
|
|
350
|
+
type: "text",
|
|
351
|
+
text: `Run ${runId} did not complete within ${waitSeconds}s (still running). Use trigger-run-detail to check later.`
|
|
352
|
+
}]
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// -----------------------------------------------------------------
|
|
356
|
+
case "trigger-cancel-run": {
|
|
357
|
+
const project = String(args2.project);
|
|
358
|
+
const runId = String(args2.runId);
|
|
359
|
+
const { conn, proxy } = await getServerConnection2(String(args2.serverId));
|
|
360
|
+
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
361
|
+
const rawJson = await triggerApi(
|
|
362
|
+
conn,
|
|
363
|
+
proxy,
|
|
364
|
+
sshExec2,
|
|
365
|
+
instance,
|
|
366
|
+
"POST",
|
|
367
|
+
`/api/v3/runs/${encodeURIComponent(runId)}/cancel`
|
|
368
|
+
);
|
|
369
|
+
let result;
|
|
370
|
+
try {
|
|
371
|
+
result = JSON.parse(rawJson);
|
|
372
|
+
} catch {
|
|
373
|
+
return { content: [{ type: "text", text: `Cancel response:
|
|
374
|
+
${rawJson.substring(0, 500)}` }] };
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
content: [{
|
|
378
|
+
type: "text",
|
|
379
|
+
text: `Run ${result.id || runId} canceled (status: ${result.status || "unknown"})`
|
|
380
|
+
}]
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
// -----------------------------------------------------------------
|
|
384
|
+
case "trigger-replay-run": {
|
|
385
|
+
const project = String(args2.project);
|
|
386
|
+
const runId = String(args2.runId);
|
|
387
|
+
const { conn, proxy } = await getServerConnection2(String(args2.serverId));
|
|
388
|
+
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
389
|
+
const rawJson = await triggerApi(
|
|
390
|
+
conn,
|
|
391
|
+
proxy,
|
|
392
|
+
sshExec2,
|
|
393
|
+
instance,
|
|
394
|
+
"POST",
|
|
395
|
+
`/api/v3/runs/${encodeURIComponent(runId)}/replay`
|
|
396
|
+
);
|
|
397
|
+
let result;
|
|
398
|
+
try {
|
|
399
|
+
result = JSON.parse(rawJson);
|
|
400
|
+
} catch {
|
|
401
|
+
return { content: [{ type: "text", text: `Replay response:
|
|
402
|
+
${rawJson.substring(0, 500)}` }] };
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
content: [{
|
|
406
|
+
type: "text",
|
|
407
|
+
text: result.id ? `Run replayed. New run ID: ${result.id}` : `Replay response:
|
|
408
|
+
${rawJson.substring(0, 500)}`
|
|
409
|
+
}]
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
// -----------------------------------------------------------------
|
|
413
|
+
default:
|
|
414
|
+
return { content: [{ type: "text", text: `Unknown trigger tool: ${name}` }] };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/index.ts
|
|
9
419
|
var args = process.argv.slice(2);
|
|
10
420
|
function getArg(name) {
|
|
11
421
|
return args.find((a) => a.startsWith(`--${name}=`))?.split("=").slice(1).join("=");
|
|
@@ -149,7 +559,8 @@ var TOOL_MODULE_MAP = {
|
|
|
149
559
|
"dns-list": "domains",
|
|
150
560
|
"dns-create": "domains",
|
|
151
561
|
"dns-update": "domains",
|
|
152
|
-
"dns-delete": "domains"
|
|
562
|
+
"dns-delete": "domains",
|
|
563
|
+
...TRIGGER_TOOL_MODULE_MAP
|
|
153
564
|
};
|
|
154
565
|
var authContext = null;
|
|
155
566
|
async function validateApiKey(key) {
|
|
@@ -386,12 +797,29 @@ async function attemptVercelSync(appName, environment, knownStageId) {
|
|
|
386
797
|
return `Vercel sync error: ${msg}`;
|
|
387
798
|
}
|
|
388
799
|
}
|
|
800
|
+
var SSH_PROXY_SERVER_ID = "03659d55-e194-400d-b82a-bf6457371ded";
|
|
801
|
+
var _proxyConnCache = null;
|
|
802
|
+
async function getProxyConnection() {
|
|
803
|
+
if (_proxyConnCache) return _proxyConnCache;
|
|
804
|
+
const { data, error } = await supabase.from("ssh_server").select("hostname, port, username, password_encrypted, ssh_key_encrypted, ssh_key_passphrase_encrypted").eq("id", SSH_PROXY_SERVER_ID).single();
|
|
805
|
+
if (error || !data) throw new Error("SSH Proxy server not found in database");
|
|
806
|
+
if (!encryptionKey) throw new Error("ENCRYPTION_KEY required to decrypt server credentials");
|
|
807
|
+
_proxyConnCache = {
|
|
808
|
+
hostname: data.hostname,
|
|
809
|
+
port: data.port || 22,
|
|
810
|
+
username: data.username,
|
|
811
|
+
password: data.password_encrypted ? decrypt(data.password_encrypted) : void 0,
|
|
812
|
+
privateKey: data.ssh_key_encrypted ? decrypt(data.ssh_key_encrypted) : void 0,
|
|
813
|
+
passphrase: data.ssh_key_passphrase_encrypted ? decrypt(data.ssh_key_passphrase_encrypted) : void 0
|
|
814
|
+
};
|
|
815
|
+
return _proxyConnCache;
|
|
816
|
+
}
|
|
389
817
|
async function getServerConnection(serverId) {
|
|
390
818
|
assertServerAccess(serverId);
|
|
391
|
-
const { data, error } = await supabase.from("ssh_server").select("hostname, port, username, password_encrypted, ssh_key_encrypted, ssh_key_passphrase_encrypted").eq("id", serverId).single();
|
|
819
|
+
const { data, error } = await supabase.from("ssh_server").select("hostname, port, username, password_encrypted, ssh_key_encrypted, ssh_key_passphrase_encrypted, allowed_ssh_ips").eq("id", serverId).single();
|
|
392
820
|
if (error || !data) throw new Error(`Server not found: ${serverId}`);
|
|
393
821
|
if (!encryptionKey) throw new Error("ENCRYPTION_KEY required to decrypt server credentials");
|
|
394
|
-
|
|
822
|
+
const conn = {
|
|
395
823
|
hostname: data.hostname,
|
|
396
824
|
port: data.port || 22,
|
|
397
825
|
username: data.username,
|
|
@@ -399,8 +827,12 @@ async function getServerConnection(serverId) {
|
|
|
399
827
|
privateKey: data.ssh_key_encrypted ? decrypt(data.ssh_key_encrypted) : void 0,
|
|
400
828
|
passphrase: data.ssh_key_passphrase_encrypted ? decrypt(data.ssh_key_passphrase_encrypted) : void 0
|
|
401
829
|
};
|
|
830
|
+
const needsProxy = data.allowed_ssh_ips !== null && serverId !== SSH_PROXY_SERVER_ID;
|
|
831
|
+
const proxy = needsProxy ? await getProxyConnection() : void 0;
|
|
832
|
+
return { conn, proxy };
|
|
402
833
|
}
|
|
403
|
-
async function sshExec(opts, command) {
|
|
834
|
+
async function sshExec(opts, command, proxy) {
|
|
835
|
+
if (proxy) return sshExecViaProxy(proxy, opts, command);
|
|
404
836
|
return new Promise((resolve) => {
|
|
405
837
|
const ssh = new Client();
|
|
406
838
|
let stdout = "";
|
|
@@ -459,6 +891,98 @@ async function sshExec(opts, command) {
|
|
|
459
891
|
});
|
|
460
892
|
});
|
|
461
893
|
}
|
|
894
|
+
function sshExecViaProxy(proxyOpts, targetOpts, command) {
|
|
895
|
+
return new Promise((resolve) => {
|
|
896
|
+
const proxyClient = new Client();
|
|
897
|
+
let done = false;
|
|
898
|
+
const timeout = targetOpts.timeout || 6e4;
|
|
899
|
+
const timer = setTimeout(() => {
|
|
900
|
+
if (!done) {
|
|
901
|
+
done = true;
|
|
902
|
+
proxyClient.end();
|
|
903
|
+
resolve({ stdout: "", stderr: "SSH proxy command timeout", exitCode: -1 });
|
|
904
|
+
}
|
|
905
|
+
}, timeout);
|
|
906
|
+
const cleanup = () => {
|
|
907
|
+
clearTimeout(timer);
|
|
908
|
+
proxyClient.end();
|
|
909
|
+
};
|
|
910
|
+
proxyClient.on("ready", () => {
|
|
911
|
+
proxyClient.forwardOut("127.0.0.1", 0, targetOpts.hostname, targetOpts.port, (err, tunnel) => {
|
|
912
|
+
if (err) {
|
|
913
|
+
if (!done) {
|
|
914
|
+
done = true;
|
|
915
|
+
cleanup();
|
|
916
|
+
resolve({ stdout: "", stderr: err.message, exitCode: -1 });
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const targetClient = new Client();
|
|
921
|
+
let stdout = "";
|
|
922
|
+
let stderr = "";
|
|
923
|
+
targetClient.on("ready", () => {
|
|
924
|
+
targetClient.exec(command, (execErr, stream) => {
|
|
925
|
+
if (execErr) {
|
|
926
|
+
if (!done) {
|
|
927
|
+
done = true;
|
|
928
|
+
targetClient.end();
|
|
929
|
+
cleanup();
|
|
930
|
+
resolve({ stdout, stderr, exitCode: -1 });
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
stream.on("data", (d) => {
|
|
935
|
+
stdout += d.toString();
|
|
936
|
+
});
|
|
937
|
+
stream.stderr.on("data", (d) => {
|
|
938
|
+
stderr += d.toString();
|
|
939
|
+
});
|
|
940
|
+
stream.on("close", (code) => {
|
|
941
|
+
if (!done) {
|
|
942
|
+
done = true;
|
|
943
|
+
targetClient.end();
|
|
944
|
+
cleanup();
|
|
945
|
+
resolve({ stdout, stderr, exitCode: code ?? 0 });
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
targetClient.on("error", (targetErr) => {
|
|
951
|
+
if (!done) {
|
|
952
|
+
done = true;
|
|
953
|
+
targetClient.end();
|
|
954
|
+
cleanup();
|
|
955
|
+
resolve({ stdout, stderr: targetErr.message, exitCode: -1 });
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
targetClient.connect({
|
|
959
|
+
sock: tunnel,
|
|
960
|
+
username: targetOpts.username,
|
|
961
|
+
password: targetOpts.password,
|
|
962
|
+
privateKey: targetOpts.privateKey,
|
|
963
|
+
passphrase: targetOpts.passphrase,
|
|
964
|
+
readyTimeout: timeout
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
proxyClient.on("error", (err) => {
|
|
969
|
+
if (!done) {
|
|
970
|
+
done = true;
|
|
971
|
+
cleanup();
|
|
972
|
+
resolve({ stdout: "", stderr: err.message, exitCode: -1 });
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
proxyClient.connect({
|
|
976
|
+
host: proxyOpts.hostname,
|
|
977
|
+
port: proxyOpts.port,
|
|
978
|
+
username: proxyOpts.username,
|
|
979
|
+
password: proxyOpts.password,
|
|
980
|
+
privateKey: proxyOpts.privateKey,
|
|
981
|
+
passphrase: proxyOpts.passphrase,
|
|
982
|
+
readyTimeout: proxyOpts.timeout || 3e4
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
}
|
|
462
986
|
function sanitizePath(path) {
|
|
463
987
|
let normalized = path.replace(/\\/g, "/").replace(/\0/g, "");
|
|
464
988
|
const parts = normalized.split("/");
|
|
@@ -743,7 +1267,7 @@ function assertAllowedLogPath(filePath) {
|
|
|
743
1267
|
throw new Error(`Path not allowed. Must be under: ${ALLOWED_LOG_PREFIXES.join(", ")}`);
|
|
744
1268
|
}
|
|
745
1269
|
}
|
|
746
|
-
async function discoverSiteDatabases(conn) {
|
|
1270
|
+
async function discoverSiteDatabases(conn, proxy) {
|
|
747
1271
|
const script = `
|
|
748
1272
|
check_dir() {
|
|
749
1273
|
local base="$1" root="$2"
|
|
@@ -780,7 +1304,7 @@ for dir in /var/www/*/; do
|
|
|
780
1304
|
done
|
|
781
1305
|
done
|
|
782
1306
|
`.trim();
|
|
783
|
-
const result = await sshExec(conn, script);
|
|
1307
|
+
const result = await sshExec(conn, script, proxy);
|
|
784
1308
|
const sites = [];
|
|
785
1309
|
for (const line of result.stdout.split("\n")) {
|
|
786
1310
|
if (!line.trim()) continue;
|
|
@@ -864,9 +1388,9 @@ DB_PORT=\${DB_PORT:-3306}
|
|
|
864
1388
|
mysql --user="$DB_USER" --password="$DB_PASS" --host="$DB_HOST" --port="$DB_PORT" -t -e '${safeQuery}' "$DB_NAME" 2>&1 | grep -v "\\[Warning\\].*password"
|
|
865
1389
|
`.trim();
|
|
866
1390
|
}
|
|
867
|
-
async function execSiteMysql(conn, sitePath, query) {
|
|
1391
|
+
async function execSiteMysql(conn, sitePath, query, proxy) {
|
|
868
1392
|
const cmd = buildSiteMysqlCommand(sitePath, query);
|
|
869
|
-
const result = await sshExec(conn, cmd);
|
|
1393
|
+
const result = await sshExec(conn, cmd, proxy);
|
|
870
1394
|
const output = (result.stdout || "").trim();
|
|
871
1395
|
if (output.startsWith("ERROR: No database config found")) {
|
|
872
1396
|
throw new Error(output);
|
|
@@ -891,7 +1415,7 @@ async function mijnhostFetch(path, options = {}) {
|
|
|
891
1415
|
"API-Key": key,
|
|
892
1416
|
"Accept": "application/json",
|
|
893
1417
|
"Content-Type": "application/json",
|
|
894
|
-
"User-Agent": "mg-dashboard-mcp/
|
|
1418
|
+
"User-Agent": "mg-dashboard-mcp/2.2.0",
|
|
895
1419
|
...options.headers || {}
|
|
896
1420
|
}
|
|
897
1421
|
});
|
|
@@ -1291,10 +1815,12 @@ var TOOLS = [
|
|
|
1291
1815
|
},
|
|
1292
1816
|
required: ["domain", "type", "name", "value"]
|
|
1293
1817
|
}
|
|
1294
|
-
}
|
|
1818
|
+
},
|
|
1819
|
+
// ----- Trigger.dev -----
|
|
1820
|
+
...TRIGGER_TOOLS
|
|
1295
1821
|
];
|
|
1296
1822
|
var server = new Server(
|
|
1297
|
-
{ name: "mg-dashboard-mcp", version: "
|
|
1823
|
+
{ name: "mg-dashboard-mcp", version: "2.2.0" },
|
|
1298
1824
|
{ capabilities: { tools: {} } }
|
|
1299
1825
|
);
|
|
1300
1826
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -1338,9 +1864,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1338
1864
|
return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No servers found" }] };
|
|
1339
1865
|
}
|
|
1340
1866
|
case "server-status": {
|
|
1341
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1867
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1342
1868
|
const cmd = 'echo "=== UPTIME ===" && uptime && echo "=== DISK ===" && df -h --total && echo "=== MEMORY ===" && free -h && echo "=== LOAD ===" && cat /proc/loadavg';
|
|
1343
|
-
const result = await sshExec(conn, cmd);
|
|
1869
|
+
const result = await sshExec(conn, cmd, proxy);
|
|
1344
1870
|
const output = result.exitCode === 0 ? result.stdout : `Exit ${result.exitCode}
|
|
1345
1871
|
${result.stdout}
|
|
1346
1872
|
${result.stderr}`;
|
|
@@ -1350,9 +1876,9 @@ ${result.stderr}`;
|
|
|
1350
1876
|
case "ssh-execute": {
|
|
1351
1877
|
const command = String(a.command);
|
|
1352
1878
|
assertSafeCommand(command);
|
|
1353
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1879
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1354
1880
|
if (a.timeout) conn.timeout = Number(a.timeout);
|
|
1355
|
-
const result = await sshExec(conn, command);
|
|
1881
|
+
const result = await sshExec(conn, command, proxy);
|
|
1356
1882
|
const output = [`Exit code: ${result.exitCode}`];
|
|
1357
1883
|
if (result.stdout) output.push(`--- stdout ---
|
|
1358
1884
|
${result.stdout}`);
|
|
@@ -1361,45 +1887,45 @@ ${result.stderr}`);
|
|
|
1361
1887
|
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
1362
1888
|
}
|
|
1363
1889
|
case "server-reboot": {
|
|
1364
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1365
|
-
const result = await sshExec(conn, "sudo reboot");
|
|
1890
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1891
|
+
const result = await sshExec(conn, "sudo reboot", proxy);
|
|
1366
1892
|
return { content: [{ type: "text", text: result.exitCode === 0 ? "Reboot command sent. Server will be unavailable shortly." : `Reboot failed: ${result.stderr}` }] };
|
|
1367
1893
|
}
|
|
1368
1894
|
case "server-restart-service": {
|
|
1369
1895
|
const service = String(a.serviceName).replace(/[^a-zA-Z0-9._@-]/g, "");
|
|
1370
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1371
|
-
const result = await sshExec(conn, `sudo systemctl restart ${service}
|
|
1896
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1897
|
+
const result = await sshExec(conn, `sudo systemctl restart ${service}`, proxy);
|
|
1372
1898
|
if (result.exitCode === 0) {
|
|
1373
|
-
const status = await sshExec(conn, `sudo systemctl is-active ${service}
|
|
1899
|
+
const status = await sshExec(conn, `sudo systemctl is-active ${service}`, proxy);
|
|
1374
1900
|
return { content: [{ type: "text", text: `Service "${service}" restarted. Status: ${status.stdout.trim()}` }] };
|
|
1375
1901
|
}
|
|
1376
1902
|
return { content: [{ type: "text", text: `Failed to restart "${service}": ${result.stderr}` }] };
|
|
1377
1903
|
}
|
|
1378
|
-
// ----- SFTP -----
|
|
1904
|
+
// ----- SFTP (no proxy support yet — direct connection only) -----
|
|
1379
1905
|
case "sftp-list": {
|
|
1380
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1906
|
+
const { conn } = await getServerConnection(String(a.serverId));
|
|
1381
1907
|
const listing = await sftpReaddir(conn, String(a.path || "/"));
|
|
1382
1908
|
return { content: [{ type: "text", text: listing }] };
|
|
1383
1909
|
}
|
|
1384
1910
|
case "sftp-read": {
|
|
1385
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1911
|
+
const { conn } = await getServerConnection(String(a.serverId));
|
|
1386
1912
|
const content = await sftpRead(conn, String(a.path));
|
|
1387
1913
|
return { content: [{ type: "text", text: content }] };
|
|
1388
1914
|
}
|
|
1389
1915
|
case "sftp-write": {
|
|
1390
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1916
|
+
const { conn } = await getServerConnection(String(a.serverId));
|
|
1391
1917
|
const result = await sftpWrite(conn, String(a.path), String(a.content));
|
|
1392
1918
|
return { content: [{ type: "text", text: result }] };
|
|
1393
1919
|
}
|
|
1394
1920
|
case "sftp-delete": {
|
|
1395
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1921
|
+
const { conn } = await getServerConnection(String(a.serverId));
|
|
1396
1922
|
const result = await sftpDelete(conn, String(a.path));
|
|
1397
1923
|
return { content: [{ type: "text", text: result }] };
|
|
1398
1924
|
}
|
|
1399
1925
|
// ----- Docker -----
|
|
1400
1926
|
case "docker-list": {
|
|
1401
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1402
|
-
const result = await sshExec(conn, 'docker ps -a --format "table {{.Names}} {{.Image}} {{.Status}} {{.Ports}}"');
|
|
1927
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1928
|
+
const result = await sshExec(conn, 'docker ps -a --format "table {{.Names}} {{.Image}} {{.Status}} {{.Ports}}"', proxy);
|
|
1403
1929
|
return { content: [{ type: "text", text: result.exitCode === 0 ? result.stdout : `Error: ${result.stderr}` }] };
|
|
1404
1930
|
}
|
|
1405
1931
|
case "docker-action": {
|
|
@@ -1408,22 +1934,22 @@ ${result.stderr}`);
|
|
|
1408
1934
|
if (!["start", "stop", "restart", "remove"].includes(action)) {
|
|
1409
1935
|
throw new Error(`Invalid action: ${action}. Use start, stop, restart, or remove.`);
|
|
1410
1936
|
}
|
|
1411
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1937
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1412
1938
|
const dockerCmd = action === "remove" ? `docker rm -f ${container}` : `docker ${action} ${container}`;
|
|
1413
|
-
const result = await sshExec(conn, dockerCmd);
|
|
1939
|
+
const result = await sshExec(conn, dockerCmd, proxy);
|
|
1414
1940
|
return { content: [{ type: "text", text: result.exitCode === 0 ? `Container "${container}" ${action}ed successfully` : `Error: ${result.stderr}` }] };
|
|
1415
1941
|
}
|
|
1416
1942
|
case "docker-logs": {
|
|
1417
1943
|
const container = String(a.containerName).replace(/[^a-zA-Z0-9._-]/g, "");
|
|
1418
1944
|
const lines = Number(a.lines) || 100;
|
|
1419
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1420
|
-
const result = await sshExec(conn, `docker logs --tail ${lines} ${container} 2>&1
|
|
1945
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1946
|
+
const result = await sshExec(conn, `docker logs --tail ${lines} ${container} 2>&1`, proxy);
|
|
1421
1947
|
return { content: [{ type: "text", text: result.exitCode === 0 ? result.stdout : `Error: ${result.stderr}` }] };
|
|
1422
1948
|
}
|
|
1423
1949
|
// ----- Database -----
|
|
1424
1950
|
case "db-discover": {
|
|
1425
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1426
|
-
const sites = await discoverSiteDatabases(conn);
|
|
1951
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1952
|
+
const sites = await discoverSiteDatabases(conn, proxy);
|
|
1427
1953
|
if (!sites.length) {
|
|
1428
1954
|
return { content: [{ type: "text", text: "No web applications with database configs found in /var/www/" }] };
|
|
1429
1955
|
}
|
|
@@ -1433,26 +1959,27 @@ ${result.stderr}`);
|
|
|
1433
1959
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1434
1960
|
}
|
|
1435
1961
|
case "db-tables": {
|
|
1436
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1962
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1437
1963
|
const sql = "SELECT TABLE_NAME, ENGINE, TABLE_ROWS, ROUND(DATA_LENGTH/1024/1024, 2) AS `Size (MB)`, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME";
|
|
1438
|
-
const output = await execSiteMysql(conn, String(a.sitePath), sql);
|
|
1964
|
+
const output = await execSiteMysql(conn, String(a.sitePath), sql, proxy);
|
|
1439
1965
|
return { content: [{ type: "text", text: output || "No tables found" }] };
|
|
1440
1966
|
}
|
|
1441
1967
|
case "db-describe": {
|
|
1442
1968
|
const table = String(a.table).replace(/[^a-zA-Z0-9_]/g, "");
|
|
1443
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1969
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1444
1970
|
const output = await execSiteMysql(
|
|
1445
1971
|
conn,
|
|
1446
1972
|
String(a.sitePath),
|
|
1447
|
-
`DESCRIBE \`${table}\`; SHOW INDEX FROM \`${table}
|
|
1973
|
+
`DESCRIBE \`${table}\`; SHOW INDEX FROM \`${table}\``,
|
|
1974
|
+
proxy
|
|
1448
1975
|
);
|
|
1449
1976
|
return { content: [{ type: "text", text: output }] };
|
|
1450
1977
|
}
|
|
1451
1978
|
case "db-query": {
|
|
1452
1979
|
const query = String(a.query).trim();
|
|
1453
1980
|
assertSafeSql(query);
|
|
1454
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
1455
|
-
const output = await execSiteMysql(conn, String(a.sitePath), query);
|
|
1981
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1982
|
+
const output = await execSiteMysql(conn, String(a.sitePath), query, proxy);
|
|
1456
1983
|
return { content: [{ type: "text", text: output || "Query executed successfully (no output)" }] };
|
|
1457
1984
|
}
|
|
1458
1985
|
// ----- Env Config -----
|
|
@@ -1550,7 +2077,7 @@ ${result.stderr}`);
|
|
|
1550
2077
|
}
|
|
1551
2078
|
// ----- Cache Purge -----
|
|
1552
2079
|
case "cache-purge": {
|
|
1553
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
2080
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1554
2081
|
conn.timeout = 12e4;
|
|
1555
2082
|
const script = `
|
|
1556
2083
|
R=""
|
|
@@ -1631,13 +2158,13 @@ else
|
|
|
1631
2158
|
fi
|
|
1632
2159
|
echo -e "$R"
|
|
1633
2160
|
`.trim();
|
|
1634
|
-
const result = await sshExec(conn, script);
|
|
2161
|
+
const result = await sshExec(conn, script, proxy);
|
|
1635
2162
|
const output = (result.stdout || "").trim();
|
|
1636
2163
|
return { content: [{ type: "text", text: output || "Cache purge completed (no output)" }] };
|
|
1637
2164
|
}
|
|
1638
2165
|
// ----- Log Reading -----
|
|
1639
2166
|
case "log-list": {
|
|
1640
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
2167
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1641
2168
|
const script = `
|
|
1642
2169
|
{
|
|
1643
2170
|
[ -d /usr/local/lsws/logs ] && find /usr/local/lsws/logs -maxdepth 2 -name "*.log" -type f 2>/dev/null
|
|
@@ -1663,7 +2190,7 @@ echo -e "$R"
|
|
|
1663
2190
|
echo "$f $HR $MOD"
|
|
1664
2191
|
done
|
|
1665
2192
|
`.trim();
|
|
1666
|
-
const result = await sshExec(conn, script);
|
|
2193
|
+
const result = await sshExec(conn, script, proxy);
|
|
1667
2194
|
return { content: [{ type: "text", text: result.stdout || "No log files found" }] };
|
|
1668
2195
|
}
|
|
1669
2196
|
case "log-read": {
|
|
@@ -1671,9 +2198,9 @@ done
|
|
|
1671
2198
|
assertAllowedLogPath(logPath);
|
|
1672
2199
|
const lineCount = Math.min(Math.max(Number(a.lines) || 100, 1), 500);
|
|
1673
2200
|
const filter = a.filter ? String(a.filter).replace(/'/g, "'\\''") : "";
|
|
1674
|
-
const conn = await getServerConnection(String(a.serverId));
|
|
2201
|
+
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
1675
2202
|
const cmd = filter ? `grep '${filter}' '${logPath}' 2>/dev/null | tail -n ${lineCount}` : `tail -n ${lineCount} '${logPath}' 2>/dev/null`;
|
|
1676
|
-
const result = await sshExec(conn, cmd);
|
|
2203
|
+
const result = await sshExec(conn, cmd, proxy);
|
|
1677
2204
|
if (result.exitCode !== 0 && !result.stdout) {
|
|
1678
2205
|
throw new Error(result.stderr || `Failed to read log: ${logPath}`);
|
|
1679
2206
|
}
|
|
@@ -1681,21 +2208,21 @@ done
|
|
|
1681
2208
|
}
|
|
1682
2209
|
// ----- Cron Jobs -----
|
|
1683
2210
|
case "cron-list": {
|
|
1684
|
-
const conn = await getServerConnection(a.serverId);
|
|
2211
|
+
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1685
2212
|
const user = a.user ? String(a.user) : null;
|
|
1686
2213
|
const parts = [];
|
|
1687
2214
|
if (user) {
|
|
1688
|
-
const result = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null || echo '(no crontab for ${user})'
|
|
2215
|
+
const result = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null || echo '(no crontab for ${user})'`, proxy);
|
|
1689
2216
|
parts.push(`## Crontab for ${user}
|
|
1690
2217
|
${result.stdout}`);
|
|
1691
2218
|
} else {
|
|
1692
|
-
const rootCron = await sshExec(conn, `crontab -l 2>/dev/null || echo '(no crontab for root)'
|
|
2219
|
+
const rootCron = await sshExec(conn, `crontab -l 2>/dev/null || echo '(no crontab for root)'`, proxy);
|
|
1693
2220
|
parts.push(`## Root crontab
|
|
1694
2221
|
${rootCron.stdout}`);
|
|
1695
|
-
const wwwCron = await sshExec(conn, `crontab -l -u www-data 2>/dev/null || echo '(no crontab for www-data)'
|
|
2222
|
+
const wwwCron = await sshExec(conn, `crontab -l -u www-data 2>/dev/null || echo '(no crontab for www-data)'`, proxy);
|
|
1696
2223
|
parts.push(`## www-data crontab
|
|
1697
2224
|
${wwwCron.stdout}`);
|
|
1698
|
-
const cronD = await sshExec(conn, `for f in /etc/cron.d/*; do [ -f "$f" ] && echo "--- $f ---" && cat "$f" && echo; done 2>/dev/null || echo '(no files in /etc/cron.d/)'
|
|
2225
|
+
const cronD = await sshExec(conn, `for f in /etc/cron.d/*; do [ -f "$f" ] && echo "--- $f ---" && cat "$f" && echo; done 2>/dev/null || echo '(no files in /etc/cron.d/)'`, proxy);
|
|
1699
2226
|
parts.push(`## /etc/cron.d/
|
|
1700
2227
|
${cronD.stdout}`);
|
|
1701
2228
|
const summary = await sshExec(conn, [
|
|
@@ -1704,23 +2231,23 @@ ${cronD.stdout}`);
|
|
|
1704
2231
|
`echo "cron.daily: $(ls /etc/cron.daily/ 2>/dev/null | wc -l) scripts"`,
|
|
1705
2232
|
`echo "cron.weekly: $(ls /etc/cron.weekly/ 2>/dev/null | wc -l) scripts"`,
|
|
1706
2233
|
`echo "cron.monthly: $(ls /etc/cron.monthly/ 2>/dev/null | wc -l) scripts"`
|
|
1707
|
-
].join(" && "));
|
|
2234
|
+
].join(" && "), proxy);
|
|
1708
2235
|
parts.push(summary.stdout);
|
|
1709
2236
|
}
|
|
1710
2237
|
return { content: [{ type: "text", text: parts.join("\n\n") }] };
|
|
1711
2238
|
}
|
|
1712
2239
|
case "cron-add": {
|
|
1713
|
-
|
|
2240
|
+
if (!a.schedule || !a.command) {
|
|
2241
|
+
throw new Error("Both schedule and command are required");
|
|
2242
|
+
}
|
|
2243
|
+
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1714
2244
|
const user = a.user ? String(a.user) : "root";
|
|
1715
2245
|
const schedule = String(a.schedule).trim();
|
|
1716
2246
|
const command = String(a.command).trim();
|
|
1717
2247
|
const commentText = a.comment ? String(a.comment).trim() : "";
|
|
1718
|
-
if (!schedule || !command) {
|
|
1719
|
-
throw new Error("Both schedule and command are required");
|
|
1720
|
-
}
|
|
1721
2248
|
const entry = commentText ? `# ${commentText}
|
|
1722
2249
|
${schedule} ${command}` : `${schedule} ${command}`;
|
|
1723
|
-
const result = await sshExec(conn, `(crontab -l -u ${user} 2>/dev/null; echo '${entry.replace(/'/g, "'\\''")}') | crontab -u ${user}
|
|
2250
|
+
const result = await sshExec(conn, `(crontab -l -u ${user} 2>/dev/null; echo '${entry.replace(/'/g, "'\\''")}') | crontab -u ${user} -`, proxy);
|
|
1724
2251
|
if (result.exitCode !== 0) {
|
|
1725
2252
|
throw new Error(result.stderr || "Failed to add cron job");
|
|
1726
2253
|
}
|
|
@@ -1728,13 +2255,13 @@ ${schedule} ${command}` : `${schedule} ${command}`;
|
|
|
1728
2255
|
${schedule} ${command}` }] };
|
|
1729
2256
|
}
|
|
1730
2257
|
case "cron-remove": {
|
|
1731
|
-
|
|
2258
|
+
if (!a.commandMatch) {
|
|
2259
|
+
throw new Error("commandMatch is required \u2013 pass a (partial) command string to identify the cron entry");
|
|
2260
|
+
}
|
|
2261
|
+
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1732
2262
|
const user = a.user ? String(a.user) : "root";
|
|
1733
2263
|
const match = String(a.commandMatch).trim();
|
|
1734
|
-
|
|
1735
|
-
throw new Error("commandMatch is required");
|
|
1736
|
-
}
|
|
1737
|
-
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`);
|
|
2264
|
+
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
|
|
1738
2265
|
const lines = current.stdout.split("\n");
|
|
1739
2266
|
const before = lines.length;
|
|
1740
2267
|
const filtered = lines.filter((line) => {
|
|
@@ -1746,21 +2273,21 @@ ${schedule} ${command}` }] };
|
|
|
1746
2273
|
return { content: [{ type: "text", text: `No cron entries found matching "${match}"` }] };
|
|
1747
2274
|
}
|
|
1748
2275
|
const escapedCrontab = filtered.join("\n").replace(/'/g, "'\\''");
|
|
1749
|
-
const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user}
|
|
2276
|
+
const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`, proxy);
|
|
1750
2277
|
if (result.exitCode !== 0) {
|
|
1751
2278
|
throw new Error(result.stderr || "Failed to update crontab");
|
|
1752
2279
|
}
|
|
1753
2280
|
return { content: [{ type: "text", text: `Removed ${removed} cron entry/entries matching "${match}" from ${user} crontab` }] };
|
|
1754
2281
|
}
|
|
1755
2282
|
case "cron-toggle": {
|
|
1756
|
-
|
|
2283
|
+
if (!a.commandMatch) {
|
|
2284
|
+
throw new Error("commandMatch is required \u2013 pass a (partial) command string to identify the cron entry");
|
|
2285
|
+
}
|
|
2286
|
+
const { conn, proxy } = await getServerConnection(a.serverId);
|
|
1757
2287
|
const user = a.user ? String(a.user) : "root";
|
|
1758
2288
|
const match = String(a.commandMatch).trim();
|
|
1759
2289
|
const enable = Boolean(a.enable);
|
|
1760
|
-
|
|
1761
|
-
throw new Error("commandMatch is required");
|
|
1762
|
-
}
|
|
1763
|
-
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`);
|
|
2290
|
+
const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
|
|
1764
2291
|
const lines = current.stdout.split("\n");
|
|
1765
2292
|
let toggled = 0;
|
|
1766
2293
|
const updated = lines.map((line) => {
|
|
@@ -1779,7 +2306,7 @@ ${schedule} ${command}` }] };
|
|
|
1779
2306
|
return { content: [{ type: "text", text: `No ${state} cron entries found matching "${match}"` }] };
|
|
1780
2307
|
}
|
|
1781
2308
|
const escapedCrontab = updated.join("\n").replace(/'/g, "'\\''");
|
|
1782
|
-
const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user}
|
|
2309
|
+
const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`, proxy);
|
|
1783
2310
|
if (result.exitCode !== 0) {
|
|
1784
2311
|
throw new Error(result.stderr || "Failed to update crontab");
|
|
1785
2312
|
}
|
|
@@ -1924,6 +2451,9 @@ ${lines.join("\n")}` }] };
|
|
|
1924
2451
|
return { content: [{ type: "text", text: `DNS record deleted: ${type} ${dnsName} = ${value} (${remaining.length} records remaining)` }] };
|
|
1925
2452
|
}
|
|
1926
2453
|
default:
|
|
2454
|
+
if (TRIGGER_TOOL_NAMES.has(name)) {
|
|
2455
|
+
return handleTriggerTool(name, a, { sshExec, getServerConnection });
|
|
2456
|
+
}
|
|
1927
2457
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
1928
2458
|
}
|
|
1929
2459
|
} catch (err) {
|