@mgsoftwarebv/mg-dashboard-mcp 6.3.0 → 6.4.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 +205 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
package/dist/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
|
7
7
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
8
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
9
|
import { ListToolsRequestSchema, CallToolRequestSchema, isInitializeRequest, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, ListToolsResultSchema, CallToolResultSchema, ListPromptsResultSchema, GetPromptResultSchema, ListResourcesResultSchema, ReadResourceResultSchema, ListResourceTemplatesResultSchema, EmptyResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { createServer as createServer$1 } from 'net';
|
|
11
|
+
import { Client } from 'ssh2';
|
|
10
12
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
11
13
|
import { createServer } from 'http';
|
|
12
14
|
import { randomUUID, createHash, randomBytes, createDecipheriv, createCipheriv } from 'crypto';
|
|
@@ -15,7 +17,6 @@ import { drizzle } from 'drizzle-orm/postgres-js';
|
|
|
15
17
|
import postgres from 'postgres';
|
|
16
18
|
import { readFile, mkdtemp, writeFile, rm } from 'fs/promises';
|
|
17
19
|
import { tmpdir } from 'os';
|
|
18
|
-
import { Client } from 'ssh2';
|
|
19
20
|
import { HeadObjectCommand, S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
|
|
20
21
|
|
|
21
22
|
var __defProp = Object.defineProperty;
|
|
@@ -272,6 +273,181 @@ var init_proxy_mode = __esm({
|
|
|
272
273
|
"src/proxy-mode.ts"() {
|
|
273
274
|
}
|
|
274
275
|
});
|
|
276
|
+
|
|
277
|
+
// src/db-ssh-tunnel.ts
|
|
278
|
+
var db_ssh_tunnel_exports = {};
|
|
279
|
+
__export(db_ssh_tunnel_exports, {
|
|
280
|
+
fetchRemoteEnvValue: () => fetchRemoteEnvValue,
|
|
281
|
+
openDbSshTunnel: () => openDbSshTunnel
|
|
282
|
+
});
|
|
283
|
+
function expandHome2(path) {
|
|
284
|
+
if (!path.startsWith("~")) return path;
|
|
285
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
286
|
+
return join(home, path.slice(1));
|
|
287
|
+
}
|
|
288
|
+
function resolvePrivateKeyPath(input) {
|
|
289
|
+
const candidate = expandHome2(input);
|
|
290
|
+
const stripped = candidate.endsWith(".pub") ? candidate.slice(0, -4) : candidate;
|
|
291
|
+
if (existsSync(stripped) && statSync(stripped).isFile()) return stripped;
|
|
292
|
+
throw new Error(
|
|
293
|
+
`SSH private key not found at ${stripped}. The --db-ssh-tunnel flag needs the private key (not just .pub) to open the tunnel.`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
function parseSshTarget(target) {
|
|
297
|
+
const at = target.indexOf("@");
|
|
298
|
+
if (at === -1) {
|
|
299
|
+
throw new Error(`--db-ssh-tunnel must be in the form user@host[:port], got: ${target}`);
|
|
300
|
+
}
|
|
301
|
+
const username = target.slice(0, at);
|
|
302
|
+
const hostPart = target.slice(at + 1);
|
|
303
|
+
const colon = hostPart.lastIndexOf(":");
|
|
304
|
+
if (colon === -1) return { username, host: hostPart, port: 22 };
|
|
305
|
+
const port = Number(hostPart.slice(colon + 1));
|
|
306
|
+
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
307
|
+
throw new Error(`Invalid SSH port in --db-ssh-tunnel: ${hostPart.slice(colon + 1)}`);
|
|
308
|
+
}
|
|
309
|
+
return { username, host: hostPart.slice(0, colon), port };
|
|
310
|
+
}
|
|
311
|
+
function rewriteUrl(originalUrl, localPort) {
|
|
312
|
+
const parsed = new URL(originalUrl);
|
|
313
|
+
parsed.hostname = "127.0.0.1";
|
|
314
|
+
parsed.port = String(localPort);
|
|
315
|
+
return parsed.toString();
|
|
316
|
+
}
|
|
317
|
+
async function connectSsh(opts) {
|
|
318
|
+
return await new Promise((resolve, reject) => {
|
|
319
|
+
const conn = new Client();
|
|
320
|
+
const onError = (err) => {
|
|
321
|
+
conn.removeAllListeners();
|
|
322
|
+
reject(err);
|
|
323
|
+
};
|
|
324
|
+
conn.once("ready", () => {
|
|
325
|
+
conn.removeListener("error", onError);
|
|
326
|
+
resolve(conn);
|
|
327
|
+
});
|
|
328
|
+
conn.once("error", onError);
|
|
329
|
+
conn.connect({
|
|
330
|
+
host: opts.host,
|
|
331
|
+
port: opts.port,
|
|
332
|
+
username: opts.username,
|
|
333
|
+
privateKey: opts.privateKey,
|
|
334
|
+
keepaliveInterval: opts.keepaliveIntervalMs,
|
|
335
|
+
keepaliveCountMax: 3,
|
|
336
|
+
readyTimeout: 2e4
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function execOverSsh(conn, cmd) {
|
|
341
|
+
return new Promise((resolve, reject) => {
|
|
342
|
+
conn.exec(cmd, (err, stream) => {
|
|
343
|
+
if (err) {
|
|
344
|
+
reject(err);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
let stdout = "";
|
|
348
|
+
let stderr = "";
|
|
349
|
+
stream.on("data", (chunk) => {
|
|
350
|
+
stdout += chunk.toString("utf8");
|
|
351
|
+
});
|
|
352
|
+
stream.stderr.on("data", (chunk) => {
|
|
353
|
+
stderr += chunk.toString("utf8");
|
|
354
|
+
});
|
|
355
|
+
stream.on("close", (code) => {
|
|
356
|
+
resolve({ stdout, stderr, code: code ?? -1 });
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function parseEnvValue(content, key) {
|
|
362
|
+
const re = new RegExp(`^${key}\\s*=\\s*(.*)$`, "m");
|
|
363
|
+
const m = content.match(re);
|
|
364
|
+
if (!m || !m[1]) return void 0;
|
|
365
|
+
let value = m[1].trim();
|
|
366
|
+
if (value.length >= 2 && (value[0] === '"' || value[0] === "'") && value[value.length - 1] === value[0]) {
|
|
367
|
+
value = value.slice(1, -1);
|
|
368
|
+
}
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
async function fetchRemoteEnvValue(options) {
|
|
372
|
+
const { username, host, port } = parseSshTarget(options.sshTarget);
|
|
373
|
+
const privateKeyPath = resolvePrivateKeyPath(options.sshKeyPath);
|
|
374
|
+
const privateKey = readFileSync(privateKeyPath);
|
|
375
|
+
const keepaliveIntervalMs = options.keepaliveIntervalMs ?? 3e4;
|
|
376
|
+
const envKey = options.envKey ?? "DATABASE_PRIMARY_URL";
|
|
377
|
+
const conn = await connectSsh({ host, port, username, privateKey, keepaliveIntervalMs });
|
|
378
|
+
try {
|
|
379
|
+
const safePath = options.remoteEnvPath.replace(/'/g, "'\\''");
|
|
380
|
+
const result = await execOverSsh(conn, `cat -- '${safePath}'`);
|
|
381
|
+
if (result.code !== 0) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`remote cat ${options.remoteEnvPath} failed (exit ${result.code}): ${result.stderr.trim()}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
const value = parseEnvValue(result.stdout, envKey);
|
|
387
|
+
if (!value) {
|
|
388
|
+
throw new Error(`${envKey} not found in ${options.remoteEnvPath}`);
|
|
389
|
+
}
|
|
390
|
+
return value;
|
|
391
|
+
} finally {
|
|
392
|
+
conn.end();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function openDbSshTunnel(options) {
|
|
396
|
+
const { username, host, port } = parseSshTarget(options.sshTarget);
|
|
397
|
+
const privateKeyPath = resolvePrivateKeyPath(options.sshKeyPath);
|
|
398
|
+
const privateKey = readFileSync(privateKeyPath);
|
|
399
|
+
const keepaliveIntervalMs = options.keepaliveIntervalMs ?? 3e4;
|
|
400
|
+
const parsedDbUrl = new URL(options.databaseUrl);
|
|
401
|
+
const remoteHost = parsedDbUrl.hostname;
|
|
402
|
+
const remotePort = Number(parsedDbUrl.port || "5432");
|
|
403
|
+
const conn = await connectSsh({ host, port, username, privateKey, keepaliveIntervalMs });
|
|
404
|
+
conn.on("error", (err) => {
|
|
405
|
+
console.error(`[mcp][db-ssh-tunnel] ssh connection error: ${err.message}`);
|
|
406
|
+
});
|
|
407
|
+
conn.on("close", () => {
|
|
408
|
+
console.error("[mcp][db-ssh-tunnel] ssh connection closed; MCP will exit so Cursor can respawn it");
|
|
409
|
+
process.exit(1);
|
|
410
|
+
});
|
|
411
|
+
const server2 = createServer$1((local) => {
|
|
412
|
+
conn.forwardOut("127.0.0.1", 0, remoteHost, remotePort, (err, stream) => {
|
|
413
|
+
if (err) {
|
|
414
|
+
console.error(`[mcp][db-ssh-tunnel] forwardOut failed: ${err.message}`);
|
|
415
|
+
local.destroy(err);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
local.on("error", () => stream.destroy());
|
|
419
|
+
stream.on("error", () => local.destroy());
|
|
420
|
+
local.pipe(stream).pipe(local);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
const localPort = await new Promise((resolve, reject) => {
|
|
424
|
+
server2.once("error", reject);
|
|
425
|
+
server2.listen(0, "127.0.0.1", () => {
|
|
426
|
+
const addr = server2.address();
|
|
427
|
+
if (!addr || typeof addr === "string") {
|
|
428
|
+
reject(new Error("Failed to bind local tunnel listener"));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
resolve(addr.port);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
const rewrittenDatabaseUrl = rewriteUrl(options.databaseUrl, localPort);
|
|
435
|
+
console.error(
|
|
436
|
+
`[mcp][db-ssh-tunnel] forwarding 127.0.0.1:${localPort} -> ${remoteHost}:${remotePort} via ssh ${username}@${host}:${port}`
|
|
437
|
+
);
|
|
438
|
+
return {
|
|
439
|
+
rewrittenDatabaseUrl,
|
|
440
|
+
localPort,
|
|
441
|
+
async close() {
|
|
442
|
+
await new Promise((resolve) => server2.close(() => resolve()));
|
|
443
|
+
conn.end();
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
var init_db_ssh_tunnel = __esm({
|
|
448
|
+
"src/db-ssh-tunnel.ts"() {
|
|
449
|
+
}
|
|
450
|
+
});
|
|
275
451
|
var connectionConfig = {
|
|
276
452
|
prepare: false,
|
|
277
453
|
idle_timeout: 20,
|
|
@@ -1979,19 +2155,42 @@ var sshKeyPath = getArg2("ssh-key") || process.env.MG_DASHBOARD_SSH_KEY;
|
|
|
1979
2155
|
var databaseUrl = getArg2("database-url") || process.env.DATABASE_PRIMARY_POOLER_URL || process.env.DATABASE_PRIMARY_URL;
|
|
1980
2156
|
var encryptionKey = getArg2("encryption-key") || process.env.ENCRYPTION_KEY;
|
|
1981
2157
|
var mijnhostApiKey = getArg2("mijnhost-api-key") || process.env.MIJNHOST_API_KEY;
|
|
2158
|
+
var dbSshTunnel = getArg2("db-ssh-tunnel") || process.env.MG_DASHBOARD_DB_SSH_TUNNEL;
|
|
2159
|
+
var dbRemoteEnvFile = getArg2("db-remote-env-file") || process.env.MG_DASHBOARD_DB_REMOTE_ENV_FILE;
|
|
2160
|
+
var dbRemoteEnvKey = getArg2("db-remote-env-key") || process.env.MG_DASHBOARD_DB_REMOTE_ENV_KEY || "DATABASE_PRIMARY_URL";
|
|
1982
2161
|
var httpMode = args.includes("--http");
|
|
1983
2162
|
var httpPort = Number(getArg2("port")) || 3100;
|
|
1984
2163
|
if (!apiKey || !sshKeyPath) {
|
|
1985
2164
|
console.error("Authentication required. Use both --api-key=dk_xxx and --ssh-key=PATH (path to your SSH private or public key), or set MG_DASHBOARD_API_KEY and MG_DASHBOARD_SSH_KEY.");
|
|
1986
2165
|
process.exit(1);
|
|
1987
2166
|
}
|
|
2167
|
+
if (dbRemoteEnvFile && !dbSshTunnel) {
|
|
2168
|
+
console.error("--db-remote-env-file requires --db-ssh-tunnel (the same SSH connection is used to read the env value).");
|
|
2169
|
+
process.exit(1);
|
|
2170
|
+
}
|
|
2171
|
+
if (dbRemoteEnvFile) {
|
|
2172
|
+
const { fetchRemoteEnvValue: fetchRemoteEnvValue2 } = await Promise.resolve().then(() => (init_db_ssh_tunnel(), db_ssh_tunnel_exports));
|
|
2173
|
+
databaseUrl = await fetchRemoteEnvValue2({
|
|
2174
|
+
sshTarget: dbSshTunnel,
|
|
2175
|
+
sshKeyPath,
|
|
2176
|
+
remoteEnvPath: dbRemoteEnvFile,
|
|
2177
|
+
envKey: dbRemoteEnvKey
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
1988
2180
|
if (!databaseUrl) {
|
|
1989
|
-
console.error("Database URL required.
|
|
2181
|
+
console.error("Database URL required. Provide one of:\n --database-url=postgres://...\n --db-remote-env-file=/path/.env (combined with --db-ssh-tunnel)\n DATABASE_PRIMARY_URL or DATABASE_PRIMARY_POOLER_URL env var");
|
|
1990
2182
|
process.exit(1);
|
|
1991
2183
|
}
|
|
1992
|
-
if (
|
|
1993
|
-
|
|
2184
|
+
if (dbSshTunnel) {
|
|
2185
|
+
const { openDbSshTunnel: openDbSshTunnel2 } = await Promise.resolve().then(() => (init_db_ssh_tunnel(), db_ssh_tunnel_exports));
|
|
2186
|
+
const tunnel = await openDbSshTunnel2({ sshTarget: dbSshTunnel, sshKeyPath, databaseUrl });
|
|
2187
|
+
databaseUrl = tunnel.rewrittenDatabaseUrl;
|
|
2188
|
+
process.on("exit", () => {
|
|
2189
|
+
void tunnel.close();
|
|
2190
|
+
});
|
|
1994
2191
|
}
|
|
2192
|
+
process.env.DATABASE_PRIMARY_URL = databaseUrl;
|
|
2193
|
+
process.env.DATABASE_PRIMARY_POOLER_URL = databaseUrl;
|
|
1995
2194
|
var db = getDb();
|
|
1996
2195
|
var RateLimiter = class {
|
|
1997
2196
|
buckets = /* @__PURE__ */ new Map();
|
|
@@ -2079,7 +2278,6 @@ async function writeAuditLog(entry) {
|
|
|
2079
2278
|
var MODULE_KEYS = [
|
|
2080
2279
|
"users",
|
|
2081
2280
|
"ssh_servers",
|
|
2082
|
-
"supabase",
|
|
2083
2281
|
"wiki",
|
|
2084
2282
|
"ci_cd",
|
|
2085
2283
|
"domains",
|
|
@@ -2088,7 +2286,7 @@ var MODULE_KEYS = [
|
|
|
2088
2286
|
];
|
|
2089
2287
|
var FULL_PERMISSIONS = {
|
|
2090
2288
|
modules: Object.fromEntries(MODULE_KEYS.map((k) => [k, true])),
|
|
2091
|
-
resources: { ssh_servers: ["*"]
|
|
2289
|
+
resources: { ssh_servers: ["*"] }
|
|
2092
2290
|
};
|
|
2093
2291
|
function parsePermissions(raw) {
|
|
2094
2292
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -2105,8 +2303,7 @@ function resolvePermissions(roleName, roleDefaults, userOverrides) {
|
|
|
2105
2303
|
modules[key] = userVal !== void 0 ? userVal : roleVal !== void 0 ? roleVal : false;
|
|
2106
2304
|
}
|
|
2107
2305
|
const resources = {
|
|
2108
|
-
ssh_servers: overrides?.resources?.ssh_servers ?? base?.resources?.ssh_servers ?? []
|
|
2109
|
-
supabase_instances: overrides?.resources?.supabase_instances ?? base?.resources?.supabase_instances ?? []
|
|
2306
|
+
ssh_servers: overrides?.resources?.ssh_servers ?? base?.resources?.ssh_servers ?? []
|
|
2110
2307
|
};
|
|
2111
2308
|
return { modules, resources };
|
|
2112
2309
|
}
|