@mgsoftwarebv/mg-dashboard-mcp 2.1.1 → 2.2.1

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