@mgsoftwarebv/mg-dashboard-mcp 6.5.0 → 6.6.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 +122 -64
- package/dist/index.js.map +1 -1
- package/package.json +50 -50
package/dist/index.js
CHANGED
|
@@ -460,7 +460,7 @@ var connectionConfig = {
|
|
|
460
460
|
}
|
|
461
461
|
},
|
|
462
462
|
connection: {
|
|
463
|
-
application_name: `mg-dashboard-${process.env.
|
|
463
|
+
application_name: `mg-dashboard-${process.env.TRIGGER_RUN_ID ? "trigger" : "local"}`
|
|
464
464
|
}
|
|
465
465
|
};
|
|
466
466
|
function getDatabaseUrl() {
|
|
@@ -3924,7 +3924,7 @@ async function mijnhostFetch(path, options = {}) {
|
|
|
3924
3924
|
"API-Key": key,
|
|
3925
3925
|
"Accept": "application/json",
|
|
3926
3926
|
"Content-Type": "application/json",
|
|
3927
|
-
"User-Agent": "mg-dashboard-mcp/
|
|
3927
|
+
"User-Agent": "mg-dashboard-mcp/6.6.0",
|
|
3928
3928
|
...options.headers || {}
|
|
3929
3929
|
}
|
|
3930
3930
|
});
|
|
@@ -3935,6 +3935,36 @@ async function mijnhostFetch(path, options = {}) {
|
|
|
3935
3935
|
}
|
|
3936
3936
|
return body;
|
|
3937
3937
|
}
|
|
3938
|
+
var DNS_MIN_INTERVAL_MS = 750;
|
|
3939
|
+
var DNS_RETRY_BACKOFF_MS = 1500;
|
|
3940
|
+
var dnsZoneQueue = /* @__PURE__ */ new Map();
|
|
3941
|
+
var dnsLastPutAt = /* @__PURE__ */ new Map();
|
|
3942
|
+
function sleep(ms) {
|
|
3943
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3944
|
+
}
|
|
3945
|
+
async function withDnsZoneLock(domain, fn) {
|
|
3946
|
+
const key = domain.toLowerCase();
|
|
3947
|
+
const previous = dnsZoneQueue.get(key) ?? Promise.resolve();
|
|
3948
|
+
const run = previous.then(async () => {
|
|
3949
|
+
const last = dnsLastPutAt.get(key) ?? 0;
|
|
3950
|
+
const wait = DNS_MIN_INTERVAL_MS - (Date.now() - last);
|
|
3951
|
+
if (wait > 0) await sleep(wait);
|
|
3952
|
+
return fn();
|
|
3953
|
+
});
|
|
3954
|
+
const queued = run.finally(() => {
|
|
3955
|
+
if (dnsZoneQueue.get(key) === queued) dnsZoneQueue.delete(key);
|
|
3956
|
+
});
|
|
3957
|
+
dnsZoneQueue.set(key, queued.catch(() => {
|
|
3958
|
+
}));
|
|
3959
|
+
return run;
|
|
3960
|
+
}
|
|
3961
|
+
function markDnsPut(domain) {
|
|
3962
|
+
dnsLastPutAt.set(domain.toLowerCase(), Date.now());
|
|
3963
|
+
}
|
|
3964
|
+
function isTransientMijnhostError(err) {
|
|
3965
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3966
|
+
return /Internal Server Error|API error: 5\d\d|fetch failed|ETIMEDOUT|ECONNRESET/i.test(msg);
|
|
3967
|
+
}
|
|
3938
3968
|
var TOOLS = [
|
|
3939
3969
|
{
|
|
3940
3970
|
name: "list-servers",
|
|
@@ -4298,7 +4328,7 @@ var TOOLS = [
|
|
|
4298
4328
|
},
|
|
4299
4329
|
{
|
|
4300
4330
|
name: "dns-record",
|
|
4301
|
-
description: 'Mutate a single DNS record on a mijn.host domain. Pick the mutation with `action`:\n- "create": add a new record. Required: type, name, value. Optional: ttl (default 3600).\n- "update": replace an existing record. Required: type, name, oldValue, newValue. Optional: ttl.\n- "delete": remove a record. Required: type, name, value.\n\nAlways pass `dryRun: true` first when touching MX / SPF / DKIM / DMARC \u2014 returns a full before/after diff with a mail-auth warning and applies nothing. Re-run without dryRun once the diff looks correct.\n\nUse `dns-list` to inspect the current zone first if you need to identify the right `oldValue`. Requires MIJNHOST_API_KEY.',
|
|
4331
|
+
description: 'Mutate a single DNS record on a mijn.host domain. Pick the mutation with `action`:\n- "create": add a new record. Required: type, name, value. Optional: ttl (default 3600).\n- "update": replace an existing record. Required: type, name, oldValue, newValue. Optional: ttl.\n- "delete": remove a record. Required: type, name, value.\n\nAlways pass `dryRun: true` first when touching MX / SPF / DKIM / DMARC \u2014 returns a full before/after diff with a mail-auth warning and applies nothing. Re-run without dryRun once the diff looks correct.\n\nUse `dns-list` to inspect the current zone first if you need to identify the right `oldValue`. Requires MIJNHOST_API_KEY.\n\nConcurrency: calls are automatically serialized per domain (mijn.host\'s API does GET-then-PUT-the-whole-zone, so parallel calls would race and silently drop records). A minimum 750 ms gap is enforced between PUTs to the same zone, transient 5xx errors are retried once, and a stale-snapshot guard refuses to PUT a zone that suddenly shrunk. You can still fire calls in parallel \u2014 they just queue safely behind each other per domain.',
|
|
4302
4332
|
inputSchema: {
|
|
4303
4333
|
type: "object",
|
|
4304
4334
|
properties: {
|
|
@@ -4320,7 +4350,7 @@ var TOOLS = [
|
|
|
4320
4350
|
// ----- Repo reference -----
|
|
4321
4351
|
...REPO_TOOLS
|
|
4322
4352
|
];
|
|
4323
|
-
var MCP_VERSION = "6.
|
|
4353
|
+
var MCP_VERSION = "6.6.0";
|
|
4324
4354
|
async function handleListTools() {
|
|
4325
4355
|
if (!authContext) return { tools: TOOLS };
|
|
4326
4356
|
const accessible = TOOLS.filter((tool) => {
|
|
@@ -5985,74 +6015,102 @@ ${lines.join("\n")}` }] };
|
|
|
5985
6015
|
if (!domain || !type || !dnsName) {
|
|
5986
6016
|
throw new Error("domain, type, and name are required");
|
|
5987
6017
|
}
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
let nextRecords;
|
|
5992
|
-
let diffArgs;
|
|
5993
|
-
let okMessage;
|
|
5994
|
-
if (action === "create") {
|
|
5995
|
-
const value = typeof a.value === "string" ? a.value : "";
|
|
5996
|
-
if (!value) throw new Error('action="create" requires value');
|
|
5997
|
-
const ttl = Number(a.ttl) || 3600;
|
|
5998
|
-
const newRecord = { type, name: dnsName, value, ttl };
|
|
5999
|
-
nextRecords = [...current.data.records, newRecord];
|
|
6000
|
-
diffArgs = { verb: "create", added: [newRecord] };
|
|
6001
|
-
okMessage = `DNS record created: ${type} ${dnsName} \u2192 ${value} (TTL: ${ttl})`;
|
|
6002
|
-
} else if (action === "update") {
|
|
6003
|
-
const oldValue = typeof a.oldValue === "string" ? a.oldValue : "";
|
|
6004
|
-
const newValue = typeof a.newValue === "string" ? a.newValue : "";
|
|
6005
|
-
if (!oldValue || !newValue) throw new Error('action="update" requires oldValue and newValue');
|
|
6006
|
-
const ttlArg = a.ttl !== void 0 ? Number(a.ttl) : void 0;
|
|
6007
|
-
const idx = current.data.records.findIndex(
|
|
6008
|
-
(r) => r.type === type && r.name === dnsName && r.value === oldValue
|
|
6018
|
+
return withDnsZoneLock(domain, async () => {
|
|
6019
|
+
const fetchZone = () => mijnhostFetch(
|
|
6020
|
+
`/domains/${encodeURIComponent(domain)}/dns`
|
|
6009
6021
|
);
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6022
|
+
let current = await fetchZone();
|
|
6023
|
+
if (current.data.records.length === 0) {
|
|
6024
|
+
await sleep(DNS_RETRY_BACKOFF_MS);
|
|
6025
|
+
current = await fetchZone();
|
|
6026
|
+
}
|
|
6027
|
+
let nextRecords;
|
|
6028
|
+
let diffArgs;
|
|
6029
|
+
let okMessage;
|
|
6030
|
+
if (action === "create") {
|
|
6031
|
+
const value = typeof a.value === "string" ? a.value : "";
|
|
6032
|
+
if (!value) throw new Error('action="create" requires value');
|
|
6033
|
+
const ttl = Number(a.ttl) || 3600;
|
|
6034
|
+
const newRecord = { type, name: dnsName, value, ttl };
|
|
6035
|
+
nextRecords = [...current.data.records, newRecord];
|
|
6036
|
+
diffArgs = { verb: "create", added: [newRecord] };
|
|
6037
|
+
okMessage = `DNS record created: ${type} ${dnsName} \u2192 ${value} (TTL: ${ttl})`;
|
|
6038
|
+
} else if (action === "update") {
|
|
6039
|
+
const oldValue = typeof a.oldValue === "string" ? a.oldValue : "";
|
|
6040
|
+
const newValue = typeof a.newValue === "string" ? a.newValue : "";
|
|
6041
|
+
if (!oldValue || !newValue) throw new Error('action="update" requires oldValue and newValue');
|
|
6042
|
+
const ttlArg = a.ttl !== void 0 ? Number(a.ttl) : void 0;
|
|
6043
|
+
const idx = current.data.records.findIndex(
|
|
6044
|
+
(r) => r.type === type && r.name === dnsName && r.value === oldValue
|
|
6045
|
+
);
|
|
6046
|
+
if (idx === -1) {
|
|
6047
|
+
throw new Error(
|
|
6048
|
+
`No matching DNS record found: ${type} ${dnsName} = ${oldValue}
|
|
6013
6049
|
` + describeDnsCandidates(current.data.records, type, dnsName, oldValue)
|
|
6050
|
+
);
|
|
6051
|
+
}
|
|
6052
|
+
const updated = [...current.data.records];
|
|
6053
|
+
const before = updated[idx];
|
|
6054
|
+
const after = {
|
|
6055
|
+
type,
|
|
6056
|
+
name: dnsName,
|
|
6057
|
+
value: newValue,
|
|
6058
|
+
ttl: ttlArg ?? before.ttl
|
|
6059
|
+
};
|
|
6060
|
+
updated[idx] = after;
|
|
6061
|
+
nextRecords = updated;
|
|
6062
|
+
diffArgs = { verb: "update", removed: [before], added: [after] };
|
|
6063
|
+
okMessage = `DNS record updated: ${type} ${dnsName} \u2192 ${newValue}${ttlArg ? ` (TTL: ${ttlArg})` : ""}`;
|
|
6064
|
+
} else {
|
|
6065
|
+
const value = typeof a.value === "string" ? a.value : "";
|
|
6066
|
+
if (!value) throw new Error('action="delete" requires value');
|
|
6067
|
+
const removed = current.data.records.filter(
|
|
6068
|
+
(r) => r.type === type && r.name === dnsName && r.value === value
|
|
6069
|
+
);
|
|
6070
|
+
const remaining = current.data.records.filter(
|
|
6071
|
+
(r) => !(r.type === type && r.name === dnsName && r.value === value)
|
|
6014
6072
|
);
|
|
6073
|
+
if (removed.length === 0) {
|
|
6074
|
+
throw new Error(
|
|
6075
|
+
`No matching DNS record found: ${type} ${dnsName} = ${value}
|
|
6076
|
+
` + describeDnsCandidates(current.data.records, type, dnsName, value)
|
|
6077
|
+
);
|
|
6078
|
+
}
|
|
6079
|
+
nextRecords = remaining;
|
|
6080
|
+
diffArgs = { verb: "delete", removed };
|
|
6081
|
+
okMessage = `DNS record deleted: ${type} ${dnsName} = ${value} (${remaining.length} records remaining)`;
|
|
6015
6082
|
}
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
value: newValue,
|
|
6022
|
-
ttl: ttlArg ?? before.ttl
|
|
6023
|
-
};
|
|
6024
|
-
updated[idx] = after;
|
|
6025
|
-
nextRecords = updated;
|
|
6026
|
-
diffArgs = { verb: "update", removed: [before], added: [after] };
|
|
6027
|
-
okMessage = `DNS record updated: ${type} ${dnsName} \u2192 ${newValue}${ttlArg ? ` (TTL: ${ttlArg})` : ""}`;
|
|
6028
|
-
} else {
|
|
6029
|
-
const value = typeof a.value === "string" ? a.value : "";
|
|
6030
|
-
if (!value) throw new Error('action="delete" requires value');
|
|
6031
|
-
const removed = current.data.records.filter(
|
|
6032
|
-
(r) => r.type === type && r.name === dnsName && r.value === value
|
|
6033
|
-
);
|
|
6034
|
-
const remaining = current.data.records.filter(
|
|
6035
|
-
(r) => !(r.type === type && r.name === dnsName && r.value === value)
|
|
6036
|
-
);
|
|
6037
|
-
if (removed.length === 0) {
|
|
6083
|
+
if (dryRun) {
|
|
6084
|
+
const diff = formatDnsDiff(domain, current.data.records, nextRecords, diffArgs);
|
|
6085
|
+
return { content: [{ type: "text", text: diff }] };
|
|
6086
|
+
}
|
|
6087
|
+
if (nextRecords.length < 3 && current.data.records.length >= 5 && action !== "delete") {
|
|
6038
6088
|
throw new Error(
|
|
6039
|
-
`
|
|
6040
|
-
` + describeDnsCandidates(current.data.records, type, dnsName, value)
|
|
6089
|
+
`Aborting PUT: would shrink ${domain} from ${current.data.records.length} to ${nextRecords.length} records. This usually indicates a stale zone snapshot from mijn.host. Run dns-list and retry.`
|
|
6041
6090
|
);
|
|
6042
6091
|
}
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6092
|
+
try {
|
|
6093
|
+
await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
|
|
6094
|
+
method: "PUT",
|
|
6095
|
+
body: JSON.stringify({ records: nextRecords })
|
|
6096
|
+
});
|
|
6097
|
+
} catch (err) {
|
|
6098
|
+
if (!isTransientMijnhostError(err)) throw err;
|
|
6099
|
+
await sleep(DNS_RETRY_BACKOFF_MS);
|
|
6100
|
+
const refetched = await fetchZone();
|
|
6101
|
+
const stillNeedsApply = action === "create" ? !refetched.data.records.some((r) => r.type === type && r.name === dnsName && r.value === (typeof a.value === "string" ? a.value : "")) : true;
|
|
6102
|
+
if (!stillNeedsApply) {
|
|
6103
|
+
markDnsPut(domain);
|
|
6104
|
+
return { content: [{ type: "text", text: `${okMessage} (already applied; recovered from transient error)` }] };
|
|
6105
|
+
}
|
|
6106
|
+
await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
|
|
6107
|
+
method: "PUT",
|
|
6108
|
+
body: JSON.stringify({ records: nextRecords })
|
|
6109
|
+
});
|
|
6110
|
+
}
|
|
6111
|
+
markDnsPut(domain);
|
|
6112
|
+
return { content: [{ type: "text", text: okMessage }] };
|
|
6054
6113
|
});
|
|
6055
|
-
return { content: [{ type: "text", text: okMessage }] };
|
|
6056
6114
|
}
|
|
6057
6115
|
default:
|
|
6058
6116
|
if (TRIGGER_TOOL_NAMES.has(name)) {
|