@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 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
- return {
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/1.7.0",
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: "1.7.0" },
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
- const conn = await getServerConnection(a.serverId);
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
- const conn = await getServerConnection(a.serverId);
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
- if (!match) {
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
- const conn = await getServerConnection(a.serverId);
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
- if (!match) {
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) {