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