@mgsoftwarebv/mg-dashboard-mcp 2.0.4 → 2.1.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
@@ -311,10 +311,13 @@ async function syncEnvVarsToVercel(token, projectId, envVars) {
311
311
  const data = await res.json().catch(() => ({}));
312
312
  return { created: data?.created?.length ?? envVars.length, error: null };
313
313
  }
314
- async function attemptVercelSync(appName, environment) {
314
+ async function attemptVercelSync(appName, environment, knownStageId) {
315
315
  try {
316
- const { data: direct } = await supabase.from("env_config").select("release_profile_stage_id").eq("app_name", appName).not("release_profile_stage_id", "is", null).limit(1).single();
317
- const stageId = direct?.release_profile_stage_id;
316
+ let stageId = knownStageId;
317
+ if (!stageId) {
318
+ const { data: direct } = await supabase.from("env_config").select("release_profile_stage_id").eq("app_name", appName).not("release_profile_stage_id", "is", null).limit(1).single();
319
+ stageId = direct?.release_profile_stage_id;
320
+ }
318
321
  if (!stageId) return "Vercel sync skipped: no stage link found";
319
322
  const { data: settings } = await supabase.from("app_setting").select("vercel_token_encrypted").maybeSingle();
320
323
  if (!settings?.vercel_token_encrypted) return "Vercel sync skipped: no Vercel token configured";
@@ -383,12 +386,29 @@ async function attemptVercelSync(appName, environment) {
383
386
  return `Vercel sync error: ${msg}`;
384
387
  }
385
388
  }
389
+ var SSH_PROXY_SERVER_ID = "03659d55-e194-400d-b82a-bf6457371ded";
390
+ var _proxyConnCache = null;
391
+ async function getProxyConnection() {
392
+ if (_proxyConnCache) return _proxyConnCache;
393
+ 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();
394
+ if (error || !data) throw new Error("SSH Proxy server not found in database");
395
+ if (!encryptionKey) throw new Error("ENCRYPTION_KEY required to decrypt server credentials");
396
+ _proxyConnCache = {
397
+ hostname: data.hostname,
398
+ port: data.port || 22,
399
+ username: data.username,
400
+ password: data.password_encrypted ? decrypt(data.password_encrypted) : void 0,
401
+ privateKey: data.ssh_key_encrypted ? decrypt(data.ssh_key_encrypted) : void 0,
402
+ passphrase: data.ssh_key_passphrase_encrypted ? decrypt(data.ssh_key_passphrase_encrypted) : void 0
403
+ };
404
+ return _proxyConnCache;
405
+ }
386
406
  async function getServerConnection(serverId) {
387
407
  assertServerAccess(serverId);
388
- 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();
408
+ 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();
389
409
  if (error || !data) throw new Error(`Server not found: ${serverId}`);
390
410
  if (!encryptionKey) throw new Error("ENCRYPTION_KEY required to decrypt server credentials");
391
- return {
411
+ const conn = {
392
412
  hostname: data.hostname,
393
413
  port: data.port || 22,
394
414
  username: data.username,
@@ -396,8 +416,12 @@ async function getServerConnection(serverId) {
396
416
  privateKey: data.ssh_key_encrypted ? decrypt(data.ssh_key_encrypted) : void 0,
397
417
  passphrase: data.ssh_key_passphrase_encrypted ? decrypt(data.ssh_key_passphrase_encrypted) : void 0
398
418
  };
419
+ const needsProxy = data.allowed_ssh_ips !== null && serverId !== SSH_PROXY_SERVER_ID;
420
+ const proxy = needsProxy ? await getProxyConnection() : void 0;
421
+ return { conn, proxy };
399
422
  }
400
- async function sshExec(opts, command) {
423
+ async function sshExec(opts, command, proxy) {
424
+ if (proxy) return sshExecViaProxy(proxy, opts, command);
401
425
  return new Promise((resolve) => {
402
426
  const ssh = new Client();
403
427
  let stdout = "";
@@ -456,6 +480,98 @@ async function sshExec(opts, command) {
456
480
  });
457
481
  });
458
482
  }
483
+ function sshExecViaProxy(proxyOpts, targetOpts, command) {
484
+ return new Promise((resolve) => {
485
+ const proxyClient = new Client();
486
+ let done = false;
487
+ const timeout = targetOpts.timeout || 6e4;
488
+ const timer = setTimeout(() => {
489
+ if (!done) {
490
+ done = true;
491
+ proxyClient.end();
492
+ resolve({ stdout: "", stderr: "SSH proxy command timeout", exitCode: -1 });
493
+ }
494
+ }, timeout);
495
+ const cleanup = () => {
496
+ clearTimeout(timer);
497
+ proxyClient.end();
498
+ };
499
+ proxyClient.on("ready", () => {
500
+ proxyClient.forwardOut("127.0.0.1", 0, targetOpts.hostname, targetOpts.port, (err, tunnel) => {
501
+ if (err) {
502
+ if (!done) {
503
+ done = true;
504
+ cleanup();
505
+ resolve({ stdout: "", stderr: err.message, exitCode: -1 });
506
+ }
507
+ return;
508
+ }
509
+ const targetClient = new Client();
510
+ let stdout = "";
511
+ let stderr = "";
512
+ targetClient.on("ready", () => {
513
+ targetClient.exec(command, (execErr, stream) => {
514
+ if (execErr) {
515
+ if (!done) {
516
+ done = true;
517
+ targetClient.end();
518
+ cleanup();
519
+ resolve({ stdout, stderr, exitCode: -1 });
520
+ }
521
+ return;
522
+ }
523
+ stream.on("data", (d) => {
524
+ stdout += d.toString();
525
+ });
526
+ stream.stderr.on("data", (d) => {
527
+ stderr += d.toString();
528
+ });
529
+ stream.on("close", (code) => {
530
+ if (!done) {
531
+ done = true;
532
+ targetClient.end();
533
+ cleanup();
534
+ resolve({ stdout, stderr, exitCode: code ?? 0 });
535
+ }
536
+ });
537
+ });
538
+ });
539
+ targetClient.on("error", (targetErr) => {
540
+ if (!done) {
541
+ done = true;
542
+ targetClient.end();
543
+ cleanup();
544
+ resolve({ stdout, stderr: targetErr.message, exitCode: -1 });
545
+ }
546
+ });
547
+ targetClient.connect({
548
+ sock: tunnel,
549
+ username: targetOpts.username,
550
+ password: targetOpts.password,
551
+ privateKey: targetOpts.privateKey,
552
+ passphrase: targetOpts.passphrase,
553
+ readyTimeout: timeout
554
+ });
555
+ });
556
+ });
557
+ proxyClient.on("error", (err) => {
558
+ if (!done) {
559
+ done = true;
560
+ cleanup();
561
+ resolve({ stdout: "", stderr: err.message, exitCode: -1 });
562
+ }
563
+ });
564
+ proxyClient.connect({
565
+ host: proxyOpts.hostname,
566
+ port: proxyOpts.port,
567
+ username: proxyOpts.username,
568
+ password: proxyOpts.password,
569
+ privateKey: proxyOpts.privateKey,
570
+ passphrase: proxyOpts.passphrase,
571
+ readyTimeout: proxyOpts.timeout || 3e4
572
+ });
573
+ });
574
+ }
459
575
  function sanitizePath(path) {
460
576
  let normalized = path.replace(/\\/g, "/").replace(/\0/g, "");
461
577
  const parts = normalized.split("/");
@@ -740,7 +856,7 @@ function assertAllowedLogPath(filePath) {
740
856
  throw new Error(`Path not allowed. Must be under: ${ALLOWED_LOG_PREFIXES.join(", ")}`);
741
857
  }
742
858
  }
743
- async function discoverSiteDatabases(conn) {
859
+ async function discoverSiteDatabases(conn, proxy) {
744
860
  const script = `
745
861
  check_dir() {
746
862
  local base="$1" root="$2"
@@ -777,7 +893,7 @@ for dir in /var/www/*/; do
777
893
  done
778
894
  done
779
895
  `.trim();
780
- const result = await sshExec(conn, script);
896
+ const result = await sshExec(conn, script, proxy);
781
897
  const sites = [];
782
898
  for (const line of result.stdout.split("\n")) {
783
899
  if (!line.trim()) continue;
@@ -861,9 +977,9 @@ DB_PORT=\${DB_PORT:-3306}
861
977
  mysql --user="$DB_USER" --password="$DB_PASS" --host="$DB_HOST" --port="$DB_PORT" -t -e '${safeQuery}' "$DB_NAME" 2>&1 | grep -v "\\[Warning\\].*password"
862
978
  `.trim();
863
979
  }
864
- async function execSiteMysql(conn, sitePath, query) {
980
+ async function execSiteMysql(conn, sitePath, query, proxy) {
865
981
  const cmd = buildSiteMysqlCommand(sitePath, query);
866
- const result = await sshExec(conn, cmd);
982
+ const result = await sshExec(conn, cmd, proxy);
867
983
  const output = (result.stdout || "").trim();
868
984
  if (output.startsWith("ERROR: No database config found")) {
869
985
  throw new Error(output);
@@ -1335,9 +1451,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1335
1451
  return { content: [{ type: "text", text: lines.length ? lines.join("\n") : "No servers found" }] };
1336
1452
  }
1337
1453
  case "server-status": {
1338
- const conn = await getServerConnection(String(a.serverId));
1454
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1339
1455
  const cmd = 'echo "=== UPTIME ===" && uptime && echo "=== DISK ===" && df -h --total && echo "=== MEMORY ===" && free -h && echo "=== LOAD ===" && cat /proc/loadavg';
1340
- const result = await sshExec(conn, cmd);
1456
+ const result = await sshExec(conn, cmd, proxy);
1341
1457
  const output = result.exitCode === 0 ? result.stdout : `Exit ${result.exitCode}
1342
1458
  ${result.stdout}
1343
1459
  ${result.stderr}`;
@@ -1347,9 +1463,9 @@ ${result.stderr}`;
1347
1463
  case "ssh-execute": {
1348
1464
  const command = String(a.command);
1349
1465
  assertSafeCommand(command);
1350
- const conn = await getServerConnection(String(a.serverId));
1466
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1351
1467
  if (a.timeout) conn.timeout = Number(a.timeout);
1352
- const result = await sshExec(conn, command);
1468
+ const result = await sshExec(conn, command, proxy);
1353
1469
  const output = [`Exit code: ${result.exitCode}`];
1354
1470
  if (result.stdout) output.push(`--- stdout ---
1355
1471
  ${result.stdout}`);
@@ -1358,45 +1474,45 @@ ${result.stderr}`);
1358
1474
  return { content: [{ type: "text", text: output.join("\n") }] };
1359
1475
  }
1360
1476
  case "server-reboot": {
1361
- const conn = await getServerConnection(String(a.serverId));
1362
- const result = await sshExec(conn, "sudo reboot");
1477
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1478
+ const result = await sshExec(conn, "sudo reboot", proxy);
1363
1479
  return { content: [{ type: "text", text: result.exitCode === 0 ? "Reboot command sent. Server will be unavailable shortly." : `Reboot failed: ${result.stderr}` }] };
1364
1480
  }
1365
1481
  case "server-restart-service": {
1366
1482
  const service = String(a.serviceName).replace(/[^a-zA-Z0-9._@-]/g, "");
1367
- const conn = await getServerConnection(String(a.serverId));
1368
- const result = await sshExec(conn, `sudo systemctl restart ${service}`);
1483
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1484
+ const result = await sshExec(conn, `sudo systemctl restart ${service}`, proxy);
1369
1485
  if (result.exitCode === 0) {
1370
- const status = await sshExec(conn, `sudo systemctl is-active ${service}`);
1486
+ const status = await sshExec(conn, `sudo systemctl is-active ${service}`, proxy);
1371
1487
  return { content: [{ type: "text", text: `Service "${service}" restarted. Status: ${status.stdout.trim()}` }] };
1372
1488
  }
1373
1489
  return { content: [{ type: "text", text: `Failed to restart "${service}": ${result.stderr}` }] };
1374
1490
  }
1375
- // ----- SFTP -----
1491
+ // ----- SFTP (no proxy support yet — direct connection only) -----
1376
1492
  case "sftp-list": {
1377
- const conn = await getServerConnection(String(a.serverId));
1493
+ const { conn } = await getServerConnection(String(a.serverId));
1378
1494
  const listing = await sftpReaddir(conn, String(a.path || "/"));
1379
1495
  return { content: [{ type: "text", text: listing }] };
1380
1496
  }
1381
1497
  case "sftp-read": {
1382
- const conn = await getServerConnection(String(a.serverId));
1498
+ const { conn } = await getServerConnection(String(a.serverId));
1383
1499
  const content = await sftpRead(conn, String(a.path));
1384
1500
  return { content: [{ type: "text", text: content }] };
1385
1501
  }
1386
1502
  case "sftp-write": {
1387
- const conn = await getServerConnection(String(a.serverId));
1503
+ const { conn } = await getServerConnection(String(a.serverId));
1388
1504
  const result = await sftpWrite(conn, String(a.path), String(a.content));
1389
1505
  return { content: [{ type: "text", text: result }] };
1390
1506
  }
1391
1507
  case "sftp-delete": {
1392
- const conn = await getServerConnection(String(a.serverId));
1508
+ const { conn } = await getServerConnection(String(a.serverId));
1393
1509
  const result = await sftpDelete(conn, String(a.path));
1394
1510
  return { content: [{ type: "text", text: result }] };
1395
1511
  }
1396
1512
  // ----- Docker -----
1397
1513
  case "docker-list": {
1398
- const conn = await getServerConnection(String(a.serverId));
1399
- const result = await sshExec(conn, 'docker ps -a --format "table {{.Names}} {{.Image}} {{.Status}} {{.Ports}}"');
1514
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1515
+ const result = await sshExec(conn, 'docker ps -a --format "table {{.Names}} {{.Image}} {{.Status}} {{.Ports}}"', proxy);
1400
1516
  return { content: [{ type: "text", text: result.exitCode === 0 ? result.stdout : `Error: ${result.stderr}` }] };
1401
1517
  }
1402
1518
  case "docker-action": {
@@ -1405,22 +1521,22 @@ ${result.stderr}`);
1405
1521
  if (!["start", "stop", "restart", "remove"].includes(action)) {
1406
1522
  throw new Error(`Invalid action: ${action}. Use start, stop, restart, or remove.`);
1407
1523
  }
1408
- const conn = await getServerConnection(String(a.serverId));
1524
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1409
1525
  const dockerCmd = action === "remove" ? `docker rm -f ${container}` : `docker ${action} ${container}`;
1410
- const result = await sshExec(conn, dockerCmd);
1526
+ const result = await sshExec(conn, dockerCmd, proxy);
1411
1527
  return { content: [{ type: "text", text: result.exitCode === 0 ? `Container "${container}" ${action}ed successfully` : `Error: ${result.stderr}` }] };
1412
1528
  }
1413
1529
  case "docker-logs": {
1414
1530
  const container = String(a.containerName).replace(/[^a-zA-Z0-9._-]/g, "");
1415
1531
  const lines = Number(a.lines) || 100;
1416
- const conn = await getServerConnection(String(a.serverId));
1417
- const result = await sshExec(conn, `docker logs --tail ${lines} ${container} 2>&1`);
1532
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1533
+ const result = await sshExec(conn, `docker logs --tail ${lines} ${container} 2>&1`, proxy);
1418
1534
  return { content: [{ type: "text", text: result.exitCode === 0 ? result.stdout : `Error: ${result.stderr}` }] };
1419
1535
  }
1420
1536
  // ----- Database -----
1421
1537
  case "db-discover": {
1422
- const conn = await getServerConnection(String(a.serverId));
1423
- const sites = await discoverSiteDatabases(conn);
1538
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1539
+ const sites = await discoverSiteDatabases(conn, proxy);
1424
1540
  if (!sites.length) {
1425
1541
  return { content: [{ type: "text", text: "No web applications with database configs found in /var/www/" }] };
1426
1542
  }
@@ -1430,26 +1546,27 @@ ${result.stderr}`);
1430
1546
  return { content: [{ type: "text", text: lines.join("\n") }] };
1431
1547
  }
1432
1548
  case "db-tables": {
1433
- const conn = await getServerConnection(String(a.serverId));
1549
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1434
1550
  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";
1435
- const output = await execSiteMysql(conn, String(a.sitePath), sql);
1551
+ const output = await execSiteMysql(conn, String(a.sitePath), sql, proxy);
1436
1552
  return { content: [{ type: "text", text: output || "No tables found" }] };
1437
1553
  }
1438
1554
  case "db-describe": {
1439
1555
  const table = String(a.table).replace(/[^a-zA-Z0-9_]/g, "");
1440
- const conn = await getServerConnection(String(a.serverId));
1556
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1441
1557
  const output = await execSiteMysql(
1442
1558
  conn,
1443
1559
  String(a.sitePath),
1444
- `DESCRIBE \`${table}\`; SHOW INDEX FROM \`${table}\``
1560
+ `DESCRIBE \`${table}\`; SHOW INDEX FROM \`${table}\``,
1561
+ proxy
1445
1562
  );
1446
1563
  return { content: [{ type: "text", text: output }] };
1447
1564
  }
1448
1565
  case "db-query": {
1449
1566
  const query = String(a.query).trim();
1450
1567
  assertSafeSql(query);
1451
- const conn = await getServerConnection(String(a.serverId));
1452
- const output = await execSiteMysql(conn, String(a.sitePath), query);
1568
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1569
+ const output = await execSiteMysql(conn, String(a.sitePath), query, proxy);
1453
1570
  return { content: [{ type: "text", text: output || "Query executed successfully (no output)" }] };
1454
1571
  }
1455
1572
  // ----- Env Config -----
@@ -1541,12 +1658,13 @@ ${result.stderr}`);
1541
1658
  if (error) throw new Error(error.message);
1542
1659
  saveMsg = `Stored env config: ${appName}/${environment}`;
1543
1660
  }
1544
- const vercelStatus = await attemptVercelSync(appName, environment);
1661
+ const syncStageId = existing?.release_profile_stage_id ?? resolvedStageIds?.[0];
1662
+ const vercelStatus = await attemptVercelSync(appName, environment, syncStageId);
1545
1663
  return { content: [{ type: "text", text: `${saveMsg}. ${vercelStatus}` }] };
1546
1664
  }
1547
1665
  // ----- Cache Purge -----
1548
1666
  case "cache-purge": {
1549
- const conn = await getServerConnection(String(a.serverId));
1667
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1550
1668
  conn.timeout = 12e4;
1551
1669
  const script = `
1552
1670
  R=""
@@ -1627,13 +1745,13 @@ else
1627
1745
  fi
1628
1746
  echo -e "$R"
1629
1747
  `.trim();
1630
- const result = await sshExec(conn, script);
1748
+ const result = await sshExec(conn, script, proxy);
1631
1749
  const output = (result.stdout || "").trim();
1632
1750
  return { content: [{ type: "text", text: output || "Cache purge completed (no output)" }] };
1633
1751
  }
1634
1752
  // ----- Log Reading -----
1635
1753
  case "log-list": {
1636
- const conn = await getServerConnection(String(a.serverId));
1754
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1637
1755
  const script = `
1638
1756
  {
1639
1757
  [ -d /usr/local/lsws/logs ] && find /usr/local/lsws/logs -maxdepth 2 -name "*.log" -type f 2>/dev/null
@@ -1659,7 +1777,7 @@ echo -e "$R"
1659
1777
  echo "$f $HR $MOD"
1660
1778
  done
1661
1779
  `.trim();
1662
- const result = await sshExec(conn, script);
1780
+ const result = await sshExec(conn, script, proxy);
1663
1781
  return { content: [{ type: "text", text: result.stdout || "No log files found" }] };
1664
1782
  }
1665
1783
  case "log-read": {
@@ -1667,9 +1785,9 @@ done
1667
1785
  assertAllowedLogPath(logPath);
1668
1786
  const lineCount = Math.min(Math.max(Number(a.lines) || 100, 1), 500);
1669
1787
  const filter = a.filter ? String(a.filter).replace(/'/g, "'\\''") : "";
1670
- const conn = await getServerConnection(String(a.serverId));
1788
+ const { conn, proxy } = await getServerConnection(String(a.serverId));
1671
1789
  const cmd = filter ? `grep '${filter}' '${logPath}' 2>/dev/null | tail -n ${lineCount}` : `tail -n ${lineCount} '${logPath}' 2>/dev/null`;
1672
- const result = await sshExec(conn, cmd);
1790
+ const result = await sshExec(conn, cmd, proxy);
1673
1791
  if (result.exitCode !== 0 && !result.stdout) {
1674
1792
  throw new Error(result.stderr || `Failed to read log: ${logPath}`);
1675
1793
  }
@@ -1677,21 +1795,21 @@ done
1677
1795
  }
1678
1796
  // ----- Cron Jobs -----
1679
1797
  case "cron-list": {
1680
- const conn = await getServerConnection(a.serverId);
1798
+ const { conn, proxy } = await getServerConnection(a.serverId);
1681
1799
  const user = a.user ? String(a.user) : null;
1682
1800
  const parts = [];
1683
1801
  if (user) {
1684
- const result = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null || echo '(no crontab for ${user})'`);
1802
+ const result = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null || echo '(no crontab for ${user})'`, proxy);
1685
1803
  parts.push(`## Crontab for ${user}
1686
1804
  ${result.stdout}`);
1687
1805
  } else {
1688
- const rootCron = await sshExec(conn, `crontab -l 2>/dev/null || echo '(no crontab for root)'`);
1806
+ const rootCron = await sshExec(conn, `crontab -l 2>/dev/null || echo '(no crontab for root)'`, proxy);
1689
1807
  parts.push(`## Root crontab
1690
1808
  ${rootCron.stdout}`);
1691
- const wwwCron = await sshExec(conn, `crontab -l -u www-data 2>/dev/null || echo '(no crontab for www-data)'`);
1809
+ const wwwCron = await sshExec(conn, `crontab -l -u www-data 2>/dev/null || echo '(no crontab for www-data)'`, proxy);
1692
1810
  parts.push(`## www-data crontab
1693
1811
  ${wwwCron.stdout}`);
1694
- 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/)'`);
1812
+ 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);
1695
1813
  parts.push(`## /etc/cron.d/
1696
1814
  ${cronD.stdout}`);
1697
1815
  const summary = await sshExec(conn, [
@@ -1700,13 +1818,13 @@ ${cronD.stdout}`);
1700
1818
  `echo "cron.daily: $(ls /etc/cron.daily/ 2>/dev/null | wc -l) scripts"`,
1701
1819
  `echo "cron.weekly: $(ls /etc/cron.weekly/ 2>/dev/null | wc -l) scripts"`,
1702
1820
  `echo "cron.monthly: $(ls /etc/cron.monthly/ 2>/dev/null | wc -l) scripts"`
1703
- ].join(" && "));
1821
+ ].join(" && "), proxy);
1704
1822
  parts.push(summary.stdout);
1705
1823
  }
1706
1824
  return { content: [{ type: "text", text: parts.join("\n\n") }] };
1707
1825
  }
1708
1826
  case "cron-add": {
1709
- const conn = await getServerConnection(a.serverId);
1827
+ const { conn, proxy } = await getServerConnection(a.serverId);
1710
1828
  const user = a.user ? String(a.user) : "root";
1711
1829
  const schedule = String(a.schedule).trim();
1712
1830
  const command = String(a.command).trim();
@@ -1716,7 +1834,7 @@ ${cronD.stdout}`);
1716
1834
  }
1717
1835
  const entry = commentText ? `# ${commentText}
1718
1836
  ${schedule} ${command}` : `${schedule} ${command}`;
1719
- const result = await sshExec(conn, `(crontab -l -u ${user} 2>/dev/null; echo '${entry.replace(/'/g, "'\\''")}') | crontab -u ${user} -`);
1837
+ const result = await sshExec(conn, `(crontab -l -u ${user} 2>/dev/null; echo '${entry.replace(/'/g, "'\\''")}') | crontab -u ${user} -`, proxy);
1720
1838
  if (result.exitCode !== 0) {
1721
1839
  throw new Error(result.stderr || "Failed to add cron job");
1722
1840
  }
@@ -1724,13 +1842,13 @@ ${schedule} ${command}` : `${schedule} ${command}`;
1724
1842
  ${schedule} ${command}` }] };
1725
1843
  }
1726
1844
  case "cron-remove": {
1727
- const conn = await getServerConnection(a.serverId);
1845
+ const { conn, proxy } = await getServerConnection(a.serverId);
1728
1846
  const user = a.user ? String(a.user) : "root";
1729
1847
  const match = String(a.commandMatch).trim();
1730
1848
  if (!match) {
1731
1849
  throw new Error("commandMatch is required");
1732
1850
  }
1733
- const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`);
1851
+ const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
1734
1852
  const lines = current.stdout.split("\n");
1735
1853
  const before = lines.length;
1736
1854
  const filtered = lines.filter((line) => {
@@ -1742,21 +1860,21 @@ ${schedule} ${command}` }] };
1742
1860
  return { content: [{ type: "text", text: `No cron entries found matching "${match}"` }] };
1743
1861
  }
1744
1862
  const escapedCrontab = filtered.join("\n").replace(/'/g, "'\\''");
1745
- const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`);
1863
+ const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`, proxy);
1746
1864
  if (result.exitCode !== 0) {
1747
1865
  throw new Error(result.stderr || "Failed to update crontab");
1748
1866
  }
1749
1867
  return { content: [{ type: "text", text: `Removed ${removed} cron entry/entries matching "${match}" from ${user} crontab` }] };
1750
1868
  }
1751
1869
  case "cron-toggle": {
1752
- const conn = await getServerConnection(a.serverId);
1870
+ const { conn, proxy } = await getServerConnection(a.serverId);
1753
1871
  const user = a.user ? String(a.user) : "root";
1754
1872
  const match = String(a.commandMatch).trim();
1755
1873
  const enable = Boolean(a.enable);
1756
1874
  if (!match) {
1757
1875
  throw new Error("commandMatch is required");
1758
1876
  }
1759
- const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`);
1877
+ const current = await sshExec(conn, `crontab -l -u ${user} 2>/dev/null`, proxy);
1760
1878
  const lines = current.stdout.split("\n");
1761
1879
  let toggled = 0;
1762
1880
  const updated = lines.map((line) => {
@@ -1775,7 +1893,7 @@ ${schedule} ${command}` }] };
1775
1893
  return { content: [{ type: "text", text: `No ${state} cron entries found matching "${match}"` }] };
1776
1894
  }
1777
1895
  const escapedCrontab = updated.join("\n").replace(/'/g, "'\\''");
1778
- const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`);
1896
+ const result = await sshExec(conn, `echo '${escapedCrontab}' | crontab -u ${user} -`, proxy);
1779
1897
  if (result.exitCode !== 0) {
1780
1898
  throw new Error(result.stderr || "Failed to update crontab");
1781
1899
  }