@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.
- package/dist/index.js +293 -5
- 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 (
|
|
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 (
|
|
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
|
|
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