@mgsoftwarebv/mg-dashboard-mcp 5.0.1 → 6.0.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
@@ -317,41 +317,19 @@ var TRIGGER_TOOLS = [
317
317
  }
318
318
  },
319
319
  {
320
- name: "trigger-run-detail",
321
- description: "Get full details of a specific run: status, payload, output, and error stack traces. Use after trigger-runs or trigger-test-task to inspect results.",
320
+ name: "trigger-run",
321
+ description: 'Inspect, create, or replay a Trigger.dev run. Pick the mode with `action`:\n- "detail": fetch full run state \u2014 status, payload, output, error stack traces, and logs. Required: runId.\n- "test": kick off a task run and (by default) wait for it to complete, returning the final detail in one shot. Required: taskId. Optional: payload (JSON string), waitSeconds (default 60, max 300, set 0 to fire-and-forget).\n- "replay": re-run a previously failed/completed run with the same payload. Required: runId. Optional: waitSeconds (when >0, also waits for the new run to finish and returns its full detail).\n\nTip: replace any old `trigger-run-detail` / `trigger-test-task` / `trigger-replay-run` calls with this single tool \u2014 output format is identical.',
322
322
  inputSchema: {
323
323
  type: "object",
324
324
  properties: {
325
- project: { type: "string", description: "Project slug from trigger-list" },
326
- runId: { type: "string", description: "Run ID (e.g. run_xxxxx)" }
327
- },
328
- required: ["project", "runId"]
329
- }
330
- },
331
- {
332
- name: "trigger-test-task",
333
- description: "Trigger a task run and wait for it to complete. Returns the final status, output, or error with stack trace. The main tool for testing trigger tasks from Cursor.",
334
- inputSchema: {
335
- type: "object",
336
- properties: {
337
- project: { type: "string", description: "Project slug from trigger-list" },
338
- taskId: { type: "string", description: 'Task identifier (e.g. "hello-world", "execute-pipeline")' },
339
- payload: { type: "string", description: "JSON payload string to pass to the task (optional)" },
340
- waitSeconds: { type: "number", description: "Max seconds to wait for completion (default 60, max 300)" }
341
- },
342
- required: ["project", "taskId"]
343
- }
344
- },
345
- {
346
- name: "trigger-replay-run",
347
- description: "Replay a previously failed or completed run with the same payload. Returns the new run ID.",
348
- inputSchema: {
349
- type: "object",
350
- properties: {
351
- project: { type: "string", description: "Project slug from trigger-list" },
352
- runId: { type: "string", description: "Run ID to replay (e.g. run_xxxxx)" }
325
+ action: { type: "string", enum: ["detail", "test", "replay"], description: "Which run operation to perform." },
326
+ project: { type: "string", description: 'Project slug from trigger-list (e.g. "mg-dashboard-bHfS")' },
327
+ runId: { type: "string", description: 'Run ID (e.g. run_xxxxx). Required for action="detail" or action="replay".' },
328
+ taskId: { type: "string", description: 'Task identifier (e.g. "hello-world"). Required for action="test".' },
329
+ payload: { type: "string", description: 'JSON payload string passed to the task (action="test" only).' },
330
+ waitSeconds: { type: "number", description: 'Max seconds to wait for completion. Default 60 (action="test") / 0 (action="replay", fire-and-forget). Max 300. Set 0 to skip the wait.' }
353
331
  },
354
- required: ["project", "runId"]
332
+ required: ["action", "project"]
355
333
  }
356
334
  }
357
335
  ];
@@ -359,9 +337,7 @@ var TRIGGER_TOOL_NAMES = new Set(TRIGGER_TOOLS.map((t) => t.name));
359
337
  var TRIGGER_TOOL_MODULE_MAP = {
360
338
  "trigger-list": "ci_cd",
361
339
  "trigger-runs": "ci_cd",
362
- "trigger-run-detail": "ci_cd",
363
- "trigger-test-task": "ci_cd",
364
- "trigger-replay-run": "ci_cd"
340
+ "trigger-run": "ci_cd"
365
341
  };
366
342
  async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
367
343
  const sql = `SELECT re.\\"apiKey\\" FROM \\"RuntimeEnvironment\\" re JOIN \\"Project\\" p ON re.\\"projectId\\" = p.id WHERE p.slug='${projectSlug}' AND re.slug='prod' LIMIT 1`;
@@ -525,104 +501,69 @@ ${rawJson.substring(0, 500)}` }] };
525
501
  return { content: [{ type: "text", text: formatRunsTable(runs) }] };
526
502
  }
527
503
  // -----------------------------------------------------------------
528
- case "trigger-run-detail": {
529
- const project = String(args2.project);
530
- const runId = String(args2.runId);
531
- const instance = await discoverInstance(project, conn, proxy, sshExec2);
532
- const [rawJson, logs] = await Promise.all([
533
- triggerApi(conn, proxy, sshExec2, instance, "GET", `/api/v3/runs/${encodeURIComponent(runId)}`),
534
- fetchRunLogs(runId, conn, proxy, sshExec2)
535
- ]);
536
- let run;
537
- try {
538
- run = JSON.parse(rawJson);
539
- } catch {
540
- return { content: [{ type: "text", text: `Invalid API response:
541
- ${rawJson.substring(0, 500)}` }] };
542
- }
543
- let text = formatRunDetail(run);
544
- if (logs) {
545
- text += "\n\n--- Logs ---\n" + logs;
504
+ case "trigger-run": {
505
+ const action = String(args2.action);
506
+ if (action !== "detail" && action !== "test" && action !== "replay") {
507
+ return { content: [{ type: "text", text: "Error: action must be one of: detail, test, replay" }] };
546
508
  }
547
- return { content: [{ type: "text", text }] };
548
- }
549
- // -----------------------------------------------------------------
550
- case "trigger-test-task": {
551
509
  const project = String(args2.project);
552
- const taskId = String(args2.taskId);
553
- const waitSeconds = Math.min(Math.max(Number(args2.waitSeconds) || 60, 5), 300);
554
- conn.timeout = (waitSeconds + 30) * 1e3;
555
- const instance = await discoverInstance(project, conn, proxy, sshExec2);
556
- let payload = "{}";
557
- if (args2.payload) {
558
- try {
559
- JSON.parse(String(args2.payload));
560
- payload = String(args2.payload);
561
- } catch {
562
- throw new Error(`Invalid JSON payload: ${String(args2.payload).substring(0, 200)}`);
563
- }
510
+ if (action === "detail") {
511
+ const runId2 = String(args2.runId ?? "");
512
+ if (!runId2) return { content: [{ type: "text", text: 'Error: action="detail" requires runId' }] };
513
+ const instance2 = await discoverInstance(project, conn, proxy, sshExec2);
514
+ return await fetchAndFormatRun(conn, proxy, sshExec2, instance2, runId2);
564
515
  }
565
- const triggerBody = JSON.stringify({
566
- payload: JSON.parse(payload),
567
- options: { tags: ["mcp-test"], test: true }
568
- });
569
- const triggerJson = await triggerApi(
570
- conn,
571
- proxy,
572
- sshExec2,
573
- instance,
574
- "POST",
575
- `/api/v1/tasks/${encodeURIComponent(taskId)}/trigger`,
576
- triggerBody
577
- );
578
- let triggerResp;
579
- try {
580
- triggerResp = JSON.parse(triggerJson);
581
- } catch {
582
- return { content: [{ type: "text", text: `Failed to trigger task. API response:
583
- ${triggerJson.substring(0, 500)}` }] };
584
- }
585
- const runId = triggerResp.id;
586
- if (!runId) {
587
- return { content: [{ type: "text", text: `Trigger API did not return a run ID:
588
- ${triggerJson.substring(0, 500)}` }] };
589
- }
590
- const pollInterval = 3e3;
591
- const maxPolls = Math.ceil(waitSeconds * 1e3 / pollInterval);
592
- for (let i = 0; i < maxPolls; i++) {
593
- await new Promise((r) => setTimeout(r, pollInterval));
594
- const pollJson = await triggerApi(
516
+ if (action === "test") {
517
+ const taskId = String(args2.taskId ?? "");
518
+ if (!taskId) return { content: [{ type: "text", text: 'Error: action="test" requires taskId' }] };
519
+ const waitArg2 = args2.waitSeconds === void 0 ? 60 : Number(args2.waitSeconds);
520
+ const waitSeconds2 = Math.min(Math.max(Number.isFinite(waitArg2) ? waitArg2 : 60, 0), 300);
521
+ conn.timeout = (waitSeconds2 + 30) * 1e3;
522
+ const instance2 = await discoverInstance(project, conn, proxy, sshExec2);
523
+ let payload = "{}";
524
+ if (args2.payload) {
525
+ try {
526
+ JSON.parse(String(args2.payload));
527
+ payload = String(args2.payload);
528
+ } catch {
529
+ throw new Error(`Invalid JSON payload: ${String(args2.payload).substring(0, 200)}`);
530
+ }
531
+ }
532
+ const triggerBody = JSON.stringify({
533
+ payload: JSON.parse(payload),
534
+ options: { tags: ["mcp-test"], test: true }
535
+ });
536
+ const triggerJson = await triggerApi(
595
537
  conn,
596
538
  proxy,
597
539
  sshExec2,
598
- instance,
599
- "GET",
600
- `/api/v3/runs/${encodeURIComponent(runId)}`
540
+ instance2,
541
+ "POST",
542
+ `/api/v1/tasks/${encodeURIComponent(taskId)}/trigger`,
543
+ triggerBody
601
544
  );
602
- let run;
545
+ let triggerResp;
603
546
  try {
604
- run = JSON.parse(pollJson);
547
+ triggerResp = JSON.parse(triggerJson);
605
548
  } catch {
606
- continue;
549
+ return { content: [{ type: "text", text: `Failed to trigger task. API response:
550
+ ${triggerJson.substring(0, 500)}` }] };
607
551
  }
608
- if (TERMINAL_STATUSES.has(run.status)) {
609
- let text = formatRunDetail(run);
610
- const logs = await fetchRunLogs(runId, conn, proxy, sshExec2);
611
- if (logs) text += "\n\n--- Logs ---\n" + logs;
612
- return { content: [{ type: "text", text }] };
552
+ const runId2 = triggerResp.id;
553
+ if (!runId2) {
554
+ return { content: [{ type: "text", text: `Trigger API did not return a run ID:
555
+ ${triggerJson.substring(0, 500)}` }] };
556
+ }
557
+ if (waitSeconds2 === 0) {
558
+ return { content: [{ type: "text", text: `Run triggered. ID: ${runId2} (fire-and-forget; use action="detail" to check).` }] };
613
559
  }
560
+ return await waitForCompletion(conn, proxy, sshExec2, instance2, runId2, waitSeconds2);
614
561
  }
615
- return {
616
- content: [{
617
- type: "text",
618
- text: `Run ${runId} did not complete within ${waitSeconds}s (still running). Use trigger-run-detail to check later.`
619
- }]
620
- };
621
- }
622
- // -----------------------------------------------------------------
623
- case "trigger-replay-run": {
624
- const project = String(args2.project);
625
- const runId = String(args2.runId);
562
+ const runId = String(args2.runId ?? "");
563
+ if (!runId) return { content: [{ type: "text", text: 'Error: action="replay" requires runId' }] };
564
+ const waitArg = args2.waitSeconds === void 0 ? 0 : Number(args2.waitSeconds);
565
+ const waitSeconds = Math.min(Math.max(Number.isFinite(waitArg) ? waitArg : 0, 0), 300);
566
+ if (waitSeconds > 0) conn.timeout = (waitSeconds + 30) * 1e3;
626
567
  const instance = await discoverInstance(project, conn, proxy, sshExec2);
627
568
  const rawJson = await triggerApi(
628
569
  conn,
@@ -639,19 +580,69 @@ ${triggerJson.substring(0, 500)}` }] };
639
580
  return { content: [{ type: "text", text: `Replay response:
640
581
  ${rawJson.substring(0, 500)}` }] };
641
582
  }
642
- return {
643
- content: [{
644
- type: "text",
645
- text: result.id ? `Run replayed. New run ID: ${result.id}` : `Replay response:
646
- ${rawJson.substring(0, 500)}`
647
- }]
648
- };
583
+ if (!result.id) {
584
+ return { content: [{ type: "text", text: `Replay response:
585
+ ${rawJson.substring(0, 500)}` }] };
586
+ }
587
+ if (waitSeconds === 0) {
588
+ return { content: [{ type: "text", text: `Run replayed. New run ID: ${result.id} (use action="detail" to check status).` }] };
589
+ }
590
+ return await waitForCompletion(conn, proxy, sshExec2, instance, result.id, waitSeconds);
649
591
  }
650
592
  // -----------------------------------------------------------------
651
593
  default:
652
594
  return { content: [{ type: "text", text: `Unknown trigger tool: ${name}` }] };
653
595
  }
654
596
  }
597
+ async function fetchAndFormatRun(conn, proxy, sshExec2, instance, runId) {
598
+ const [rawJson, logs] = await Promise.all([
599
+ triggerApi(conn, proxy, sshExec2, instance, "GET", `/api/v3/runs/${encodeURIComponent(runId)}`),
600
+ fetchRunLogs(runId, conn, proxy, sshExec2)
601
+ ]);
602
+ let run;
603
+ try {
604
+ run = JSON.parse(rawJson);
605
+ } catch {
606
+ return { content: [{ type: "text", text: `Invalid API response:
607
+ ${rawJson.substring(0, 500)}` }] };
608
+ }
609
+ let text = formatRunDetail(run);
610
+ if (logs) text += "\n\n--- Logs ---\n" + logs;
611
+ return { content: [{ type: "text", text }] };
612
+ }
613
+ async function waitForCompletion(conn, proxy, sshExec2, instance, runId, waitSeconds) {
614
+ const pollInterval = 3e3;
615
+ const maxPolls = Math.ceil(waitSeconds * 1e3 / pollInterval);
616
+ for (let i = 0; i < maxPolls; i++) {
617
+ await new Promise((r) => setTimeout(r, pollInterval));
618
+ const pollJson = await triggerApi(
619
+ conn,
620
+ proxy,
621
+ sshExec2,
622
+ instance,
623
+ "GET",
624
+ `/api/v3/runs/${encodeURIComponent(runId)}`
625
+ );
626
+ let run;
627
+ try {
628
+ run = JSON.parse(pollJson);
629
+ } catch {
630
+ continue;
631
+ }
632
+ if (TERMINAL_STATUSES.has(run.status)) {
633
+ let text = formatRunDetail(run);
634
+ const logs = await fetchRunLogs(runId, conn, proxy, sshExec2);
635
+ if (logs) text += "\n\n--- Logs ---\n" + logs;
636
+ return { content: [{ type: "text", text }] };
637
+ }
638
+ }
639
+ return {
640
+ content: [{
641
+ type: "text",
642
+ text: `Run ${runId} did not complete within ${waitSeconds}s (still running). Use action="detail" with this runId to check later.`
643
+ }]
644
+ };
645
+ }
655
646
 
656
647
  // src/vercel-tools.ts
657
648
  var VERCEL_API = "https://api.vercel.com";
@@ -1419,9 +1410,7 @@ var TOOL_MODULE_MAP = {
1419
1410
  "env-store": "ci_cd",
1420
1411
  "domain-list": "domains",
1421
1412
  "dns-list": "domains",
1422
- "dns-create": "domains",
1423
- "dns-update": "domains",
1424
- "dns-delete": "domains",
1413
+ "dns-record": "domains",
1425
1414
  ...TRIGGER_TOOL_MODULE_MAP,
1426
1415
  ...VERCEL_TOOL_MODULE_MAP
1427
1416
  };
@@ -3843,51 +3832,22 @@ var TOOLS = [
3843
3832
  }
3844
3833
  },
3845
3834
  {
3846
- name: "dns-create",
3847
- description: "Add a new DNS record to a domain. Uses PATCH to add without replacing existing records.\n\nPass `dryRun: true` to preview the proposed change (full before/after diff) without calling the mijn.host PUT \u2014 recommended before touching MX, SPF, DKIM, or DMARC records.",
3848
- inputSchema: {
3849
- type: "object",
3850
- properties: {
3851
- domain: { type: "string", description: "Domain name (e.g. example.com)" },
3852
- type: { type: "string", description: "Record type: A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, or TLSA" },
3853
- name: { type: "string", description: "Record name (e.g. @ or subdomain)" },
3854
- value: { type: "string", description: "Record value (e.g. IP address, hostname)" },
3855
- ttl: { type: "number", description: "TTL in seconds (min 60, default 3600)" },
3856
- dryRun: { type: "boolean", description: "Preview the change (returns proposed diff) without applying." }
3857
- },
3858
- required: ["domain", "type", "name", "value"]
3859
- }
3860
- },
3861
- {
3862
- name: "dns-update",
3863
- description: "Update an existing DNS record. Identifies the record by type+name+oldValue, then replaces it with new values via PATCH.\n\nPass `dryRun: true` to preview the change without applying \u2014 strongly recommended for MX/SPF/DKIM/DMARC.",
3835
+ name: "dns-record",
3836
+ 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.',
3864
3837
  inputSchema: {
3865
3838
  type: "object",
3866
3839
  properties: {
3840
+ action: { type: "string", enum: ["create", "update", "delete"], description: "Which mutation to perform." },
3867
3841
  domain: { type: "string", description: "Domain name (e.g. example.com)" },
3868
3842
  type: { type: "string", description: "Record type: A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, or TLSA" },
3869
- name: { type: "string", description: "Record name" },
3870
- oldValue: { type: "string", description: "Current value of the record to update" },
3871
- newValue: { type: "string", description: "New value for the record" },
3872
- ttl: { type: "number", description: "New TTL in seconds (min 60)" },
3873
- dryRun: { type: "boolean", description: "Preview the change (returns proposed diff) without applying." }
3843
+ name: { type: "string", description: "Record name (e.g. @ or subdomain)." },
3844
+ value: { type: "string", description: "Record value. Required for create + delete. Use newValue for update." },
3845
+ oldValue: { type: "string", description: "Current value of the record (update only) \u2014 used to identify which record to replace." },
3846
+ newValue: { type: "string", description: "New value for the record (update only)." },
3847
+ ttl: { type: "number", description: "TTL in seconds (min 60). Default 3600 for create; defaults to existing TTL for update." },
3848
+ dryRun: { type: "boolean", description: "Preview the change (returns proposed diff) without applying. Recommended for MX/SPF/DKIM/DMARC." }
3874
3849
  },
3875
- required: ["domain", "type", "name", "oldValue", "newValue"]
3876
- }
3877
- },
3878
- {
3879
- name: "dns-delete",
3880
- description: "Delete a DNS record by type, name, and value. Fetches all records, removes the matching one, then replaces the full set.\n\nPass `dryRun: true` to preview the deletion without applying.",
3881
- inputSchema: {
3882
- type: "object",
3883
- properties: {
3884
- domain: { type: "string", description: "Domain name (e.g. example.com)" },
3885
- type: { type: "string", description: "Record type to delete" },
3886
- name: { type: "string", description: "Record name to delete" },
3887
- value: { type: "string", description: "Record value to delete (must match exactly)" },
3888
- dryRun: { type: "boolean", description: "Preview the deletion (returns proposed diff) without applying." }
3889
- },
3890
- required: ["domain", "type", "name", "value"]
3850
+ required: ["action", "domain", "type", "name"]
3891
3851
  }
3892
3852
  },
3893
3853
  // ----- Trigger.dev -----
@@ -3895,7 +3855,7 @@ var TOOLS = [
3895
3855
  // ----- Vercel -----
3896
3856
  ...VERCEL_TOOLS
3897
3857
  ];
3898
- var MCP_VERSION = "5.0.1";
3858
+ var MCP_VERSION = "6.0.0";
3899
3859
  async function handleListTools() {
3900
3860
  if (!authContext) return { tools: TOOLS };
3901
3861
  const accessible = TOOLS.filter((tool) => {
@@ -5211,112 +5171,80 @@ ${header}
5211
5171
  ${sep}
5212
5172
  ${lines.join("\n")}` }] };
5213
5173
  }
5214
- case "dns-create": {
5215
- const domain = String(a.domain);
5216
- const type = String(a.type).toUpperCase();
5217
- const dnsName = String(a.name);
5218
- const value = String(a.value);
5219
- const ttl = Number(a.ttl) || 3600;
5220
- const dryRun = a.dryRun === true;
5221
- if (!domain || !type || !dnsName || !value) {
5222
- throw new Error("domain, type, name, and value are required");
5223
- }
5224
- const current = await mijnhostFetch(
5225
- `/domains/${encodeURIComponent(domain)}/dns`
5226
- );
5227
- const newRecord = { type, name: dnsName, value, ttl };
5228
- const records = [...current.data.records, newRecord];
5229
- if (dryRun) {
5230
- const diff = formatDnsDiff(domain, current.data.records, records, {
5231
- verb: "create",
5232
- added: [newRecord]
5233
- });
5234
- return { content: [{ type: "text", text: diff }] };
5235
- }
5236
- await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
5237
- method: "PUT",
5238
- body: JSON.stringify({ records })
5239
- });
5240
- return { content: [{ type: "text", text: `DNS record created: ${type} ${dnsName} \u2192 ${value} (TTL: ${ttl})` }] };
5241
- }
5242
- case "dns-update": {
5243
- const domain = String(a.domain);
5244
- const type = String(a.type).toUpperCase();
5245
- const dnsName = String(a.name);
5246
- const oldValue = String(a.oldValue);
5247
- const newValue = String(a.newValue);
5248
- const ttl = Number(a.ttl) || void 0;
5249
- const dryRun = a.dryRun === true;
5250
- if (!domain || !type || !dnsName || !oldValue || !newValue) {
5251
- throw new Error("domain, type, name, oldValue, and newValue are required");
5252
- }
5253
- const current = await mijnhostFetch(
5254
- `/domains/${encodeURIComponent(domain)}/dns`
5255
- );
5256
- const idx = current.data.records.findIndex(
5257
- (r) => r.type === type && r.name === dnsName && r.value === oldValue
5258
- );
5259
- if (idx === -1) {
5260
- throw new Error(`No matching DNS record found: ${type} ${dnsName} = ${oldValue}`);
5261
- }
5262
- const updated = [...current.data.records];
5263
- const existingTtl = updated[idx].ttl;
5264
- const before = updated[idx];
5265
- const after = {
5266
- type,
5267
- name: dnsName,
5268
- value: newValue,
5269
- ttl: ttl ?? existingTtl
5270
- };
5271
- updated[idx] = after;
5272
- if (dryRun) {
5273
- const diff = formatDnsDiff(domain, current.data.records, updated, {
5274
- verb: "update",
5275
- removed: [before],
5276
- added: [after]
5277
- });
5278
- return { content: [{ type: "text", text: diff }] };
5174
+ case "dns-record": {
5175
+ const action = String(a.action);
5176
+ if (action !== "create" && action !== "update" && action !== "delete") {
5177
+ throw new Error("action must be one of: create, update, delete");
5279
5178
  }
5280
- await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
5281
- method: "PUT",
5282
- body: JSON.stringify({ records: updated })
5283
- });
5284
- return { content: [{ type: "text", text: `DNS record updated: ${type} ${dnsName} \u2192 ${newValue}${ttl ? ` (TTL: ${ttl})` : ""}` }] };
5285
- }
5286
- case "dns-delete": {
5287
5179
  const domain = String(a.domain);
5288
5180
  const type = String(a.type).toUpperCase();
5289
5181
  const dnsName = String(a.name);
5290
- const value = String(a.value);
5291
5182
  const dryRun = a.dryRun === true;
5292
- if (!domain || !type || !dnsName || !value) {
5293
- throw new Error("domain, type, name, and value are required");
5183
+ if (!domain || !type || !dnsName) {
5184
+ throw new Error("domain, type, and name are required");
5294
5185
  }
5295
5186
  const current = await mijnhostFetch(
5296
5187
  `/domains/${encodeURIComponent(domain)}/dns`
5297
5188
  );
5298
- const before = current.data.records.length;
5299
- const removed = current.data.records.filter(
5300
- (r) => r.type === type && r.name === dnsName && r.value === value
5301
- );
5302
- const remaining = current.data.records.filter(
5303
- (r) => !(r.type === type && r.name === dnsName && r.value === value)
5304
- );
5305
- if (remaining.length === before) {
5306
- throw new Error(`No matching DNS record found: ${type} ${dnsName} = ${value}`);
5189
+ let nextRecords;
5190
+ let diffArgs;
5191
+ let okMessage;
5192
+ if (action === "create") {
5193
+ const value = typeof a.value === "string" ? a.value : "";
5194
+ if (!value) throw new Error('action="create" requires value');
5195
+ const ttl = Number(a.ttl) || 3600;
5196
+ const newRecord = { type, name: dnsName, value, ttl };
5197
+ nextRecords = [...current.data.records, newRecord];
5198
+ diffArgs = { verb: "create", added: [newRecord] };
5199
+ okMessage = `DNS record created: ${type} ${dnsName} \u2192 ${value} (TTL: ${ttl})`;
5200
+ } else if (action === "update") {
5201
+ const oldValue = typeof a.oldValue === "string" ? a.oldValue : "";
5202
+ const newValue = typeof a.newValue === "string" ? a.newValue : "";
5203
+ if (!oldValue || !newValue) throw new Error('action="update" requires oldValue and newValue');
5204
+ const ttlArg = a.ttl !== void 0 ? Number(a.ttl) : void 0;
5205
+ const idx = current.data.records.findIndex(
5206
+ (r) => r.type === type && r.name === dnsName && r.value === oldValue
5207
+ );
5208
+ if (idx === -1) {
5209
+ throw new Error(`No matching DNS record found: ${type} ${dnsName} = ${oldValue}`);
5210
+ }
5211
+ const updated = [...current.data.records];
5212
+ const before = updated[idx];
5213
+ const after = {
5214
+ type,
5215
+ name: dnsName,
5216
+ value: newValue,
5217
+ ttl: ttlArg ?? before.ttl
5218
+ };
5219
+ updated[idx] = after;
5220
+ nextRecords = updated;
5221
+ diffArgs = { verb: "update", removed: [before], added: [after] };
5222
+ okMessage = `DNS record updated: ${type} ${dnsName} \u2192 ${newValue}${ttlArg ? ` (TTL: ${ttlArg})` : ""}`;
5223
+ } else {
5224
+ const value = typeof a.value === "string" ? a.value : "";
5225
+ if (!value) throw new Error('action="delete" requires value');
5226
+ const removed = current.data.records.filter(
5227
+ (r) => r.type === type && r.name === dnsName && r.value === value
5228
+ );
5229
+ const remaining = current.data.records.filter(
5230
+ (r) => !(r.type === type && r.name === dnsName && r.value === value)
5231
+ );
5232
+ if (removed.length === 0) {
5233
+ throw new Error(`No matching DNS record found: ${type} ${dnsName} = ${value}`);
5234
+ }
5235
+ nextRecords = remaining;
5236
+ diffArgs = { verb: "delete", removed };
5237
+ okMessage = `DNS record deleted: ${type} ${dnsName} = ${value} (${remaining.length} records remaining)`;
5307
5238
  }
5308
5239
  if (dryRun) {
5309
- const diff = formatDnsDiff(domain, current.data.records, remaining, {
5310
- verb: "delete",
5311
- removed
5312
- });
5240
+ const diff = formatDnsDiff(domain, current.data.records, nextRecords, diffArgs);
5313
5241
  return { content: [{ type: "text", text: diff }] };
5314
5242
  }
5315
5243
  await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
5316
5244
  method: "PUT",
5317
- body: JSON.stringify({ records: remaining })
5245
+ body: JSON.stringify({ records: nextRecords })
5318
5246
  });
5319
- return { content: [{ type: "text", text: `DNS record deleted: ${type} ${dnsName} = ${value} (${remaining.length} records remaining)` }] };
5247
+ return { content: [{ type: "text", text: okMessage }] };
5320
5248
  }
5321
5249
  default:
5322
5250
  if (TRIGGER_TOOL_NAMES.has(name)) {