@mgsoftwarebv/mg-dashboard-mcp 6.6.0 → 6.6.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
@@ -17,6 +17,9 @@ import { drizzle } from 'drizzle-orm/postgres-js';
17
17
  import postgres from 'postgres';
18
18
  import { readFile, mkdtemp, writeFile, rm } from 'fs/promises';
19
19
  import { tmpdir } from 'os';
20
+ import { once } from 'events';
21
+ import https from 'https';
22
+ import { connect } from 'tls';
20
23
  import { HeadObjectCommand, S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
21
24
 
22
25
  var __defProp = Object.defineProperty;
@@ -314,7 +317,7 @@ function rewriteUrl(originalUrl, localPort) {
314
317
  parsed.port = String(localPort);
315
318
  return parsed.toString();
316
319
  }
317
- async function connectSsh(opts) {
320
+ async function connectSsh2(opts) {
318
321
  return await new Promise((resolve, reject) => {
319
322
  const conn = new Client();
320
323
  const onError = (err) => {
@@ -374,7 +377,7 @@ async function fetchRemoteEnvValue(options) {
374
377
  const privateKey = readFileSync(privateKeyPath);
375
378
  const keepaliveIntervalMs = options.keepaliveIntervalMs ?? 3e4;
376
379
  const envKey = options.envKey ?? "DATABASE_PRIMARY_URL";
377
- const conn = await connectSsh({ host, port, username, privateKey, keepaliveIntervalMs });
380
+ const conn = await connectSsh2({ host, port, username, privateKey, keepaliveIntervalMs });
378
381
  try {
379
382
  const safePath = options.remoteEnvPath.replace(/'/g, "'\\''");
380
383
  const result = await execOverSsh(conn, `cat -- '${safePath}'`);
@@ -400,7 +403,7 @@ async function openDbSshTunnel(options) {
400
403
  const parsedDbUrl = new URL(options.databaseUrl);
401
404
  const remoteHost = parsedDbUrl.hostname;
402
405
  const remotePort = Number(parsedDbUrl.port || "5432");
403
- const conn = await connectSsh({ host, port, username, privateKey, keepaliveIntervalMs });
406
+ const conn = await connectSsh2({ host, port, username, privateKey, keepaliveIntervalMs });
404
407
  conn.on("error", (err) => {
405
408
  console.error(`[mcp][db-ssh-tunnel] ssh connection error: ${err.message}`);
406
409
  });
@@ -1525,6 +1528,97 @@ async function handleRepoTool(name, args2, deps) {
1525
1528
  return { content: [{ type: "text", text: `Unknown repo tool: ${name}` }] };
1526
1529
  }
1527
1530
  }
1531
+ function connectSsh(opts) {
1532
+ return new Promise((resolve, reject) => {
1533
+ const client = new Client();
1534
+ client.once("ready", () => resolve(client));
1535
+ client.once("error", reject);
1536
+ client.connect({
1537
+ host: opts.hostname,
1538
+ port: opts.port,
1539
+ username: opts.username,
1540
+ password: opts.password,
1541
+ privateKey: opts.privateKey,
1542
+ passphrase: opts.passphrase,
1543
+ readyTimeout: opts.timeout ?? 3e4
1544
+ });
1545
+ });
1546
+ }
1547
+ function forwardOut(client, host, port) {
1548
+ return new Promise((resolve, reject) => {
1549
+ client.forwardOut("127.0.0.1", 0, host, port, (err, stream) => {
1550
+ if (err) reject(err);
1551
+ else resolve(stream);
1552
+ });
1553
+ });
1554
+ }
1555
+ async function fetchViaSshProxy(options) {
1556
+ const parsedUrl = new URL(options.url);
1557
+ if (parsedUrl.protocol !== "https:") {
1558
+ throw new Error(`fetchViaSshProxy only supports https URLs, got ${parsedUrl.protocol}`);
1559
+ }
1560
+ const targetHost = parsedUrl.hostname;
1561
+ const targetPort = parsedUrl.port ? Number(parsedUrl.port) : 443;
1562
+ const method = options.method ?? "GET";
1563
+ const body = options.body ?? "";
1564
+ const headers = { ...options.headers };
1565
+ if (body && !headers["Content-Length"]) {
1566
+ headers["Content-Length"] = String(Buffer.byteLength(body));
1567
+ }
1568
+ headers.Host = parsedUrl.host;
1569
+ const ssh = await connectSsh(options.proxy);
1570
+ const timeoutMs = options.timeoutMs ?? 6e4;
1571
+ let timer;
1572
+ try {
1573
+ const stream = await forwardOut(ssh, targetHost, targetPort);
1574
+ const tlsSocket = connect({
1575
+ socket: stream,
1576
+ servername: targetHost,
1577
+ ALPNProtocols: ["http/1.1"]
1578
+ });
1579
+ await Promise.race([
1580
+ once(tlsSocket, "secureConnect"),
1581
+ new Promise((_, reject) => {
1582
+ timer = setTimeout(
1583
+ () => reject(new Error(`mijn.host fetch via SSH proxy timed out after ${timeoutMs}ms`)),
1584
+ timeoutMs
1585
+ );
1586
+ })
1587
+ ]);
1588
+ if (timer) clearTimeout(timer);
1589
+ return await new Promise((resolve, reject) => {
1590
+ const req = https.request(
1591
+ {
1592
+ createConnection: () => tlsSocket,
1593
+ hostname: targetHost,
1594
+ port: targetPort,
1595
+ path: `${parsedUrl.pathname}${parsedUrl.search}`,
1596
+ method,
1597
+ headers,
1598
+ agent: false
1599
+ },
1600
+ (res) => {
1601
+ const chunks = [];
1602
+ res.on("data", (chunk) => chunks.push(chunk));
1603
+ res.on("end", () => {
1604
+ resolve({
1605
+ status: res.statusCode ?? 0,
1606
+ statusText: res.statusMessage ?? "",
1607
+ body: Buffer.concat(chunks).toString("utf8")
1608
+ });
1609
+ });
1610
+ res.on("error", reject);
1611
+ }
1612
+ );
1613
+ req.on("error", reject);
1614
+ if (body) req.write(body);
1615
+ req.end();
1616
+ });
1617
+ } finally {
1618
+ if (timer) clearTimeout(timer);
1619
+ ssh.end();
1620
+ }
1621
+ }
1528
1622
  var args = process.argv.slice(2);
1529
1623
  function getArg2(name) {
1530
1624
  return args.find((a) => a.startsWith(`--${name}=`))?.split("=").slice(1).join("=");
@@ -1540,6 +1634,14 @@ var sshKeyPath = getArg2("ssh-key") || process.env.MG_DASHBOARD_SSH_KEY;
1540
1634
  var databaseUrl = getArg2("database-url") || process.env.DATABASE_PRIMARY_POOLER_URL || process.env.DATABASE_PRIMARY_URL;
1541
1635
  var encryptionKey = getArg2("encryption-key") || process.env.ENCRYPTION_KEY;
1542
1636
  var mijnhostApiKey = getArg2("mijnhost-api-key") || process.env.MIJNHOST_API_KEY;
1637
+ function isMijnhostViaSshProxyEnabled() {
1638
+ const arg = getArg2("mijnhost-via-ssh-proxy");
1639
+ if (arg === "false" || arg === "0") return false;
1640
+ const env = process.env.MG_DASHBOARD_MIJNHOST_VIA_SSH_PROXY;
1641
+ if (env === "0" || env === "false") return false;
1642
+ return true;
1643
+ }
1644
+ var mijnhostViaSshProxy = isMijnhostViaSshProxyEnabled();
1543
1645
  var dbSshTunnel = getArg2("db-ssh-tunnel") || process.env.MG_DASHBOARD_DB_SSH_TUNNEL;
1544
1646
  var dbRemoteEnvFile = getArg2("db-remote-env-file") || process.env.MG_DASHBOARD_DB_REMOTE_ENV_FILE;
1545
1647
  var dbRemoteEnvKey = getArg2("db-remote-env-key") || process.env.MG_DASHBOARD_DB_REMOTE_ENV_KEY || "DATABASE_PRIMARY_URL";
@@ -1577,6 +1679,9 @@ if (dbSshTunnel) {
1577
1679
  process.env.DATABASE_PRIMARY_URL = databaseUrl;
1578
1680
  process.env.DATABASE_PRIMARY_POOLER_URL = databaseUrl;
1579
1681
  var db = getDb();
1682
+ if (mijnhostApiKey && mijnhostViaSshProxy) {
1683
+ console.error("[mcp][mijnhost] Routing mijn.host API via SSH proxy");
1684
+ }
1580
1685
  var RateLimiter = class {
1581
1686
  buckets = /* @__PURE__ */ new Map();
1582
1687
  maxAttempts;
@@ -3918,20 +4023,48 @@ function describeDnsCandidates(records, type, name, attemptedValue) {
3918
4023
  }
3919
4024
  async function mijnhostFetch(path, options = {}) {
3920
4025
  const key = requireMijnhostApiKey();
3921
- const res = await fetch(`${MIJNHOST_BASE_URL}${path}`, {
3922
- ...options,
3923
- headers: {
3924
- "API-Key": key,
3925
- "Accept": "application/json",
3926
- "Content-Type": "application/json",
3927
- "User-Agent": "mg-dashboard-mcp/6.6.0",
3928
- ...options.headers || {}
3929
- }
3930
- });
3931
- const json = await res.json();
4026
+ const headers = {
4027
+ "API-Key": key,
4028
+ "Accept": "application/json",
4029
+ "Content-Type": "application/json",
4030
+ "User-Agent": "mg-dashboard-mcp/6.6.0",
4031
+ ...options.headers || {}
4032
+ };
4033
+ const method = options.method ?? "GET";
4034
+ const requestBody = typeof options.body === "string" ? options.body : options.body != null ? String(options.body) : void 0;
4035
+ let status;
4036
+ let responseText;
4037
+ if (mijnhostViaSshProxy) {
4038
+ const proxy = await getProxyConnection();
4039
+ const proxied = await fetchViaSshProxy({
4040
+ proxy,
4041
+ url: `${MIJNHOST_BASE_URL}${path}`,
4042
+ method,
4043
+ headers,
4044
+ body: requestBody,
4045
+ timeoutMs: 6e4
4046
+ });
4047
+ status = proxied.status;
4048
+ responseText = proxied.body;
4049
+ } else {
4050
+ const res = await fetch(`${MIJNHOST_BASE_URL}${path}`, {
4051
+ ...options,
4052
+ headers
4053
+ });
4054
+ status = res.status;
4055
+ responseText = await res.text();
4056
+ }
4057
+ let json;
4058
+ try {
4059
+ json = JSON.parse(responseText);
4060
+ } catch {
4061
+ throw new Error(
4062
+ `mijn.host API returned non-JSON (${status}): ${responseText.slice(0, 300)}`
4063
+ );
4064
+ }
3932
4065
  const body = json;
3933
- if (!res.ok) {
3934
- throw new Error(body?.status_description || `mijn.host API error: ${res.status}`);
4066
+ if (status < 200 || status >= 300) {
4067
+ throw new Error(body?.status_description || `mijn.host API error: ${status}`);
3935
4068
  }
3936
4069
  return body;
3937
4070
  }