@mgsoftwarebv/mg-dashboard-mcp 4.0.0 → 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 +602 -279
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -296,57 +296,40 @@ var TRIGGER_TOOLS = [
|
|
|
296
296
|
},
|
|
297
297
|
{
|
|
298
298
|
name: "trigger-runs",
|
|
299
|
-
description: "List recent task runs for a Trigger.dev project. Returns run ID, task name, status, duration, and timestamps.",
|
|
299
|
+
description: "List recent task runs for a Trigger.dev project. Returns run ID, task name, status, duration, and timestamps.\n\nCommon shortcuts: `failedOnly: true` returns only FAILED/CRASHED/SYSTEM_FAILURE/INTERRUPTED/TIMED_OUT. `sinceMinutes: 60` filters to runs created in the last hour (combines with status/taskIdentifier).",
|
|
300
300
|
inputSchema: {
|
|
301
301
|
type: "object",
|
|
302
302
|
properties: {
|
|
303
303
|
project: { type: "string", description: 'Project slug from trigger-list (e.g. "mg-dashboard-bHfS")' },
|
|
304
304
|
status: {
|
|
305
305
|
type: "string",
|
|
306
|
-
description: "Comma-separated status filter: QUEUED,EXECUTING,COMPLETED,FAILED,CRASHED,CANCELED,SYSTEM_FAILURE"
|
|
306
|
+
description: "Comma-separated status filter: QUEUED,EXECUTING,COMPLETED,FAILED,CRASHED,CANCELED,SYSTEM_FAILURE. Ignored when failedOnly is true."
|
|
307
|
+
},
|
|
308
|
+
failedOnly: {
|
|
309
|
+
type: "boolean",
|
|
310
|
+
description: "Shortcut: only return FAILED + CRASHED + SYSTEM_FAILURE + INTERRUPTED + TIMED_OUT runs. Overrides `status` when true."
|
|
307
311
|
},
|
|
308
312
|
taskIdentifier: { type: "string", description: 'Filter by task identifier (e.g. "hello-world")' },
|
|
313
|
+
sinceMinutes: { type: "number", description: "Only runs created in the last N minutes (uses Trigger filter[from]). Max 10080 (= 1 week)." },
|
|
309
314
|
limit: { type: "number", description: "Max runs to return (default 20, max 100)" }
|
|
310
315
|
},
|
|
311
316
|
required: ["project"]
|
|
312
317
|
}
|
|
313
318
|
},
|
|
314
319
|
{
|
|
315
|
-
name: "trigger-run
|
|
316
|
-
description:
|
|
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.',
|
|
317
322
|
inputSchema: {
|
|
318
323
|
type: "object",
|
|
319
324
|
properties: {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
{
|
|
327
|
-
name: "trigger-test-task",
|
|
328
|
-
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.",
|
|
329
|
-
inputSchema: {
|
|
330
|
-
type: "object",
|
|
331
|
-
properties: {
|
|
332
|
-
project: { type: "string", description: "Project slug from trigger-list" },
|
|
333
|
-
taskId: { type: "string", description: 'Task identifier (e.g. "hello-world", "execute-pipeline")' },
|
|
334
|
-
payload: { type: "string", description: "JSON payload string to pass to the task (optional)" },
|
|
335
|
-
waitSeconds: { type: "number", description: "Max seconds to wait for completion (default 60, max 300)" }
|
|
336
|
-
},
|
|
337
|
-
required: ["project", "taskId"]
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
{
|
|
341
|
-
name: "trigger-replay-run",
|
|
342
|
-
description: "Replay a previously failed or completed run with the same payload. Returns the new run ID.",
|
|
343
|
-
inputSchema: {
|
|
344
|
-
type: "object",
|
|
345
|
-
properties: {
|
|
346
|
-
project: { type: "string", description: "Project slug from trigger-list" },
|
|
347
|
-
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.' }
|
|
348
331
|
},
|
|
349
|
-
required: ["
|
|
332
|
+
required: ["action", "project"]
|
|
350
333
|
}
|
|
351
334
|
}
|
|
352
335
|
];
|
|
@@ -354,9 +337,7 @@ var TRIGGER_TOOL_NAMES = new Set(TRIGGER_TOOLS.map((t) => t.name));
|
|
|
354
337
|
var TRIGGER_TOOL_MODULE_MAP = {
|
|
355
338
|
"trigger-list": "ci_cd",
|
|
356
339
|
"trigger-runs": "ci_cd",
|
|
357
|
-
"trigger-run
|
|
358
|
-
"trigger-test-task": "ci_cd",
|
|
359
|
-
"trigger-replay-run": "ci_cd"
|
|
340
|
+
"trigger-run": "ci_cd"
|
|
360
341
|
};
|
|
361
342
|
async function discoverInstance(projectSlug, conn, proxy, sshExec2) {
|
|
362
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`;
|
|
@@ -489,8 +470,18 @@ ${lines.join("\n")}` }] };
|
|
|
489
470
|
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
490
471
|
const limit = Math.min(Math.max(Number(args2.limit) || 20, 1), 100);
|
|
491
472
|
const queryParts = [`page%5Bsize%5D=${limit}`];
|
|
492
|
-
|
|
493
|
-
|
|
473
|
+
const failedOnly = args2.failedOnly === true;
|
|
474
|
+
const statusArg = failedOnly ? "FAILED,CRASHED,SYSTEM_FAILURE,INTERRUPTED,TIMED_OUT" : args2.status ? String(args2.status) : "";
|
|
475
|
+
if (statusArg) queryParts.push(`filter%5Bstatus%5D=${encodeURIComponent(statusArg)}`);
|
|
476
|
+
if (args2.taskIdentifier) {
|
|
477
|
+
queryParts.push(`filter%5BtaskIdentifier%5D=${encodeURIComponent(String(args2.taskIdentifier))}`);
|
|
478
|
+
}
|
|
479
|
+
const sinceMinutes = Number(args2.sinceMinutes);
|
|
480
|
+
if (Number.isFinite(sinceMinutes) && sinceMinutes > 0) {
|
|
481
|
+
const capped = Math.min(sinceMinutes, 10080);
|
|
482
|
+
const fromMs = Date.now() - capped * 6e4;
|
|
483
|
+
queryParts.push(`filter%5Bfrom%5D=${fromMs}`);
|
|
484
|
+
}
|
|
494
485
|
const rawJson = await triggerApi(
|
|
495
486
|
conn,
|
|
496
487
|
proxy,
|
|
@@ -510,104 +501,69 @@ ${rawJson.substring(0, 500)}` }] };
|
|
|
510
501
|
return { content: [{ type: "text", text: formatRunsTable(runs) }] };
|
|
511
502
|
}
|
|
512
503
|
// -----------------------------------------------------------------
|
|
513
|
-
case "trigger-run
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const [rawJson, logs] = await Promise.all([
|
|
518
|
-
triggerApi(conn, proxy, sshExec2, instance, "GET", `/api/v3/runs/${encodeURIComponent(runId)}`),
|
|
519
|
-
fetchRunLogs(runId, conn, proxy, sshExec2)
|
|
520
|
-
]);
|
|
521
|
-
let run;
|
|
522
|
-
try {
|
|
523
|
-
run = JSON.parse(rawJson);
|
|
524
|
-
} catch {
|
|
525
|
-
return { content: [{ type: "text", text: `Invalid API response:
|
|
526
|
-
${rawJson.substring(0, 500)}` }] };
|
|
527
|
-
}
|
|
528
|
-
let text = formatRunDetail(run);
|
|
529
|
-
if (logs) {
|
|
530
|
-
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" }] };
|
|
531
508
|
}
|
|
532
|
-
return { content: [{ type: "text", text }] };
|
|
533
|
-
}
|
|
534
|
-
// -----------------------------------------------------------------
|
|
535
|
-
case "trigger-test-task": {
|
|
536
509
|
const project = String(args2.project);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (args2.payload) {
|
|
543
|
-
try {
|
|
544
|
-
JSON.parse(String(args2.payload));
|
|
545
|
-
payload = String(args2.payload);
|
|
546
|
-
} catch {
|
|
547
|
-
throw new Error(`Invalid JSON payload: ${String(args2.payload).substring(0, 200)}`);
|
|
548
|
-
}
|
|
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);
|
|
549
515
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
conn
|
|
556
|
-
proxy,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (!runId) {
|
|
572
|
-
return { content: [{ type: "text", text: `Trigger API did not return a run ID:
|
|
573
|
-
${triggerJson.substring(0, 500)}` }] };
|
|
574
|
-
}
|
|
575
|
-
const pollInterval = 3e3;
|
|
576
|
-
const maxPolls = Math.ceil(waitSeconds * 1e3 / pollInterval);
|
|
577
|
-
for (let i = 0; i < maxPolls; i++) {
|
|
578
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
579
|
-
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(
|
|
580
537
|
conn,
|
|
581
538
|
proxy,
|
|
582
539
|
sshExec2,
|
|
583
|
-
|
|
584
|
-
"
|
|
585
|
-
`/api/
|
|
540
|
+
instance2,
|
|
541
|
+
"POST",
|
|
542
|
+
`/api/v1/tasks/${encodeURIComponent(taskId)}/trigger`,
|
|
543
|
+
triggerBody
|
|
586
544
|
);
|
|
587
|
-
let
|
|
545
|
+
let triggerResp;
|
|
588
546
|
try {
|
|
589
|
-
|
|
547
|
+
triggerResp = JSON.parse(triggerJson);
|
|
590
548
|
} catch {
|
|
591
|
-
|
|
549
|
+
return { content: [{ type: "text", text: `Failed to trigger task. API response:
|
|
550
|
+
${triggerJson.substring(0, 500)}` }] };
|
|
592
551
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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).` }] };
|
|
598
559
|
}
|
|
560
|
+
return await waitForCompletion(conn, proxy, sshExec2, instance2, runId2, waitSeconds2);
|
|
599
561
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
// -----------------------------------------------------------------
|
|
608
|
-
case "trigger-replay-run": {
|
|
609
|
-
const project = String(args2.project);
|
|
610
|
-
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;
|
|
611
567
|
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
612
568
|
const rawJson = await triggerApi(
|
|
613
569
|
conn,
|
|
@@ -624,19 +580,69 @@ ${triggerJson.substring(0, 500)}` }] };
|
|
|
624
580
|
return { content: [{ type: "text", text: `Replay response:
|
|
625
581
|
${rawJson.substring(0, 500)}` }] };
|
|
626
582
|
}
|
|
627
|
-
|
|
628
|
-
content: [{
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}]
|
|
633
|
-
}
|
|
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);
|
|
634
591
|
}
|
|
635
592
|
// -----------------------------------------------------------------
|
|
636
593
|
default:
|
|
637
594
|
return { content: [{ type: "text", text: `Unknown trigger tool: ${name}` }] };
|
|
638
595
|
}
|
|
639
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
|
+
}
|
|
640
646
|
|
|
641
647
|
// src/vercel-tools.ts
|
|
642
648
|
var VERCEL_API = "https://api.vercel.com";
|
|
@@ -669,6 +675,41 @@ var VERCEL_TOOLS = [
|
|
|
669
675
|
required: ["project"]
|
|
670
676
|
}
|
|
671
677
|
},
|
|
678
|
+
{
|
|
679
|
+
name: "vercel-domains",
|
|
680
|
+
description: 'Manage domains attached to a Vercel project. Use `action` to pick the operation:\n- "list" (default): list all domains attached to the project, including verification status and any redirect.\n- "add": attach a new domain to the project. Returns DNS records to set if the domain is not yet verified.\n- "verify": re-check verification status and surface required CNAME / A / TXT records if misconfigured.\n- "remove": detach a domain from the project.\nPick the project via vercel-projects first.',
|
|
681
|
+
inputSchema: {
|
|
682
|
+
type: "object",
|
|
683
|
+
properties: {
|
|
684
|
+
action: {
|
|
685
|
+
type: "string",
|
|
686
|
+
enum: ["list", "add", "remove", "verify"],
|
|
687
|
+
description: "Which operation to perform (default: list)."
|
|
688
|
+
},
|
|
689
|
+
project: {
|
|
690
|
+
type: "string",
|
|
691
|
+
description: "Vercel project ID or name (from vercel-projects)."
|
|
692
|
+
},
|
|
693
|
+
domain: {
|
|
694
|
+
type: "string",
|
|
695
|
+
description: 'Domain name (required for action="add", "remove", or "verify").'
|
|
696
|
+
},
|
|
697
|
+
gitBranch: {
|
|
698
|
+
type: "string",
|
|
699
|
+
description: 'Optional git branch this domain should deploy from (action="add" only).'
|
|
700
|
+
},
|
|
701
|
+
redirect: {
|
|
702
|
+
type: "string",
|
|
703
|
+
description: 'Optional redirect target domain (action="add" only).'
|
|
704
|
+
},
|
|
705
|
+
redirectStatusCode: {
|
|
706
|
+
type: "number",
|
|
707
|
+
description: 'Optional redirect status code, e.g. 301 / 302 / 307 / 308 (action="add" only).'
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
required: ["project"]
|
|
711
|
+
}
|
|
712
|
+
},
|
|
672
713
|
{
|
|
673
714
|
name: "vercel-logs",
|
|
674
715
|
description: 'Unified log inspector for Vercel. Use `kind` to pick the source:\n- "build" (default): build / deployment console events (stdout, stderr, command, exit). Requires deploymentId.\n- "runtime": runtime / function logs after a successful build. Requires project + deploymentId.\n- "webhooks": our own vercel_webhook_logs table (Telegram / push delivery history). No deployment needed.\nPick deployments via vercel-deployments first.',
|
|
@@ -698,7 +739,7 @@ var VERCEL_TOOLS = [
|
|
|
698
739
|
},
|
|
699
740
|
sinceMinutes: {
|
|
700
741
|
type: "number",
|
|
701
|
-
description:
|
|
742
|
+
description: `Time window in minutes (kind="runtime" only, max 7 days). When omitted, the tool auto-detects the deployment's created timestamp and queries from there with a 5-minute buffer \u2014 so you don't miss logs by picking a too-small window.`
|
|
702
743
|
},
|
|
703
744
|
limit: {
|
|
704
745
|
type: "number",
|
|
@@ -712,11 +753,14 @@ var VERCEL_TOOL_NAMES = new Set(VERCEL_TOOLS.map((t) => t.name));
|
|
|
712
753
|
var VERCEL_TOOL_MODULE_MAP = {
|
|
713
754
|
"vercel-projects": "ci_cd",
|
|
714
755
|
"vercel-deployments": "ci_cd",
|
|
715
|
-
"vercel-logs": "ci_cd"
|
|
756
|
+
"vercel-logs": "ci_cd",
|
|
757
|
+
"vercel-domains": "ci_cd"
|
|
716
758
|
};
|
|
717
|
-
async function vercelFetch(token, path) {
|
|
759
|
+
async function vercelFetch(token, path, init) {
|
|
718
760
|
const res = await fetch(`${VERCEL_API}${path}`, {
|
|
719
|
-
|
|
761
|
+
method: init?.method ?? "GET",
|
|
762
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
763
|
+
body: init?.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
720
764
|
});
|
|
721
765
|
const body = await res.text();
|
|
722
766
|
let parsed = null;
|
|
@@ -800,6 +844,14 @@ async function getDeploymentBuildEvents(token, deploymentId, limit) {
|
|
|
800
844
|
}
|
|
801
845
|
return { events, error: null };
|
|
802
846
|
}
|
|
847
|
+
async function getDeploymentCreatedMs(token, deploymentId) {
|
|
848
|
+
const res = await vercelFetch(
|
|
849
|
+
token,
|
|
850
|
+
`/v13/deployments/${encodeURIComponent(deploymentId)}`
|
|
851
|
+
);
|
|
852
|
+
if (res.error || !res.data) return null;
|
|
853
|
+
return res.data.createdAt ?? res.data.created ?? null;
|
|
854
|
+
}
|
|
803
855
|
async function getRuntimeLogs(token, projectIdOrName, deploymentId, limit, sinceMs) {
|
|
804
856
|
const params = new URLSearchParams({
|
|
805
857
|
limit: String(Math.min(Math.max(limit, 1), 1e3))
|
|
@@ -840,6 +892,47 @@ async function getRuntimeLogs(token, projectIdOrName, deploymentId, limit, since
|
|
|
840
892
|
}
|
|
841
893
|
return { logs, error: null };
|
|
842
894
|
}
|
|
895
|
+
async function listProjectDomains(token, projectId) {
|
|
896
|
+
const res = await vercelFetch(
|
|
897
|
+
token,
|
|
898
|
+
`/v9/projects/${encodeURIComponent(projectId)}/domains?limit=100`
|
|
899
|
+
);
|
|
900
|
+
if (res.error) return { domains: [], error: res.error };
|
|
901
|
+
return { domains: res.data?.domains ?? [], error: null };
|
|
902
|
+
}
|
|
903
|
+
async function addProjectDomain(token, projectId, body) {
|
|
904
|
+
const res = await vercelFetch(
|
|
905
|
+
token,
|
|
906
|
+
`/v10/projects/${encodeURIComponent(projectId)}/domains`,
|
|
907
|
+
{ method: "POST", body }
|
|
908
|
+
);
|
|
909
|
+
if (res.error) return { domain: null, error: res.error };
|
|
910
|
+
return { domain: res.data, error: null };
|
|
911
|
+
}
|
|
912
|
+
async function removeProjectDomain(token, projectId, domain) {
|
|
913
|
+
const res = await vercelFetch(
|
|
914
|
+
token,
|
|
915
|
+
`/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}`,
|
|
916
|
+
{ method: "DELETE" }
|
|
917
|
+
);
|
|
918
|
+
return { error: res.error };
|
|
919
|
+
}
|
|
920
|
+
async function getProjectDomain(token, projectId, domain) {
|
|
921
|
+
const res = await vercelFetch(
|
|
922
|
+
token,
|
|
923
|
+
`/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}`
|
|
924
|
+
);
|
|
925
|
+
if (res.error) return { domain: null, error: res.error };
|
|
926
|
+
return { domain: res.data, error: null };
|
|
927
|
+
}
|
|
928
|
+
async function getDomainConfig(token, domain) {
|
|
929
|
+
const res = await vercelFetch(
|
|
930
|
+
token,
|
|
931
|
+
`/v6/domains/${encodeURIComponent(domain)}/config`
|
|
932
|
+
);
|
|
933
|
+
if (res.error) return { config: null, error: res.error };
|
|
934
|
+
return { config: res.data, error: null };
|
|
935
|
+
}
|
|
843
936
|
async function getVercelToken(deps) {
|
|
844
937
|
const { data, error } = await deps.supabase.from("app_setting").select("vercel_token_encrypted").maybeSingle();
|
|
845
938
|
if (error) throw new Error(`Could not read app_setting: ${error.message}`);
|
|
@@ -935,6 +1028,42 @@ function formatWebhookHistory(rows) {
|
|
|
935
1028
|
${"-".repeat(header.length)}
|
|
936
1029
|
${lines.join("\n")}`;
|
|
937
1030
|
}
|
|
1031
|
+
function formatDomainsTable(domains) {
|
|
1032
|
+
if (domains.length === 0) return "No domains attached to this project";
|
|
1033
|
+
const lines = domains.map((d) => {
|
|
1034
|
+
const verified = d.verified ? "yes" : "no";
|
|
1035
|
+
const branch = d.gitBranch ?? "";
|
|
1036
|
+
const redirect = d.redirect ? `${d.redirect}${d.redirectStatusCode ? ` (${d.redirectStatusCode})` : ""}` : "";
|
|
1037
|
+
return `${d.name.padEnd(45)} ${verified.padEnd(9)} ${branch.padEnd(20)} ${redirect.padEnd(35)} ${formatTimestamp(d.createdAt)}`;
|
|
1038
|
+
});
|
|
1039
|
+
const header = `${"DOMAIN".padEnd(45)} ${"VERIFIED".padEnd(9)} ${"GIT BRANCH".padEnd(20)} ${"REDIRECT".padEnd(35)} CREATED`;
|
|
1040
|
+
return `${header}
|
|
1041
|
+
${"-".repeat(header.length)}
|
|
1042
|
+
${lines.join("\n")}`;
|
|
1043
|
+
}
|
|
1044
|
+
function formatDomainStatus(domain, config) {
|
|
1045
|
+
const lines = [];
|
|
1046
|
+
lines.push(`Domain: ${domain.name}`);
|
|
1047
|
+
lines.push(`Verified: ${domain.verified ? "yes" : "no"}`);
|
|
1048
|
+
if (config) {
|
|
1049
|
+
lines.push(`Misconfigured: ${config.misconfigured ? "yes" : "no"}`);
|
|
1050
|
+
if (config.configuredBy) lines.push(`Configured by: ${config.configuredBy}`);
|
|
1051
|
+
}
|
|
1052
|
+
if (domain.gitBranch) lines.push(`Git branch: ${domain.gitBranch}`);
|
|
1053
|
+
if (domain.redirect) {
|
|
1054
|
+
lines.push(
|
|
1055
|
+
`Redirect: ${domain.redirect}${domain.redirectStatusCode ? ` (${domain.redirectStatusCode})` : ""}`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
if (domain.verification && domain.verification.length > 0) {
|
|
1059
|
+
lines.push("");
|
|
1060
|
+
lines.push("Required DNS records to verify ownership:");
|
|
1061
|
+
for (const v of domain.verification) {
|
|
1062
|
+
lines.push(` ${v.type.padEnd(6)} ${v.domain.padEnd(45)} ${v.value}${v.reason ? ` // ${v.reason}` : ""}`);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return lines.join("\n");
|
|
1066
|
+
}
|
|
938
1067
|
async function handleVercelTool(name, args2, deps) {
|
|
939
1068
|
switch (name) {
|
|
940
1069
|
case "vercel-projects": {
|
|
@@ -985,9 +1114,33 @@ async function handleVercelTool(name, args2, deps) {
|
|
|
985
1114
|
const projectInput = String(args2.project);
|
|
986
1115
|
const deploymentId = String(args2.deploymentId);
|
|
987
1116
|
const limit = Math.min(Math.max(Number(args2.limit) || 200, 1), 1e3);
|
|
988
|
-
const
|
|
989
|
-
const
|
|
990
|
-
const projectId = await
|
|
1117
|
+
const sinceMinutesRaw = Number(args2.sinceMinutes);
|
|
1118
|
+
const sinceExplicit = Number.isFinite(sinceMinutesRaw) && sinceMinutesRaw > 0;
|
|
1119
|
+
const [projectId, deploymentCreatedMs] = await Promise.all([
|
|
1120
|
+
resolveProjectId(token, projectInput),
|
|
1121
|
+
sinceExplicit ? Promise.resolve(null) : getDeploymentCreatedMs(token, deploymentId)
|
|
1122
|
+
]);
|
|
1123
|
+
const maxWindowMin = 7 * 24 * 60;
|
|
1124
|
+
let sinceMs;
|
|
1125
|
+
let windowNote = "";
|
|
1126
|
+
if (sinceExplicit) {
|
|
1127
|
+
const capped = Math.min(sinceMinutesRaw, maxWindowMin);
|
|
1128
|
+
sinceMs = Date.now() - capped * 6e4;
|
|
1129
|
+
windowNote = `window: last ${capped} min (caller-specified)`;
|
|
1130
|
+
} else if (deploymentCreatedMs) {
|
|
1131
|
+
const bufferMs = 5 * 6e4;
|
|
1132
|
+
sinceMs = deploymentCreatedMs - bufferMs;
|
|
1133
|
+
const ageMin = Math.max(1, Math.round((Date.now() - sinceMs) / 6e4));
|
|
1134
|
+
if (ageMin > maxWindowMin) {
|
|
1135
|
+
sinceMs = Date.now() - maxWindowMin * 6e4;
|
|
1136
|
+
windowNote = `window: capped to ${maxWindowMin} min (deployment is older than 7 days)`;
|
|
1137
|
+
} else {
|
|
1138
|
+
windowNote = `window: auto ${ageMin} min from deployment createdAt - 5 min buffer`;
|
|
1139
|
+
}
|
|
1140
|
+
} else {
|
|
1141
|
+
sinceMs = Date.now() - 60 * 6e4;
|
|
1142
|
+
windowNote = "window: last 60 min (deployment metadata unavailable, used fallback)";
|
|
1143
|
+
}
|
|
991
1144
|
const { logs, error } = await getRuntimeLogs(
|
|
992
1145
|
token,
|
|
993
1146
|
projectId,
|
|
@@ -999,7 +1152,10 @@ async function handleVercelTool(name, args2, deps) {
|
|
|
999
1152
|
const hint = error.includes("404") || error.includes("400") ? '\n\nThis endpoint requires both project ID and deployment ID and may not be available on every Vercel plan. Use kind="webhooks" or the supabase MCP (vercel_deployment_log table) for archived runtime logs.' : "";
|
|
1000
1153
|
return { content: [{ type: "text", text: `Error: ${error}${hint}` }] };
|
|
1001
1154
|
}
|
|
1002
|
-
|
|
1155
|
+
const text = `${formatRuntimeLogs(logs)}
|
|
1156
|
+
|
|
1157
|
+
[${windowNote}]`;
|
|
1158
|
+
return { content: [{ type: "text", text }] };
|
|
1003
1159
|
}
|
|
1004
1160
|
if (kind === "webhooks") {
|
|
1005
1161
|
const limit = Math.min(Math.max(Number(args2.limit) || 25, 1), 200);
|
|
@@ -1020,6 +1176,67 @@ async function handleVercelTool(name, args2, deps) {
|
|
|
1020
1176
|
]
|
|
1021
1177
|
};
|
|
1022
1178
|
}
|
|
1179
|
+
case "vercel-domains": {
|
|
1180
|
+
const action = (args2.action ? String(args2.action) : "list").toLowerCase();
|
|
1181
|
+
if (!args2.project) {
|
|
1182
|
+
return { content: [{ type: "text", text: 'Error: vercel-domains requires "project".' }] };
|
|
1183
|
+
}
|
|
1184
|
+
const token = await getVercelToken(deps);
|
|
1185
|
+
const projectInput = String(args2.project);
|
|
1186
|
+
const projectId = await resolveProjectId(token, projectInput);
|
|
1187
|
+
if (action === "list") {
|
|
1188
|
+
const { domains, error } = await listProjectDomains(token, projectId);
|
|
1189
|
+
if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
|
|
1190
|
+
return { content: [{ type: "text", text: formatDomainsTable(domains) }] };
|
|
1191
|
+
}
|
|
1192
|
+
if (action === "add") {
|
|
1193
|
+
if (!args2.domain) {
|
|
1194
|
+
return { content: [{ type: "text", text: 'Error: action="add" requires "domain".' }] };
|
|
1195
|
+
}
|
|
1196
|
+
const body = { name: String(args2.domain) };
|
|
1197
|
+
if (args2.gitBranch) body.gitBranch = String(args2.gitBranch);
|
|
1198
|
+
if (args2.redirect) body.redirect = String(args2.redirect);
|
|
1199
|
+
if (args2.redirectStatusCode) body.redirectStatusCode = Number(args2.redirectStatusCode);
|
|
1200
|
+
const { domain, error } = await addProjectDomain(token, projectId, body);
|
|
1201
|
+
if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
|
|
1202
|
+
if (!domain) {
|
|
1203
|
+
return { content: [{ type: "text", text: `Domain ${body.name} added (no detail returned).` }] };
|
|
1204
|
+
}
|
|
1205
|
+
const { config } = await getDomainConfig(token, domain.name);
|
|
1206
|
+
const status = formatDomainStatus(domain, config);
|
|
1207
|
+
const headline = domain.verified ? `Domain ${domain.name} added and verified.` : `Domain ${domain.name} added. DNS verification still pending.`;
|
|
1208
|
+
return { content: [{ type: "text", text: `${headline}
|
|
1209
|
+
|
|
1210
|
+
${status}` }] };
|
|
1211
|
+
}
|
|
1212
|
+
if (action === "verify") {
|
|
1213
|
+
if (!args2.domain) {
|
|
1214
|
+
return { content: [{ type: "text", text: 'Error: action="verify" requires "domain".' }] };
|
|
1215
|
+
}
|
|
1216
|
+
const domainName = String(args2.domain);
|
|
1217
|
+
const { domain, error } = await getProjectDomain(token, projectId, domainName);
|
|
1218
|
+
if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
|
|
1219
|
+
if (!domain) {
|
|
1220
|
+
return { content: [{ type: "text", text: `Domain ${domainName} not found on this project.` }] };
|
|
1221
|
+
}
|
|
1222
|
+
const { config } = await getDomainConfig(token, domainName);
|
|
1223
|
+
return { content: [{ type: "text", text: formatDomainStatus(domain, config) }] };
|
|
1224
|
+
}
|
|
1225
|
+
if (action === "remove") {
|
|
1226
|
+
if (!args2.domain) {
|
|
1227
|
+
return { content: [{ type: "text", text: 'Error: action="remove" requires "domain".' }] };
|
|
1228
|
+
}
|
|
1229
|
+
const domainName = String(args2.domain);
|
|
1230
|
+
const { error } = await removeProjectDomain(token, projectId, domainName);
|
|
1231
|
+
if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
|
|
1232
|
+
return { content: [{ type: "text", text: `Domain ${domainName} removed from project.` }] };
|
|
1233
|
+
}
|
|
1234
|
+
return {
|
|
1235
|
+
content: [
|
|
1236
|
+
{ type: "text", text: `Error: unknown action "${action}". Use list, add, verify, or remove.` }
|
|
1237
|
+
]
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1023
1240
|
default:
|
|
1024
1241
|
return { content: [{ type: "text", text: `Unknown vercel tool: ${name}` }] };
|
|
1025
1242
|
}
|
|
@@ -1186,7 +1403,6 @@ var TOOL_MODULE_MAP = {
|
|
|
1186
1403
|
"wait-for": "ssh_servers",
|
|
1187
1404
|
"db-discover": "ssh_servers",
|
|
1188
1405
|
"db-tables": "ssh_servers",
|
|
1189
|
-
"db-describe": "ssh_servers",
|
|
1190
1406
|
"db-query": "ssh_servers",
|
|
1191
1407
|
"cache-purge": "ssh_servers",
|
|
1192
1408
|
"env-list": "ci_cd",
|
|
@@ -1194,9 +1410,7 @@ var TOOL_MODULE_MAP = {
|
|
|
1194
1410
|
"env-store": "ci_cd",
|
|
1195
1411
|
"domain-list": "domains",
|
|
1196
1412
|
"dns-list": "domains",
|
|
1197
|
-
"dns-
|
|
1198
|
-
"dns-update": "domains",
|
|
1199
|
-
"dns-delete": "domains",
|
|
1413
|
+
"dns-record": "domains",
|
|
1200
1414
|
...TRIGGER_TOOL_MODULE_MAP,
|
|
1201
1415
|
...VERCEL_TOOL_MODULE_MAP
|
|
1202
1416
|
};
|
|
@@ -3146,6 +3360,42 @@ var BLOCKED_SQL_PATTERNS = [
|
|
|
3146
3360
|
/\bALTER\s+TABLE\s+\w+\s+DROP\b/i,
|
|
3147
3361
|
/\bDELETE\s+FROM\s+\w+\s*$/i
|
|
3148
3362
|
];
|
|
3363
|
+
function buildDescribeSql(target, engine) {
|
|
3364
|
+
if (target === "*" || target === "") {
|
|
3365
|
+
if (engine === "postgres") {
|
|
3366
|
+
return "SELECT schemaname, tablename FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema') ORDER BY schemaname, tablename";
|
|
3367
|
+
}
|
|
3368
|
+
if (engine === "mssql") {
|
|
3369
|
+
return "SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' ORDER BY TABLE_SCHEMA, TABLE_NAME";
|
|
3370
|
+
}
|
|
3371
|
+
return "SHOW TABLES";
|
|
3372
|
+
}
|
|
3373
|
+
const safe = target.replace(/[^a-zA-Z0-9_]/g, "");
|
|
3374
|
+
if (!safe) throw new Error(`Invalid describe target: ${target}`);
|
|
3375
|
+
if (engine === "postgres") {
|
|
3376
|
+
return `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name='${safe}' ORDER BY ordinal_position;
|
|
3377
|
+
SELECT indexname, indexdef FROM pg_indexes WHERE tablename='${safe}'`;
|
|
3378
|
+
}
|
|
3379
|
+
if (engine === "mssql") {
|
|
3380
|
+
return `EXEC sp_help '${safe}'`;
|
|
3381
|
+
}
|
|
3382
|
+
return `DESCRIBE \`${safe}\`; SHOW INDEX FROM \`${safe}\``;
|
|
3383
|
+
}
|
|
3384
|
+
function formatDbQueryFooter(output, appliedLimit, maxRows, explainMode) {
|
|
3385
|
+
if (explainMode) {
|
|
3386
|
+
return "\n\n[explain] Plan returned, no rows executed.";
|
|
3387
|
+
}
|
|
3388
|
+
if (!appliedLimit) return "";
|
|
3389
|
+
const rows = output.split("\n").filter((l) => l.trim() && !/^\s*[-+|]/.test(l)).length;
|
|
3390
|
+
if (rows > maxRows) {
|
|
3391
|
+
return `
|
|
3392
|
+
|
|
3393
|
+
[truncated] auto-LIMIT ${maxRows} hit \u2014 more rows available. Refine WHERE / ORDER BY or raise maxRows (max 10000).`;
|
|
3394
|
+
}
|
|
3395
|
+
return `
|
|
3396
|
+
|
|
3397
|
+
[ok] returned ${rows} row(s), under auto-LIMIT ${maxRows}.`;
|
|
3398
|
+
}
|
|
3149
3399
|
function assertSafeSql(query) {
|
|
3150
3400
|
const trimmed = query.trim();
|
|
3151
3401
|
for (const pattern of BLOCKED_SQL_PATTERNS) {
|
|
@@ -3217,6 +3467,36 @@ function requireMijnhostApiKey() {
|
|
|
3217
3467
|
}
|
|
3218
3468
|
return mijnhostApiKey;
|
|
3219
3469
|
}
|
|
3470
|
+
function formatDnsDiff(domain, before, after, change) {
|
|
3471
|
+
const fmt = (r) => `${r.type.padEnd(6)} ${r.name.padEnd(30)} ttl=${String(r.ttl).padEnd(5)} ${r.value}`;
|
|
3472
|
+
const lines = [];
|
|
3473
|
+
lines.push(`[dryRun] ${change.verb.toUpperCase()} on ${domain} \u2014 no changes applied.`);
|
|
3474
|
+
lines.push(`Records before: ${before.length} \u2192 after: ${after.length}`);
|
|
3475
|
+
if (change.removed?.length) {
|
|
3476
|
+
lines.push("", "REMOVED:");
|
|
3477
|
+
for (const r of change.removed) lines.push(` - ${fmt(r)}`);
|
|
3478
|
+
}
|
|
3479
|
+
if (change.added?.length) {
|
|
3480
|
+
lines.push("", "ADDED:");
|
|
3481
|
+
for (const r of change.added) lines.push(` + ${fmt(r)}`);
|
|
3482
|
+
}
|
|
3483
|
+
const sensitive = [
|
|
3484
|
+
...change.added ?? [],
|
|
3485
|
+
...change.removed ?? []
|
|
3486
|
+
].filter((r) => {
|
|
3487
|
+
if (r.type === "MX") return true;
|
|
3488
|
+
if (r.type === "TXT") {
|
|
3489
|
+
const v = r.value.toLowerCase();
|
|
3490
|
+
return v.includes("v=spf1") || v.includes("v=dmarc1") || v.includes("_domainkey") || r.name.toLowerCase().includes("_domainkey");
|
|
3491
|
+
}
|
|
3492
|
+
return false;
|
|
3493
|
+
});
|
|
3494
|
+
if (sensitive.length > 0) {
|
|
3495
|
+
lines.push("", "! WARNING: touches mail-auth records (MX / SPF / DMARC / DKIM). Double-check before re-running without dryRun.");
|
|
3496
|
+
}
|
|
3497
|
+
lines.push("", "Re-run without `dryRun: true` to apply.");
|
|
3498
|
+
return lines.join("\n");
|
|
3499
|
+
}
|
|
3220
3500
|
async function mijnhostFetch(path, options = {}) {
|
|
3221
3501
|
const key = requireMijnhostApiKey();
|
|
3222
3502
|
const res = await fetch(`${MIJNHOST_BASE_URL}${path}`, {
|
|
@@ -3457,35 +3737,25 @@ var TOOLS = [
|
|
|
3457
3737
|
required: ["serverId", "sitePath"]
|
|
3458
3738
|
}
|
|
3459
3739
|
},
|
|
3460
|
-
{
|
|
3461
|
-
name: "db-describe",
|
|
3462
|
-
description: "Show the structure of a database table (columns, types, keys, defaults). Credentials are auto-discovered from site config files.",
|
|
3463
|
-
inputSchema: {
|
|
3464
|
-
type: "object",
|
|
3465
|
-
properties: {
|
|
3466
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
3467
|
-
sitePath: { type: "string", description: "Site root path (e.g. /var/www/example.com)" },
|
|
3468
|
-
table: { type: "string", description: "Table name" }
|
|
3469
|
-
},
|
|
3470
|
-
required: ["serverId", "sitePath", "table"]
|
|
3471
|
-
}
|
|
3472
|
-
},
|
|
3473
3740
|
{
|
|
3474
3741
|
name: "db-query",
|
|
3475
|
-
description: 'Execute a SQL query against any database. Two routing modes:\n- MySQL via `/var/www` autodiscover (default): pass `sitePath` (and `engine: "mysql"` implicitly). Credentials are read from wp-config.php, parameters.php, .env. Same behaviour as before.\n- Direct via Docker container: pass `containerName` (the container running the DB server). Engine defaults to `postgres`; pass `engine: "mysql"` or `"mssql"` to override. The query is piped via stdin so quotes / `$` / `;` are never re-interpreted.\n\nPostgres example: `{ serverId, containerName: "supabase-db", engine: "postgres", dbName: "main", dbUser: "postgres", query: "SELECT 1" }`. Defaults: dbUser=postgres, dbName=postgres.\nMSSQL example: `{ serverId, containerName: "mssql-1", engine: "mssql", dbUser: "sa", dbPass: "...", query: "SELECT 1" }`.\n\nDestructive operations (DROP, TRUNCATE, etc.) are blocked.',
|
|
3742
|
+
description: 'Execute a SQL query against any database. Two routing modes:\n- MySQL via `/var/www` autodiscover (default): pass `sitePath` (and `engine: "mysql"` implicitly). Credentials are read from wp-config.php, parameters.php, .env. Same behaviour as before.\n- Direct via Docker container: pass `containerName` (the container running the DB server). Engine defaults to `postgres`; pass `engine: "mysql"` or `"mssql"` to override. The query is piped via stdin so quotes / `$` / `;` are never re-interpreted.\n\nPostgres example: `{ serverId, containerName: "supabase-db", engine: "postgres", dbName: "main", dbUser: "postgres", query: "SELECT 1" }`. Defaults: dbUser=postgres, dbName=postgres.\nMSSQL example: `{ serverId, containerName: "mssql-1", engine: "mssql", dbUser: "sa", dbPass: "...", query: "SELECT 1" }`.\n\nResult safety: `maxRows` (default 1000, max 10000) auto-injects a `LIMIT` for SELECT queries that don\'t have one, so unbounded scans never blow up the token budget. The footer reports whether the cap was applied. Pass `maxRows: 0` to disable.\n\nDiagnostics: `explain: true` prepends EXPLAIN (postgres / mysql) or runs `SET SHOWPLAN_TEXT ON` (mssql) so you can inspect the query plan without rewriting the query.\n\nSchema introspection: pass `describe: "tablename"` to get columns + indexes (replaces the old db-describe tool, works for mysql, postgres, and mssql). Pass `describe: "*"` to list all tables in the current database. When `describe` is set, `query` is ignored.\n\nDestructive operations (DROP, TRUNCATE, etc.) are blocked.',
|
|
3476
3743
|
inputSchema: {
|
|
3477
3744
|
type: "object",
|
|
3478
3745
|
properties: {
|
|
3479
3746
|
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
3480
|
-
query: { type: "string", description: "SQL query to execute" },
|
|
3747
|
+
query: { type: "string", description: "SQL query to execute (ignored when describe is set)" },
|
|
3748
|
+
describe: { type: "string", description: 'Schema introspection shortcut. Pass a table name for columns + indexes, or "*" to list all tables. Works for mysql / postgres / mssql.' },
|
|
3481
3749
|
sitePath: { type: "string", description: "Site root path (e.g. /var/www/example.com). Used only with engine=mysql autodiscover." },
|
|
3482
3750
|
containerName: { type: "string", description: 'Docker container running the DB server (e.g. "supabase-db", "trigger-postgres"). Activates direct-query mode.' },
|
|
3483
3751
|
engine: { type: "string", enum: ["mysql", "postgres", "mssql"], description: 'DB engine. Defaults to "mysql" with sitePath, "postgres" with containerName.' },
|
|
3484
3752
|
dbName: { type: "string", description: 'Database name (containerName mode). Defaults: postgres \u2192 "postgres", mysql \u2192 server default, mssql \u2192 server default.' },
|
|
3485
3753
|
dbUser: { type: "string", description: 'Database user (containerName mode). Defaults: postgres \u2192 "postgres", mysql \u2192 "root", mssql \u2192 "sa".' },
|
|
3486
|
-
dbPass: { type: "string", description: "Database password (containerName mode). Required for mssql; optional for mysql; postgres uses trust auth by default." }
|
|
3754
|
+
dbPass: { type: "string", description: "Database password (containerName mode). Required for mssql; optional for mysql; postgres uses trust auth by default." },
|
|
3755
|
+
maxRows: { type: "number", description: "Auto-inject LIMIT for unbounded SELECTs. Default 1000, max 10000, set 0 to disable." },
|
|
3756
|
+
explain: { type: "boolean", description: "Prepend EXPLAIN (postgres/mysql) or SHOWPLAN (mssql) instead of executing. Useful for slow-query diagnosis." }
|
|
3487
3757
|
},
|
|
3488
|
-
required: ["serverId"
|
|
3758
|
+
required: ["serverId"]
|
|
3489
3759
|
}
|
|
3490
3760
|
},
|
|
3491
3761
|
{
|
|
@@ -3540,63 +3810,44 @@ var TOOLS = [
|
|
|
3540
3810
|
// ----- Domains (mijn.host) -----
|
|
3541
3811
|
{
|
|
3542
3812
|
name: "domain-list",
|
|
3543
|
-
description: "List all domains from the mijn.host account. Returns domain name, status, renewal date, and tags. Requires MIJNHOST_API_KEY.",
|
|
3544
|
-
inputSchema: { type: "object", properties: {}, required: [] }
|
|
3545
|
-
},
|
|
3546
|
-
{
|
|
3547
|
-
name: "dns-list",
|
|
3548
|
-
description: "List all DNS records for a domain. Returns type (A, AAAA, CNAME, MX, TXT, etc.), name, value, and TTL for each record.",
|
|
3813
|
+
description: "List all domains from the mijn.host account. Returns domain name, status, renewal date (= expiration), and tags. Requires MIJNHOST_API_KEY.\n\nPass `details: true` to also fetch DNS zone summary per domain in parallel: NS records, MX target, and presence of SPF/DMARC. Useful as a single-call overview instead of N follow-up dns-list calls. Skipped for inactive/expired domains.",
|
|
3549
3814
|
inputSchema: {
|
|
3550
3815
|
type: "object",
|
|
3551
3816
|
properties: {
|
|
3552
|
-
|
|
3817
|
+
details: { type: "boolean", description: "Inline DNS zone summary (NS, MX, SPF/DMARC presence) per domain. Adds N parallel API calls \u2014 use sparingly on large accounts." },
|
|
3818
|
+
concurrency: { type: "number", description: "Max concurrent DNS lookups when details=true (default 8, max 20)." }
|
|
3553
3819
|
},
|
|
3554
|
-
required: [
|
|
3820
|
+
required: []
|
|
3555
3821
|
}
|
|
3556
3822
|
},
|
|
3557
3823
|
{
|
|
3558
|
-
name: "dns-
|
|
3559
|
-
description: "
|
|
3824
|
+
name: "dns-list",
|
|
3825
|
+
description: "List all DNS records for a domain. Returns type (A, AAAA, CNAME, MX, TXT, etc.), name, value, and TTL for each record.",
|
|
3560
3826
|
inputSchema: {
|
|
3561
3827
|
type: "object",
|
|
3562
3828
|
properties: {
|
|
3563
|
-
domain: { type: "string", description: "Domain name (e.g. example.com)" }
|
|
3564
|
-
type: { type: "string", description: "Record type: A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, or TLSA" },
|
|
3565
|
-
name: { type: "string", description: "Record name (e.g. @ or subdomain)" },
|
|
3566
|
-
value: { type: "string", description: "Record value (e.g. IP address, hostname)" },
|
|
3567
|
-
ttl: { type: "number", description: "TTL in seconds (min 60, default 3600)" }
|
|
3829
|
+
domain: { type: "string", description: "Domain name (e.g. example.com)" }
|
|
3568
3830
|
},
|
|
3569
|
-
required: ["domain"
|
|
3831
|
+
required: ["domain"]
|
|
3570
3832
|
}
|
|
3571
3833
|
},
|
|
3572
3834
|
{
|
|
3573
|
-
name: "dns-
|
|
3574
|
-
description:
|
|
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.',
|
|
3575
3837
|
inputSchema: {
|
|
3576
3838
|
type: "object",
|
|
3577
3839
|
properties: {
|
|
3840
|
+
action: { type: "string", enum: ["create", "update", "delete"], description: "Which mutation to perform." },
|
|
3578
3841
|
domain: { type: "string", description: "Domain name (e.g. example.com)" },
|
|
3579
3842
|
type: { type: "string", description: "Record type: A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, or TLSA" },
|
|
3580
|
-
name: { type: "string", description: "Record name" },
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
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." }
|
|
3584
3849
|
},
|
|
3585
|
-
required: ["
|
|
3586
|
-
}
|
|
3587
|
-
},
|
|
3588
|
-
{
|
|
3589
|
-
name: "dns-delete",
|
|
3590
|
-
description: "Delete a DNS record by type, name, and value. Fetches all records, removes the matching one, then replaces the full set.",
|
|
3591
|
-
inputSchema: {
|
|
3592
|
-
type: "object",
|
|
3593
|
-
properties: {
|
|
3594
|
-
domain: { type: "string", description: "Domain name (e.g. example.com)" },
|
|
3595
|
-
type: { type: "string", description: "Record type to delete" },
|
|
3596
|
-
name: { type: "string", description: "Record name to delete" },
|
|
3597
|
-
value: { type: "string", description: "Record value to delete (must match exactly)" }
|
|
3598
|
-
},
|
|
3599
|
-
required: ["domain", "type", "name", "value"]
|
|
3850
|
+
required: ["action", "domain", "type", "name"]
|
|
3600
3851
|
}
|
|
3601
3852
|
},
|
|
3602
3853
|
// ----- Trigger.dev -----
|
|
@@ -3604,7 +3855,7 @@ var TOOLS = [
|
|
|
3604
3855
|
// ----- Vercel -----
|
|
3605
3856
|
...VERCEL_TOOLS
|
|
3606
3857
|
];
|
|
3607
|
-
var MCP_VERSION = "
|
|
3858
|
+
var MCP_VERSION = "6.0.0";
|
|
3608
3859
|
async function handleListTools() {
|
|
3609
3860
|
if (!authContext) return { tools: TOOLS };
|
|
3610
3861
|
const accessible = TOOLS.filter((tool) => {
|
|
@@ -4568,29 +4819,46 @@ ${trail.join("\n")}` }] };
|
|
|
4568
4819
|
const output = await execSiteMysql(conn, String(a.sitePath), sql, proxy);
|
|
4569
4820
|
return { content: [{ type: "text", text: output || "No tables found" }] };
|
|
4570
4821
|
}
|
|
4571
|
-
case "db-describe": {
|
|
4572
|
-
const table = String(a.table).replace(/[^a-zA-Z0-9_]/g, "");
|
|
4573
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
4574
|
-
const output = await execSiteMysql(
|
|
4575
|
-
conn,
|
|
4576
|
-
String(a.sitePath),
|
|
4577
|
-
`DESCRIBE \`${table}\`; SHOW INDEX FROM \`${table}\``,
|
|
4578
|
-
proxy
|
|
4579
|
-
);
|
|
4580
|
-
return { content: [{ type: "text", text: output }] };
|
|
4581
|
-
}
|
|
4582
4822
|
case "db-query": {
|
|
4583
|
-
const query = String(a.query).trim();
|
|
4584
|
-
if (!query) return { content: [{ type: "text", text: "Error: query is required" }] };
|
|
4585
|
-
assertSafeSql(query);
|
|
4586
4823
|
const containerName = typeof a.containerName === "string" && a.containerName ? a.containerName.replace(/[^a-zA-Z0-9._-]/g, "") : "";
|
|
4587
4824
|
const explicitEngine = a.engine === "mysql" || a.engine === "postgres" || a.engine === "mssql" ? a.engine : null;
|
|
4588
4825
|
const engine = explicitEngine || (containerName ? "postgres" : "mysql");
|
|
4826
|
+
const describeArg = typeof a.describe === "string" ? a.describe.trim() : "";
|
|
4827
|
+
const explainMode = a.explain === true;
|
|
4828
|
+
const maxRowsRaw = a.maxRows === void 0 ? 1e3 : Number(a.maxRows);
|
|
4829
|
+
const maxRows = Math.min(Math.max(Number.isFinite(maxRowsRaw) ? maxRowsRaw : 0, 0), 1e4);
|
|
4830
|
+
let rawQuery;
|
|
4831
|
+
if (describeArg) {
|
|
4832
|
+
rawQuery = buildDescribeSql(describeArg, engine);
|
|
4833
|
+
} else {
|
|
4834
|
+
rawQuery = String(a.query ?? "").trim();
|
|
4835
|
+
if (!rawQuery) return { content: [{ type: "text", text: "Error: query (or describe) is required" }] };
|
|
4836
|
+
}
|
|
4837
|
+
assertSafeSql(rawQuery);
|
|
4838
|
+
let query = rawQuery.replace(/;\s*$/, "");
|
|
4839
|
+
let appliedLimit = false;
|
|
4840
|
+
if (explainMode && !describeArg) {
|
|
4841
|
+
if (engine === "mssql") {
|
|
4842
|
+
query = `SET SHOWPLAN_TEXT ON;
|
|
4843
|
+
${query}`;
|
|
4844
|
+
} else {
|
|
4845
|
+
query = `EXPLAIN ${query}`;
|
|
4846
|
+
}
|
|
4847
|
+
} else if (maxRows > 0 && !describeArg) {
|
|
4848
|
+
const isSelect = /^\s*(with\b[\s\S]+?\bselect\b|select\b)/i.test(query);
|
|
4849
|
+
const alreadyLimited = /\blimit\s+\d+\b/i.test(query) || /\btop\s*\(?\s*\d+/i.test(query);
|
|
4850
|
+
if (isSelect && !alreadyLimited && engine !== "mssql") {
|
|
4851
|
+
query = `${query}
|
|
4852
|
+
LIMIT ${maxRows + 1}`;
|
|
4853
|
+
appliedLimit = true;
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4589
4856
|
if (!containerName && engine === "mysql") {
|
|
4590
4857
|
if (!a.sitePath) return { content: [{ type: "text", text: "Error: sitePath is required for mysql autodiscover (or pass containerName + engine for direct DB queries)" }] };
|
|
4591
4858
|
const { conn: conn2, proxy: proxy2 } = await getServerConnection(String(a.serverId));
|
|
4592
4859
|
const output2 = await execSiteMysql(conn2, String(a.sitePath), query, proxy2);
|
|
4593
|
-
|
|
4860
|
+
const footer2 = formatDbQueryFooter(output2, appliedLimit, maxRows, explainMode);
|
|
4861
|
+
return { content: [{ type: "text", text: (output2 || "Query executed successfully (no output)") + footer2 }] };
|
|
4594
4862
|
}
|
|
4595
4863
|
if (!containerName) {
|
|
4596
4864
|
return { content: [{ type: "text", text: `Error: engine=${engine} requires containerName (the docker container running the DB server, e.g. "supabase-db" or "trigger-postgres")` }] };
|
|
@@ -4629,7 +4897,8 @@ GO
|
|
|
4629
4897
|
if (result.exitCode !== 0 && !result.stdout) {
|
|
4630
4898
|
return { content: [{ type: "text", text: `Error (exit ${result.exitCode}, ${engine}): ${output}` }] };
|
|
4631
4899
|
}
|
|
4632
|
-
|
|
4900
|
+
const footer = formatDbQueryFooter(output, appliedLimit, maxRows, explainMode);
|
|
4901
|
+
return { content: [{ type: "text", text: output + footer }] };
|
|
4633
4902
|
}
|
|
4634
4903
|
// ----- Env Config -----
|
|
4635
4904
|
case "env-list": {
|
|
@@ -4818,13 +5087,68 @@ echo -e "$R"
|
|
|
4818
5087
|
if (!domains.length) {
|
|
4819
5088
|
return { content: [{ type: "text", text: "No domains found" }] };
|
|
4820
5089
|
}
|
|
5090
|
+
const details = a.details === true;
|
|
5091
|
+
if (!details) {
|
|
5092
|
+
const lines2 = domains.map((d) => {
|
|
5093
|
+
const tags = d.tags?.length ? ` [${d.tags.join(", ")}]` : "";
|
|
5094
|
+
return `${d.domain} status=${d.status} renewal=${d.renewal_date}${tags}`;
|
|
5095
|
+
});
|
|
5096
|
+
return { content: [{ type: "text", text: `${domains.length} domain(s):
|
|
5097
|
+
|
|
5098
|
+
${lines2.join("\n")}` }] };
|
|
5099
|
+
}
|
|
5100
|
+
const concurrency = Math.min(Math.max(Number(a.concurrency) || 8, 1), 20);
|
|
5101
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
5102
|
+
const skipStatuses = /* @__PURE__ */ new Set(["expired", "redemptionperiod", "pendingdelete", "inactive"]);
|
|
5103
|
+
const activeDomains = domains.filter((d) => !skipStatuses.has((d.status || "").toLowerCase()));
|
|
5104
|
+
async function fetchSummary(domain) {
|
|
5105
|
+
try {
|
|
5106
|
+
const r = await mijnhostFetch(
|
|
5107
|
+
`/domains/${encodeURIComponent(domain)}/dns`
|
|
5108
|
+
);
|
|
5109
|
+
const recs = r.data.records || [];
|
|
5110
|
+
const ns = recs.filter((x) => x.type === "NS").map((x) => x.value).sort();
|
|
5111
|
+
const mx = recs.filter((x) => x.type === "MX").map((x) => x.value).sort();
|
|
5112
|
+
const hasSpf = recs.some((x) => x.type === "TXT" && x.value.toLowerCase().includes("v=spf1"));
|
|
5113
|
+
const hasDmarc = recs.some(
|
|
5114
|
+
(x) => x.type === "TXT" && (x.name.toLowerCase().startsWith("_dmarc") || x.value.toLowerCase().includes("v=dmarc1"))
|
|
5115
|
+
);
|
|
5116
|
+
return { ns, mx, hasSpf, hasDmarc };
|
|
5117
|
+
} catch (err) {
|
|
5118
|
+
return { ns: [], mx: [], hasSpf: false, hasDmarc: false, error: err instanceof Error ? err.message : String(err) };
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
const queue = [...activeDomains];
|
|
5122
|
+
async function worker() {
|
|
5123
|
+
while (queue.length > 0) {
|
|
5124
|
+
const d = queue.shift();
|
|
5125
|
+
if (!d) return;
|
|
5126
|
+
summaries.set(d.domain, await fetchSummary(d.domain));
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, worker));
|
|
4821
5130
|
const lines = domains.map((d) => {
|
|
4822
5131
|
const tags = d.tags?.length ? ` [${d.tags.join(", ")}]` : "";
|
|
4823
|
-
|
|
5132
|
+
const head = `${d.domain} status=${d.status} expires=${d.renewal_date}${tags}`;
|
|
5133
|
+
const s = summaries.get(d.domain);
|
|
5134
|
+
if (!s) return head;
|
|
5135
|
+
if (s.error) return `${head}
|
|
5136
|
+
dns: error: ${s.error}`;
|
|
5137
|
+
const ns = s.ns.length ? s.ns.join(", ") : "(none)";
|
|
5138
|
+
const mx = s.mx.length ? s.mx.join(", ") : "(none)";
|
|
5139
|
+
const mail = `mx=${mx} spf=${s.hasSpf ? "yes" : "NO"} dmarc=${s.hasDmarc ? "yes" : "NO"}`;
|
|
5140
|
+
return `${head}
|
|
5141
|
+
ns: ${ns}
|
|
5142
|
+
${mail}`;
|
|
4824
5143
|
});
|
|
4825
|
-
return {
|
|
5144
|
+
return {
|
|
5145
|
+
content: [{
|
|
5146
|
+
type: "text",
|
|
5147
|
+
text: `${domains.length} domain(s) (${activeDomains.length} with DNS lookup):
|
|
4826
5148
|
|
|
4827
|
-
${lines.join("\n")}`
|
|
5149
|
+
${lines.join("\n")}`
|
|
5150
|
+
}]
|
|
5151
|
+
};
|
|
4828
5152
|
}
|
|
4829
5153
|
case "dns-list": {
|
|
4830
5154
|
const domain = String(a.domain);
|
|
@@ -4847,81 +5171,80 @@ ${header}
|
|
|
4847
5171
|
${sep}
|
|
4848
5172
|
${lines.join("\n")}` }] };
|
|
4849
5173
|
}
|
|
4850
|
-
case "dns-
|
|
4851
|
-
const
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
const value = String(a.value);
|
|
4855
|
-
const ttl = Number(a.ttl) || 3600;
|
|
4856
|
-
if (!domain || !type || !dnsName || !value) {
|
|
4857
|
-
throw new Error("domain, type, name, and value are required");
|
|
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");
|
|
4858
5178
|
}
|
|
4859
|
-
const current = await mijnhostFetch(
|
|
4860
|
-
`/domains/${encodeURIComponent(domain)}/dns`
|
|
4861
|
-
);
|
|
4862
|
-
const records = [...current.data.records, { type, name: dnsName, value, ttl }];
|
|
4863
|
-
await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
|
|
4864
|
-
method: "PUT",
|
|
4865
|
-
body: JSON.stringify({ records })
|
|
4866
|
-
});
|
|
4867
|
-
return { content: [{ type: "text", text: `DNS record created: ${type} ${dnsName} \u2192 ${value} (TTL: ${ttl})` }] };
|
|
4868
|
-
}
|
|
4869
|
-
case "dns-update": {
|
|
4870
5179
|
const domain = String(a.domain);
|
|
4871
5180
|
const type = String(a.type).toUpperCase();
|
|
4872
5181
|
const dnsName = String(a.name);
|
|
4873
|
-
const
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
if (!domain || !type || !dnsName || !oldValue || !newValue) {
|
|
4877
|
-
throw new Error("domain, type, name, oldValue, and newValue are required");
|
|
5182
|
+
const dryRun = a.dryRun === true;
|
|
5183
|
+
if (!domain || !type || !dnsName) {
|
|
5184
|
+
throw new Error("domain, type, and name are required");
|
|
4878
5185
|
}
|
|
4879
5186
|
const current = await mijnhostFetch(
|
|
4880
5187
|
`/domains/${encodeURIComponent(domain)}/dns`
|
|
4881
5188
|
);
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
if (
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
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)`;
|
|
4909
5238
|
}
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
const before = current.data.records.length;
|
|
4914
|
-
const remaining = current.data.records.filter(
|
|
4915
|
-
(r) => !(r.type === type && r.name === dnsName && r.value === value)
|
|
4916
|
-
);
|
|
4917
|
-
if (remaining.length === before) {
|
|
4918
|
-
throw new Error(`No matching DNS record found: ${type} ${dnsName} = ${value}`);
|
|
5239
|
+
if (dryRun) {
|
|
5240
|
+
const diff = formatDnsDiff(domain, current.data.records, nextRecords, diffArgs);
|
|
5241
|
+
return { content: [{ type: "text", text: diff }] };
|
|
4919
5242
|
}
|
|
4920
5243
|
await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
|
|
4921
5244
|
method: "PUT",
|
|
4922
|
-
body: JSON.stringify({ records:
|
|
5245
|
+
body: JSON.stringify({ records: nextRecords })
|
|
4923
5246
|
});
|
|
4924
|
-
return { content: [{ type: "text", text:
|
|
5247
|
+
return { content: [{ type: "text", text: okMessage }] };
|
|
4925
5248
|
}
|
|
4926
5249
|
default:
|
|
4927
5250
|
if (TRIGGER_TOOL_NAMES.has(name)) {
|