@mgsoftwarebv/mg-dashboard-mcp 7.4.19 → 7.4.21

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 +293 -5
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1316,6 +1316,257 @@ async function proxyJson(ctx, route, body) {
1316
1316
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1317
1317
  };
1318
1318
  }
1319
+
1320
+ // src/mailserver-tools.ts
1321
+ var MAILSERVER_TOOL_NAME = "mailserver";
1322
+ var MAILSERVER_ACTIONS = [
1323
+ "list-domains",
1324
+ "add-domain",
1325
+ "verify-domain",
1326
+ "list-mailboxes",
1327
+ "create-mailbox",
1328
+ "list-keys",
1329
+ "create-key",
1330
+ "send"
1331
+ ];
1332
+ var DEFAULT_MAILSERVER_URL = "https://mailserver.mgsoftware.nl";
1333
+ var MAILSERVER_TOOLS = [
1334
+ {
1335
+ name: MAILSERVER_TOOL_NAME,
1336
+ description: "Configure internal MG Mailserver (mailserver.mgsoftware.nl, send domain mgsoftware.email). Do not use the Postgres tables directly. Actions: list-domains, add-domain, verify-domain, list-mailboxes, create-mailbox, list-keys, create-key (plaintext secret once), send. Customer mailboxes stay on mijn.host \u2014 this is internal Cloudflare Email only.",
1337
+ inputSchema: {
1338
+ type: "object",
1339
+ properties: {
1340
+ action: {
1341
+ type: "string",
1342
+ enum: [...MAILSERVER_ACTIONS],
1343
+ description: "Which mailserver operation to run."
1344
+ },
1345
+ domain: {
1346
+ type: "string",
1347
+ description: "action=add-domain: hostname to register (e.g. mgsoftware.email)."
1348
+ },
1349
+ domainId: {
1350
+ type: "string",
1351
+ description: "UUID of a domains row (verify-domain, create-mailbox, create-key)."
1352
+ },
1353
+ email: {
1354
+ type: "string",
1355
+ description: "action=create-mailbox: full address on a registered domain."
1356
+ },
1357
+ password: {
1358
+ type: "string",
1359
+ description: "action=create-mailbox: webmail password (min 8 chars)."
1360
+ },
1361
+ displayName: {
1362
+ type: "string",
1363
+ description: "action=create-mailbox: optional display name."
1364
+ },
1365
+ keyName: {
1366
+ type: "string",
1367
+ description: "action=create-key: label for the frs_ key."
1368
+ },
1369
+ permissions: {
1370
+ type: "array",
1371
+ items: { type: "string" },
1372
+ description: 'action=create-key: default ["send"].'
1373
+ },
1374
+ from: {
1375
+ type: "string",
1376
+ description: "action=send: From (e.g. MG Mailserver <noreply@mgsoftware.email>)."
1377
+ },
1378
+ to: {
1379
+ type: "string",
1380
+ description: "action=send: recipient email (or comma-separated)."
1381
+ },
1382
+ subject: { type: "string", description: "action=send: subject." },
1383
+ text: { type: "string", description: "action=send: plain text body." },
1384
+ html: { type: "string", description: "action=send: HTML body." }
1385
+ },
1386
+ required: ["action"]
1387
+ }
1388
+ }
1389
+ ];
1390
+ function tokenFromDotenv(content) {
1391
+ for (const line of content.split(/\r?\n/)) {
1392
+ const trimmed = line.trim();
1393
+ if (!trimmed || trimmed.startsWith("#")) continue;
1394
+ const eq4 = trimmed.indexOf("=");
1395
+ if (eq4 < 1) continue;
1396
+ const key = trimmed.slice(0, eq4).trim();
1397
+ if (key !== "MAILSERVER_MCP_TOKEN") continue;
1398
+ let value = trimmed.slice(eq4 + 1).trim();
1399
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1400
+ value = value.slice(1, -1);
1401
+ }
1402
+ return value;
1403
+ }
1404
+ return "";
1405
+ }
1406
+ function asString(value) {
1407
+ return typeof value === "string" ? value.trim() : "";
1408
+ }
1409
+ function planMailserverCall(action, args2) {
1410
+ switch (action) {
1411
+ case "list-domains":
1412
+ return { method: "GET", path: "/api/domains" };
1413
+ case "add-domain": {
1414
+ const domain = asString(args2.domain);
1415
+ if (!domain) return { error: "add-domain needs `domain`" };
1416
+ return { method: "POST", path: "/api/domains", body: { domain } };
1417
+ }
1418
+ case "verify-domain": {
1419
+ const domainId = asString(args2.domainId);
1420
+ if (!domainId) return { error: "verify-domain needs `domainId`" };
1421
+ return { method: "POST", path: `/api/domains/${domainId}/verify` };
1422
+ }
1423
+ case "list-mailboxes":
1424
+ return { method: "GET", path: "/api/imap/accounts" };
1425
+ case "create-mailbox": {
1426
+ const email = asString(args2.email);
1427
+ const password = asString(args2.password);
1428
+ const domainId = asString(args2.domainId);
1429
+ if (!email) return { error: "create-mailbox needs `email`" };
1430
+ if (password.length < 8) return { error: "create-mailbox needs `password` (min 8)" };
1431
+ if (!domainId) return { error: "create-mailbox needs `domainId` (or let the handler resolve it)" };
1432
+ return {
1433
+ method: "POST",
1434
+ path: "/api/imap/accounts",
1435
+ body: {
1436
+ email,
1437
+ password,
1438
+ domain_id: domainId,
1439
+ display_name: asString(args2.displayName) || void 0
1440
+ }
1441
+ };
1442
+ }
1443
+ case "list-keys":
1444
+ return { method: "GET", path: "/api/api-keys" };
1445
+ case "create-key": {
1446
+ const domainId = asString(args2.domainId);
1447
+ const keyName = asString(args2.keyName);
1448
+ if (!domainId) return { error: "create-key needs `domainId`" };
1449
+ if (!keyName) return { error: "create-key needs `keyName`" };
1450
+ const permissions = Array.isArray(args2.permissions) ? args2.permissions.filter((p) => typeof p === "string") : ["send"];
1451
+ return {
1452
+ method: "POST",
1453
+ path: "/api/api-keys",
1454
+ body: { domainId, keyName, permissions }
1455
+ };
1456
+ }
1457
+ case "send": {
1458
+ const from = asString(args2.from);
1459
+ const toRaw = asString(args2.to);
1460
+ const subject = asString(args2.subject);
1461
+ if (!from || !toRaw || !subject) {
1462
+ return { error: "send needs `from`, `to`, and `subject`" };
1463
+ }
1464
+ const to = toRaw.includes(",") ? toRaw.split(",").map((s) => s.trim()).filter(Boolean) : toRaw;
1465
+ return {
1466
+ method: "POST",
1467
+ path: "/api/emails",
1468
+ body: {
1469
+ from,
1470
+ to,
1471
+ subject,
1472
+ text: asString(args2.text) || void 0,
1473
+ html: asString(args2.html) || void 0
1474
+ }
1475
+ };
1476
+ }
1477
+ default:
1478
+ return {
1479
+ error: `Unknown action "${action}". Use: ${MAILSERVER_ACTIONS.join(", ")}`
1480
+ };
1481
+ }
1482
+ }
1483
+ function domainListFromPayload(payload) {
1484
+ if (!payload || typeof payload !== "object") return [];
1485
+ const data = payload.data;
1486
+ const list = data?.domains;
1487
+ if (!Array.isArray(list)) return [];
1488
+ return list.filter(
1489
+ (d) => Boolean(d) && typeof d === "object" && typeof d.id === "string" && typeof d.domain === "string"
1490
+ );
1491
+ }
1492
+ async function mailserverFetch(deps, plan) {
1493
+ const fetchFn = deps.fetchFn ?? fetch;
1494
+ const url = `${deps.baseUrl.replace(/\/$/, "")}${plan.path}`;
1495
+ const res = await fetchFn(url, {
1496
+ method: plan.method,
1497
+ headers: {
1498
+ authorization: `Bearer ${deps.token}`,
1499
+ ...plan.body ? { "content-type": "application/json" } : {}
1500
+ },
1501
+ body: plan.body ? JSON.stringify(plan.body) : void 0
1502
+ });
1503
+ const text21 = await res.text();
1504
+ let json = text21;
1505
+ try {
1506
+ json = text21 ? JSON.parse(text21) : null;
1507
+ } catch {
1508
+ json = { error: text21.slice(0, 500) };
1509
+ }
1510
+ return { status: res.status, json };
1511
+ }
1512
+ async function handleMailserverTool(args2, deps) {
1513
+ const action = asString(args2.action);
1514
+ if (!deps.token) {
1515
+ return {
1516
+ content: [
1517
+ {
1518
+ type: "text",
1519
+ text: "MAILSERVER_MCP_TOKEN is not set (env or MG Mailserver env-store web/production)."
1520
+ }
1521
+ ]
1522
+ };
1523
+ }
1524
+ if (action === "create-mailbox" && !asString(args2.domainId)) {
1525
+ const email = asString(args2.email);
1526
+ const host = email.split("@")[1]?.toLowerCase();
1527
+ if (!host) {
1528
+ return {
1529
+ content: [{ type: "text", text: "create-mailbox needs a full `email` (user@domain)." }]
1530
+ };
1531
+ }
1532
+ const listed = await mailserverFetch(deps, { method: "GET", path: "/api/domains" });
1533
+ const match = domainListFromPayload(listed.json).find(
1534
+ (d) => d.domain.toLowerCase() === host
1535
+ );
1536
+ if (!match) {
1537
+ return {
1538
+ content: [
1539
+ {
1540
+ type: "text",
1541
+ text: JSON.stringify(
1542
+ {
1543
+ error: `No registered domain for ${host}. add-domain first.`,
1544
+ status: listed.status,
1545
+ domains: listed.json
1546
+ },
1547
+ null,
1548
+ 2
1549
+ )
1550
+ }
1551
+ ]
1552
+ };
1553
+ }
1554
+ args2 = { ...args2, domainId: match.id };
1555
+ }
1556
+ const plan = planMailserverCall(action, args2);
1557
+ if ("error" in plan) {
1558
+ return { content: [{ type: "text", text: plan.error }] };
1559
+ }
1560
+ const result = await mailserverFetch(deps, plan);
1561
+ return {
1562
+ content: [
1563
+ {
1564
+ type: "text",
1565
+ text: JSON.stringify({ status: result.status, body: result.json }, null, 2)
1566
+ }
1567
+ ]
1568
+ };
1569
+ }
1319
1570
  var ALGORITHM = "aes-256-gcm";
1320
1571
  var IV_LENGTH = 16;
1321
1572
  var AUTH_TAG_LENGTH = 16;
@@ -9029,6 +9280,11 @@ function normalizeAllowedTools(value) {
9029
9280
  const tools = value.filter((t) => typeof t === "string").map((t) => t.trim()).filter(Boolean);
9030
9281
  return tools.length > 0 ? tools : null;
9031
9282
  }
9283
+ function isMcpToolAllowed(name, allowed, denied) {
9284
+ if (denied?.includes(name)) return false;
9285
+ if (allowed && allowed.length > 0) return allowed.includes(name);
9286
+ return true;
9287
+ }
9032
9288
  var authContext = null;
9033
9289
  var currentClientIp;
9034
9290
  function extractClientIp(req) {
@@ -9057,7 +9313,7 @@ async function validateApiKey(key) {
9057
9313
  return null;
9058
9314
  }
9059
9315
  const apiKeyRows = await db.execute(sql`
9060
- SELECT id, name, created_by, allowed_server_ids, allowed_tools, is_active, expires_at
9316
+ SELECT id, name, created_by, allowed_server_ids, allowed_tools, denied_tools, is_active, expires_at
9061
9317
  FROM dashboard_mcp_api_key
9062
9318
  WHERE api_key_hash = ${keyHash} AND is_active = true
9063
9319
  LIMIT 1
@@ -9110,6 +9366,7 @@ async function validateApiKey(key) {
9110
9366
  userId: data.created_by,
9111
9367
  allowedServerIds,
9112
9368
  allowedTools: normalizeAllowedTools(data.allowed_tools),
9369
+ deniedTools: normalizeAllowedTools(data.denied_tools),
9113
9370
  permissions,
9114
9371
  roleName
9115
9372
  };
@@ -9304,7 +9561,7 @@ async function validateSshKey(pubkeyPathInput, expectedApiKeyId) {
9304
9561
  return null;
9305
9562
  }
9306
9563
  const apiRows = await db.execute(sql`
9307
- SELECT id, name, created_by, allowed_server_ids, allowed_tools, is_active, expires_at
9564
+ SELECT id, name, created_by, allowed_server_ids, allowed_tools, denied_tools, is_active, expires_at
9308
9565
  FROM dashboard_mcp_api_key
9309
9566
  WHERE id = ${keyRow.api_key_id} AND is_active = true
9310
9567
  LIMIT 1
@@ -9359,6 +9616,7 @@ async function validateSshKey(pubkeyPathInput, expectedApiKeyId) {
9359
9616
  userId: apiRow.created_by,
9360
9617
  allowedServerIds,
9361
9618
  allowedTools: normalizeAllowedTools(apiRow.allowed_tools),
9619
+ deniedTools: normalizeAllowedTools(apiRow.denied_tools),
9362
9620
  permissions,
9363
9621
  roleName
9364
9622
  };
@@ -9547,6 +9805,23 @@ function decrypt2(payload) {
9547
9805
  decrypted += decipher.final("utf8");
9548
9806
  return decrypted;
9549
9807
  }
9808
+ async function readMailserverMcpTokenFromEnvStore() {
9809
+ try {
9810
+ const { stageIds } = await resolveReleaseProfileStageIds("MG Mailserver");
9811
+ const data = await db.execute(sql`
9812
+ SELECT env_data_encrypted
9813
+ FROM env_config
9814
+ WHERE app_name = 'web'
9815
+ AND environment = 'production'
9816
+ AND release_profile_stage_id = ANY(${uuidArrayParam(stageIds)})
9817
+ `);
9818
+ const row = data[0];
9819
+ if (!row) return "";
9820
+ return tokenFromDotenv(decrypt2(row.env_data_encrypted));
9821
+ } catch {
9822
+ return "";
9823
+ }
9824
+ }
9550
9825
  function posixQuote(arg) {
9551
9826
  if (arg === "") return "''";
9552
9827
  if (/^[A-Za-z0-9._\/=:@%+\-]+$/.test(arg)) return arg;
@@ -13628,6 +13903,7 @@ var TOOLS = [
13628
13903
  required: ["action"]
13629
13904
  }
13630
13905
  },
13906
+ ...MAILSERVER_TOOLS,
13631
13907
  // ----- Trigger.dev -----
13632
13908
  ...TRIGGER_TOOLS
13633
13909
  ];
@@ -13650,7 +13926,8 @@ async function handleListTools() {
13650
13926
  const allowedTools = authContext.allowedTools;
13651
13927
  const isSuperadmin = authContext.roleName === "superadmin";
13652
13928
  const accessible = TOOLS.filter((tool) => {
13653
- if (allowedTools && !allowedTools.includes(tool.name)) return false;
13929
+ if (!isMcpToolAllowed(tool.name, allowedTools, authContext.deniedTools))
13930
+ return false;
13654
13931
  if (SUPERADMIN_ONLY_TOOLS.has(tool.name) && !isSuperadmin) return false;
13655
13932
  const requiredModule = TOOL_MODULE_MAP[tool.name];
13656
13933
  if (!requiredModule) return true;
@@ -13664,12 +13941,16 @@ async function handleCallTool(request) {
13664
13941
  }
13665
13942
  const { name, arguments: toolArgs } = request.params;
13666
13943
  const a = toolArgs || {};
13667
- if (authContext.allowedTools && !authContext.allowedTools.includes(name)) {
13944
+ if (!isMcpToolAllowed(
13945
+ name,
13946
+ authContext.allowedTools,
13947
+ authContext.deniedTools
13948
+ )) {
13668
13949
  return {
13669
13950
  content: [
13670
13951
  {
13671
13952
  type: "text",
13672
- text: `Access denied: this API key is restricted to a specific tool allowlist and may not call "${name}". Allowed: ${authContext.allowedTools.join(", ")}`
13953
+ text: `Access denied: this API key may not call "${name}".`
13673
13954
  }
13674
13955
  ]
13675
13956
  };
@@ -17383,6 +17664,13 @@ Install: GET https://dashboard.mgsoftware.nl/api/downloads/versions.txt then \u2
17383
17664
  ]
17384
17665
  };
17385
17666
  }
17667
+ case MAILSERVER_TOOL_NAME: {
17668
+ const token = process.env.MAILSERVER_MCP_TOKEN?.trim() || await readMailserverMcpTokenFromEnvStore();
17669
+ return handleMailserverTool(a, {
17670
+ baseUrl: process.env.MAILSERVER_URL?.trim() || DEFAULT_MAILSERVER_URL,
17671
+ token
17672
+ });
17673
+ }
17386
17674
  default:
17387
17675
  if (TRIGGER_TOOL_NAMES.has(name)) {
17388
17676
  return handleTriggerTool(name, a, { sshExec, getServerConnection });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mgsoftwarebv/mg-dashboard-mcp",
3
- "version": "7.4.19",
3
+ "version": "7.4.21",
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",