@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 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. Use --database-url=postgres://... or set DATABASE_PRIMARY_URL (or DATABASE_PRIMARY_POOLER_URL).");
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 (!process.env.DATABASE_PRIMARY_URL && !process.env.DATABASE_PRIMARY_POOLER_URL) {
1993
- process.env.DATABASE_PRIMARY_URL = databaseUrl;
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: ["*"], supabase_instances: ["*"] }
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
  }