@mgsoftwarebv/mg-dashboard-mcp 7.4.18 → 7.4.20

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.
Files changed (2) hide show
  1. package/dist/index.js +32 -157
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'child_process';
3
3
  import { readFileSync, existsSync, statSync, createReadStream } from 'fs';
4
- import { join, dirname, sep, isAbsolute } from 'path';
4
+ import { dirname, sep, join, isAbsolute } from 'path';
5
5
  import { Client as Client$1 } from '@modelcontextprotocol/sdk/client/index.js';
6
6
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
7
7
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -13,6 +13,7 @@ import crypto, { randomUUID, createHash, randomBytes, createCipheriv, createDeci
13
13
  import { readFile, mkdtemp, writeFile, rm } from 'fs/promises';
14
14
  import { createServer } from 'http';
15
15
  import { tmpdir } from 'os';
16
+ import { fileURLToPath } from 'url';
16
17
  import { AsyncLocalStorage } from 'async_hooks';
17
18
  import { drizzle } from 'drizzle-orm/postgres-js';
18
19
  import postgres from 'postgres';
@@ -21,7 +22,6 @@ import { sql } from 'drizzle-orm';
21
22
  import { once } from 'events';
22
23
  import { lookup } from 'dns/promises';
23
24
  import { connect } from 'tls';
24
- import { fileURLToPath } from 'url';
25
25
  import { pgTable, timestamp, uuid, jsonb, boolean, text, customType, integer, date, real, index, uniqueIndex, pgEnum, check, bigint, doublePrecision, primaryKey, foreignKey } from 'drizzle-orm/pg-core';
26
26
  import { HeadObjectCommand, ListObjectsV2Command, DeleteObjectsCommand, S3Client, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
27
27
 
@@ -1316,151 +1316,6 @@ async function proxyJson(ctx, route, body) {
1316
1316
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1317
1317
  };
1318
1318
  }
1319
- var MCP_NPM_PACKAGE = "@mgsoftwarebv/mg-dashboard-mcp";
1320
- var MCP_SKIP_SELF_UPDATE_ENV = "MG_MCP_SKIP_SELF_UPDATE";
1321
- var MCP_SELF_UPDATE_TIMEOUT_MS = 2500;
1322
- var MCP_REQUIRED_VERSION_PATH = "/api/mcp/required-version";
1323
- var MG_DASHBOARD_ORIGIN_DEFAULT = "https://dashboard.mgsoftware.nl";
1324
- function npmLatestUrl(pkg = MCP_NPM_PACKAGE) {
1325
- const slash = pkg.indexOf("/");
1326
- if (slash < 0) return `https://registry.npmjs.org/${pkg}/latest`;
1327
- return `https://registry.npmjs.org/${pkg.slice(0, slash)}%2f${pkg.slice(slash + 1)}/latest`;
1328
- }
1329
- function parseNpmVersion(value) {
1330
- const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value?.trim() ?? "");
1331
- if (!match) return null;
1332
- return [Number(match[1]), Number(match[2]), Number(match[3])];
1333
- }
1334
- function isNpmVersionBehind(installed, latest) {
1335
- const a = parseNpmVersion(installed);
1336
- const b = parseNpmVersion(latest);
1337
- if (!a || !b) return false;
1338
- if (a[0] !== b[0]) return a[0] < b[0];
1339
- if (a[1] !== b[1]) return a[1] < b[1];
1340
- return a[2] < b[2];
1341
- }
1342
- function packageRootFromModuleUrl(moduleUrl) {
1343
- const here = dirname(fileURLToPath(moduleUrl));
1344
- const base = here.split(sep).pop();
1345
- if (base === "dist" || base === "src") return join(here, "..");
1346
- return here;
1347
- }
1348
- function isLocalMonorepoCheckout(packageRoot) {
1349
- return existsSync(join(packageRoot, "../../apps/backoffice/package.json"));
1350
- }
1351
- function readInstalledMcpVersion(packageRoot = packageRootFromModuleUrl(import.meta.url)) {
1352
- try {
1353
- const raw = readFileSync(join(packageRoot, "package.json"), "utf8");
1354
- const parsed = JSON.parse(raw);
1355
- return typeof parsed.version === "string" ? parsed.version : null;
1356
- } catch {
1357
- return null;
1358
- }
1359
- }
1360
- function mcpRequiredVersionUrl(origin = MG_DASHBOARD_ORIGIN_DEFAULT) {
1361
- return `${origin.replace(/\/$/, "")}${MCP_REQUIRED_VERSION_PATH}`;
1362
- }
1363
- function parseDashboardRequiredVersion(json) {
1364
- if (!json || typeof json !== "object") return null;
1365
- const version = json.version;
1366
- if (typeof version !== "string") return null;
1367
- return parseNpmVersion(version) ? version.trim() : null;
1368
- }
1369
- async function fetchDashboardRequiredVersion(fetchFn, origin, timeoutMs = MCP_SELF_UPDATE_TIMEOUT_MS) {
1370
- const controller = new AbortController();
1371
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1372
- try {
1373
- const response = await fetchFn(mcpRequiredVersionUrl(origin), {
1374
- signal: controller.signal,
1375
- headers: { Accept: "application/json", "cache-control": "no-store" }
1376
- });
1377
- if (!response.ok) return null;
1378
- return parseDashboardRequiredVersion(await response.json());
1379
- } catch {
1380
- return null;
1381
- } finally {
1382
- clearTimeout(timer);
1383
- }
1384
- }
1385
- async function fetchLatestNpmVersion(fetchFn, pkg = MCP_NPM_PACKAGE, timeoutMs = MCP_SELF_UPDATE_TIMEOUT_MS) {
1386
- const controller = new AbortController();
1387
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1388
- try {
1389
- const response = await fetchFn(npmLatestUrl(pkg), {
1390
- signal: controller.signal,
1391
- headers: { Accept: "application/json" }
1392
- });
1393
- if (!response.ok) return null;
1394
- const json = await response.json();
1395
- return typeof json.version === "string" ? json.version : null;
1396
- } catch {
1397
- return null;
1398
- } finally {
1399
- clearTimeout(timer);
1400
- }
1401
- }
1402
- function shouldSkipMcpSelfUpdate(io = {}) {
1403
- const env = io.env ?? process.env;
1404
- if (env[MCP_SKIP_SELF_UPDATE_ENV] === "1") return true;
1405
- const packageRoot = packageRootFromModuleUrl(io.moduleUrl ?? import.meta.url);
1406
- const monorepo = io.monorepo ?? isLocalMonorepoCheckout(packageRoot);
1407
- return monorepo;
1408
- }
1409
- function buildNpxSelfUpdateArgs(latest, argv) {
1410
- const command = process.platform === "win32" ? "npx.cmd" : "npx";
1411
- return {
1412
- command,
1413
- args: ["-y", "--prefer-online", `${MCP_NPM_PACKAGE}@${latest}`, ...argv]
1414
- };
1415
- }
1416
- function waitForSpawnedChild(child) {
1417
- return new Promise((resolve) => {
1418
- child.on("error", () => resolve(1));
1419
- child.on("close", (code) => resolve(code ?? 1));
1420
- });
1421
- }
1422
- async function maybeReexecLatestMcp(io = {}) {
1423
- if (shouldSkipMcpSelfUpdate(io)) return false;
1424
- const packageRoot = packageRootFromModuleUrl(io.moduleUrl ?? import.meta.url);
1425
- const installed = io.installedVersion !== void 0 ? io.installedVersion : readInstalledMcpVersion(packageRoot);
1426
- if (!installed) return false;
1427
- const fetchFn = io.fetchFn ?? fetch;
1428
- const origin = (io.dashboardBaseUrl || io.env?.MG_DASHBOARD_BASE_URL || MG_DASHBOARD_ORIGIN_DEFAULT).replace(/\/$/, "");
1429
- const dashboardVersion = await fetchDashboardRequiredVersion(fetchFn, origin);
1430
- const npmVersion = dashboardVersion ? null : await fetchLatestNpmVersion(fetchFn);
1431
- const latest = dashboardVersion ?? npmVersion;
1432
- if (!latest || !isNpmVersionBehind(installed, latest)) return false;
1433
- const argv = io.argv ?? process.argv.slice(2);
1434
- const { command, args: args2 } = buildNpxSelfUpdateArgs(latest, argv);
1435
- const log = io.log ?? ((message) => console.error(message));
1436
- log(
1437
- `[mcp] ${MCP_NPM_PACKAGE}@${installed} is behind npm ${latest}; restarting via npx --prefer-online`
1438
- );
1439
- const spawnFn = io.spawnFn ?? spawn;
1440
- let spawnFailed = false;
1441
- const child = spawnFn(command, args2, {
1442
- stdio: "inherit",
1443
- env: {
1444
- ...process.env,
1445
- [MCP_SKIP_SELF_UPDATE_ENV]: "1"
1446
- },
1447
- shell: process.platform === "win32",
1448
- windowsHide: true
1449
- });
1450
- child.on?.("error", () => {
1451
- spawnFailed = true;
1452
- });
1453
- const wait = io.waitForChild ?? waitForSpawnedChild;
1454
- const code = await wait(child);
1455
- if (spawnFailed) {
1456
- log(
1457
- `[mcp] self-update spawn failed; continuing on ${MCP_NPM_PACKAGE}@${installed}`
1458
- );
1459
- return false;
1460
- }
1461
- if (code !== 0) process.exitCode = code;
1462
- return true;
1463
- }
1464
1319
  var ALGORITHM = "aes-256-gcm";
1465
1320
  var IV_LENGTH = 16;
1466
1321
  var AUTH_TAG_LENGTH = 16;
@@ -9174,6 +9029,11 @@ function normalizeAllowedTools(value) {
9174
9029
  const tools = value.filter((t) => typeof t === "string").map((t) => t.trim()).filter(Boolean);
9175
9030
  return tools.length > 0 ? tools : null;
9176
9031
  }
9032
+ function isMcpToolAllowed(name, allowed, denied) {
9033
+ if (denied?.includes(name)) return false;
9034
+ if (allowed && allowed.length > 0) return allowed.includes(name);
9035
+ return true;
9036
+ }
9177
9037
  var authContext = null;
9178
9038
  var currentClientIp;
9179
9039
  function extractClientIp(req) {
@@ -9202,7 +9062,7 @@ async function validateApiKey(key) {
9202
9062
  return null;
9203
9063
  }
9204
9064
  const apiKeyRows = await db.execute(sql`
9205
- SELECT id, name, created_by, allowed_server_ids, allowed_tools, is_active, expires_at
9065
+ SELECT id, name, created_by, allowed_server_ids, allowed_tools, denied_tools, is_active, expires_at
9206
9066
  FROM dashboard_mcp_api_key
9207
9067
  WHERE api_key_hash = ${keyHash} AND is_active = true
9208
9068
  LIMIT 1
@@ -9255,6 +9115,7 @@ async function validateApiKey(key) {
9255
9115
  userId: data.created_by,
9256
9116
  allowedServerIds,
9257
9117
  allowedTools: normalizeAllowedTools(data.allowed_tools),
9118
+ deniedTools: normalizeAllowedTools(data.denied_tools),
9258
9119
  permissions,
9259
9120
  roleName
9260
9121
  };
@@ -9449,7 +9310,7 @@ async function validateSshKey(pubkeyPathInput, expectedApiKeyId) {
9449
9310
  return null;
9450
9311
  }
9451
9312
  const apiRows = await db.execute(sql`
9452
- SELECT id, name, created_by, allowed_server_ids, allowed_tools, is_active, expires_at
9313
+ SELECT id, name, created_by, allowed_server_ids, allowed_tools, denied_tools, is_active, expires_at
9453
9314
  FROM dashboard_mcp_api_key
9454
9315
  WHERE id = ${keyRow.api_key_id} AND is_active = true
9455
9316
  LIMIT 1
@@ -9504,6 +9365,7 @@ async function validateSshKey(pubkeyPathInput, expectedApiKeyId) {
9504
9365
  userId: apiRow.created_by,
9505
9366
  allowedServerIds,
9506
9367
  allowedTools: normalizeAllowedTools(apiRow.allowed_tools),
9368
+ deniedTools: normalizeAllowedTools(apiRow.denied_tools),
9507
9369
  permissions,
9508
9370
  roleName
9509
9371
  };
@@ -13776,13 +13638,27 @@ var TOOLS = [
13776
13638
  // ----- Trigger.dev -----
13777
13639
  ...TRIGGER_TOOLS
13778
13640
  ];
13641
+ function readInstalledMcpVersion() {
13642
+ try {
13643
+ const here = dirname(fileURLToPath(import.meta.url));
13644
+ const base = here.split(sep).pop();
13645
+ const packageRoot = base === "dist" || base === "src" ? join(here, "..") : here;
13646
+ const parsed = JSON.parse(
13647
+ readFileSync(join(packageRoot, "package.json"), "utf8")
13648
+ );
13649
+ return typeof parsed.version === "string" ? parsed.version : null;
13650
+ } catch {
13651
+ return null;
13652
+ }
13653
+ }
13779
13654
  var MCP_VERSION = readInstalledMcpVersion() ?? "0.0.0";
13780
13655
  async function handleListTools() {
13781
13656
  if (!authContext) return { tools: TOOLS };
13782
13657
  const allowedTools = authContext.allowedTools;
13783
13658
  const isSuperadmin = authContext.roleName === "superadmin";
13784
13659
  const accessible = TOOLS.filter((tool) => {
13785
- if (allowedTools && !allowedTools.includes(tool.name)) return false;
13660
+ if (!isMcpToolAllowed(tool.name, allowedTools, authContext.deniedTools))
13661
+ return false;
13786
13662
  if (SUPERADMIN_ONLY_TOOLS.has(tool.name) && !isSuperadmin) return false;
13787
13663
  const requiredModule = TOOL_MODULE_MAP[tool.name];
13788
13664
  if (!requiredModule) return true;
@@ -13796,12 +13672,16 @@ async function handleCallTool(request) {
13796
13672
  }
13797
13673
  const { name, arguments: toolArgs } = request.params;
13798
13674
  const a = toolArgs || {};
13799
- if (authContext.allowedTools && !authContext.allowedTools.includes(name)) {
13675
+ if (!isMcpToolAllowed(
13676
+ name,
13677
+ authContext.allowedTools,
13678
+ authContext.deniedTools
13679
+ )) {
13800
13680
  return {
13801
13681
  content: [
13802
13682
  {
13803
13683
  type: "text",
13804
- text: `Access denied: this API key is restricted to a specific tool allowlist and may not call "${name}". Allowed: ${authContext.allowedTools.join(", ")}`
13684
+ text: `Access denied: this API key may not call "${name}".`
13805
13685
  }
13806
13686
  ]
13807
13687
  };
@@ -17541,11 +17421,6 @@ function createMcpServer() {
17541
17421
  var server = createMcpServer();
17542
17422
  async function main() {
17543
17423
  console.error("Starting MG Dashboard MCP Server...");
17544
- const replaced = await maybeReexecLatestMcp({
17545
- argv: process.argv.slice(2),
17546
- dashboardBaseUrl
17547
- });
17548
- if (replaced) return;
17549
17424
  const apiAuthContext = await validateApiKey(apiKey);
17550
17425
  if (!apiAuthContext) {
17551
17426
  console.error("API key validation failed");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgsoftwarebv/mg-dashboard-mcp",
3
- "version": "7.4.18",
3
+ "version": "7.4.20",
4
4
  "description": "MCP Server for MG Dashboard - SSH, SFTP, Docker, domains, DNS, and environment config tools for Cursor",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",