@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 CHANGED
@@ -460,7 +460,7 @@ var connectionConfig = {
460
460
  }
461
461
  },
462
462
  connection: {
463
- application_name: `mg-dashboard-${process.env.VERCEL ? "vercel" : process.env.TRIGGER_RUN_ID ? "trigger" : "local"}`
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/2.2.0",
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.3.0";
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
- const current = await mijnhostFetch(
5989
- `/domains/${encodeURIComponent(domain)}/dns`
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
- if (idx === -1) {
6011
- throw new Error(
6012
- `No matching DNS record found: ${type} ${dnsName} = ${oldValue}
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
- const updated = [...current.data.records];
6017
- const before = updated[idx];
6018
- const after = {
6019
- type,
6020
- name: dnsName,
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
- `No matching DNS record found: ${type} ${dnsName} = ${value}
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
- nextRecords = remaining;
6044
- diffArgs = { verb: "delete", removed };
6045
- okMessage = `DNS record deleted: ${type} ${dnsName} = ${value} (${remaining.length} records remaining)`;
6046
- }
6047
- if (dryRun) {
6048
- const diff = formatDnsDiff(domain, current.data.records, nextRecords, diffArgs);
6049
- return { content: [{ type: "text", text: diff }] };
6050
- }
6051
- await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
6052
- method: "PUT",
6053
- body: JSON.stringify({ records: nextRecords })
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)) {