@mgsoftwarebv/mg-dashboard-mcp 2.1.1 → 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) {
@@ -1004,7 +1415,7 @@ async function mijnhostFetch(path, options = {}) {
1004
1415
  "API-Key": key,
1005
1416
  "Accept": "application/json",
1006
1417
  "Content-Type": "application/json",
1007
- "User-Agent": "mg-dashboard-mcp/1.7.0",
1418
+ "User-Agent": "mg-dashboard-mcp/2.2.0",
1008
1419
  ...options.headers || {}
1009
1420
  }
1010
1421
  });
@@ -1404,10 +1815,12 @@ var TOOLS = [
1404
1815
  },
1405
1816
  required: ["domain", "type", "name", "value"]
1406
1817
  }
1407
- }
1818
+ },
1819
+ // ----- Trigger.dev -----
1820
+ ...TRIGGER_TOOLS
1408
1821
  ];
1409
1822
  var server = new Server(
1410
- { name: "mg-dashboard-mcp", version: "1.7.0" },
1823
+ { name: "mg-dashboard-mcp", version: "2.2.0" },
1411
1824
  { capabilities: { tools: {} } }
1412
1825
  );
1413
1826
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -1824,14 +2237,14 @@ ${cronD.stdout}`);
1824
2237
  return { content: [{ type: "text", text: parts.join("\n\n") }] };
1825
2238
  }
1826
2239
  case "cron-add": {
2240
+ if (!a.schedule || !a.command) {
2241
+ throw new Error("Both schedule and command are required");
2242
+ }
1827
2243
  const { conn, proxy } = await getServerConnection(a.serverId);
1828
2244
  const user = a.user ? String(a.user) : "root";
1829
2245
  const schedule = String(a.schedule).trim();
1830
2246
  const command = String(a.command).trim();
1831
2247
  const commentText = a.comment ? String(a.comment).trim() : "";
1832
- if (!schedule || !command) {
1833
- throw new Error("Both schedule and command are required");
1834
- }
1835
2248
  const entry = commentText ? `# ${commentText}
1836
2249
  ${schedule} ${command}` : `${schedule} ${command}`;
1837
2250
  const result = await sshExec(conn, `(crontab -l -u ${user} 2>/dev/null; echo '${entry.replace(/'/g, "'\\''")}') | crontab -u ${user} -`, proxy);
@@ -1842,12 +2255,12 @@ ${schedule} ${command}` : `${schedule} ${command}`;
1842
2255
  ${schedule} ${command}` }] };
1843
2256
  }
1844
2257
  case "cron-remove": {
2258
+ if (!a.commandMatch) {
2259
+ throw new Error("commandMatch is required \u2013 pass a (partial) command string to identify the cron entry");
2260
+ }
1845
2261
  const { conn, proxy } = await getServerConnection(a.serverId);
1846
2262
  const user = a.user ? String(a.user) : "root";
1847
2263
  const match = String(a.commandMatch).trim();
1848
- if (!match) {
1849
- throw new Error("commandMatch is required");
1850
- }
1851
2264
  const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
1852
2265
  const lines = current.stdout.split("\n");
1853
2266
  const before = lines.length;
@@ -1867,13 +2280,13 @@ ${schedule} ${command}` }] };
1867
2280
  return { content: [{ type: "text", text: `Removed ${removed} cron entry/entries matching "${match}" from ${user} crontab` }] };
1868
2281
  }
1869
2282
  case "cron-toggle": {
2283
+ if (!a.commandMatch) {
2284
+ throw new Error("commandMatch is required \u2013 pass a (partial) command string to identify the cron entry");
2285
+ }
1870
2286
  const { conn, proxy } = await getServerConnection(a.serverId);
1871
2287
  const user = a.user ? String(a.user) : "root";
1872
2288
  const match = String(a.commandMatch).trim();
1873
2289
  const enable = Boolean(a.enable);
1874
- if (!match) {
1875
- throw new Error("commandMatch is required");
1876
- }
1877
2290
  const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
1878
2291
  const lines = current.stdout.split("\n");
1879
2292
  let toggled = 0;
@@ -2038,6 +2451,9 @@ ${lines.join("\n")}` }] };
2038
2451
  return { content: [{ type: "text", text: `DNS record deleted: ${type} ${dnsName} = ${value} (${remaining.length} records remaining)` }] };
2039
2452
  }
2040
2453
  default:
2454
+ if (TRIGGER_TOOL_NAMES.has(name)) {
2455
+ return handleTriggerTool(name, a, { sshExec, getServerConnection });
2456
+ }
2041
2457
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
2042
2458
  }
2043
2459
  } catch (err) {