@mgsoftwarebv/mg-dashboard-mcp 5.0.1 → 6.0.1
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 +228 -279
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -317,41 +317,19 @@ var TRIGGER_TOOLS = [
|
|
|
317
317
|
}
|
|
318
318
|
},
|
|
319
319
|
{
|
|
320
|
-
name: "trigger-run
|
|
321
|
-
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.',
|
|
322
322
|
inputSchema: {
|
|
323
323
|
type: "object",
|
|
324
324
|
properties: {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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: ["
|
|
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
|
|
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
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
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)}` }] };
|
|
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" }] };
|
|
542
508
|
}
|
|
543
|
-
let text = formatRunDetail(run);
|
|
544
|
-
if (logs) {
|
|
545
|
-
text += "\n\n--- Logs ---\n" + logs;
|
|
546
|
-
}
|
|
547
|
-
return { content: [{ type: "text", text }] };
|
|
548
|
-
}
|
|
549
|
-
// -----------------------------------------------------------------
|
|
550
|
-
case "trigger-test-task": {
|
|
551
509
|
const project = String(args2.project);
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
conn
|
|
571
|
-
proxy,
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
599
|
-
"
|
|
600
|
-
`/api/
|
|
540
|
+
instance2,
|
|
541
|
+
"POST",
|
|
542
|
+
`/api/v1/tasks/${encodeURIComponent(taskId)}/trigger`,
|
|
543
|
+
triggerBody
|
|
601
544
|
);
|
|
602
|
-
let
|
|
545
|
+
let triggerResp;
|
|
603
546
|
try {
|
|
604
|
-
|
|
547
|
+
triggerResp = JSON.parse(triggerJson);
|
|
605
548
|
} catch {
|
|
606
|
-
|
|
549
|
+
return { content: [{ type: "text", text: `Failed to trigger task. API response:
|
|
550
|
+
${triggerJson.substring(0, 500)}` }] };
|
|
607
551
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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)}` }] };
|
|
613
556
|
}
|
|
557
|
+
if (waitSeconds2 === 0) {
|
|
558
|
+
return { content: [{ type: "text", text: `Run triggered. ID: ${runId2} (fire-and-forget; use action="detail" to check).` }] };
|
|
559
|
+
}
|
|
560
|
+
return await waitForCompletion(conn, proxy, sshExec2, instance2, runId2, waitSeconds2);
|
|
614
561
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
643
|
-
content: [{
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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";
|
|
@@ -1130,6 +1121,7 @@ async function handleVercelTool(name, args2, deps) {
|
|
|
1130
1121
|
sinceExplicit ? Promise.resolve(null) : getDeploymentCreatedMs(token, deploymentId)
|
|
1131
1122
|
]);
|
|
1132
1123
|
const maxWindowMin = 7 * 24 * 60;
|
|
1124
|
+
const autoCapMin = 30;
|
|
1133
1125
|
let sinceMs;
|
|
1134
1126
|
let windowNote = "";
|
|
1135
1127
|
if (sinceExplicit) {
|
|
@@ -1138,17 +1130,18 @@ async function handleVercelTool(name, args2, deps) {
|
|
|
1138
1130
|
windowNote = `window: last ${capped} min (caller-specified)`;
|
|
1139
1131
|
} else if (deploymentCreatedMs) {
|
|
1140
1132
|
const bufferMs = 5 * 6e4;
|
|
1141
|
-
|
|
1142
|
-
const ageMin = Math.max(1, Math.round((Date.now() -
|
|
1143
|
-
if (ageMin
|
|
1144
|
-
sinceMs =
|
|
1145
|
-
windowNote = `window: capped to ${maxWindowMin} min (deployment is older than 7 days)`;
|
|
1146
|
-
} else {
|
|
1133
|
+
const sinceDeploymentMs = deploymentCreatedMs - bufferMs;
|
|
1134
|
+
const ageMin = Math.max(1, Math.round((Date.now() - sinceDeploymentMs) / 6e4));
|
|
1135
|
+
if (ageMin <= autoCapMin) {
|
|
1136
|
+
sinceMs = sinceDeploymentMs;
|
|
1147
1137
|
windowNote = `window: auto ${ageMin} min from deployment createdAt - 5 min buffer`;
|
|
1138
|
+
} else {
|
|
1139
|
+
sinceMs = Date.now() - autoCapMin * 6e4;
|
|
1140
|
+
windowNote = `window: capped to last ${autoCapMin} min (deployment is ${ageMin} min old \u2014 pass sinceMinutes to widen up to ${maxWindowMin})`;
|
|
1148
1141
|
}
|
|
1149
1142
|
} else {
|
|
1150
|
-
sinceMs = Date.now() -
|
|
1151
|
-
windowNote =
|
|
1143
|
+
sinceMs = Date.now() - autoCapMin * 6e4;
|
|
1144
|
+
windowNote = `window: last ${autoCapMin} min (deployment metadata unavailable, used fallback)`;
|
|
1152
1145
|
}
|
|
1153
1146
|
const { logs, error } = await getRuntimeLogs(
|
|
1154
1147
|
token,
|
|
@@ -1161,10 +1154,15 @@ async function handleVercelTool(name, args2, deps) {
|
|
|
1161
1154
|
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.' : "";
|
|
1162
1155
|
return { content: [{ type: "text", text: `Error: ${error}${hint}` }] };
|
|
1163
1156
|
}
|
|
1164
|
-
const
|
|
1157
|
+
const body = formatRuntimeLogs(logs);
|
|
1158
|
+
const hitDurationLimit = /Exceeded query duration limit/i.test(body);
|
|
1159
|
+
const footer = hitDurationLimit ? `
|
|
1160
|
+
|
|
1161
|
+
[${windowNote}]
|
|
1162
|
+
[hint] Vercel hit its 5-min query budget for this window. Try a smaller sinceMinutes (e.g. 5-10), lower limit, or use kind="webhooks" / the supabase MCP vercel_deployment_log table for archived logs.` : `
|
|
1165
1163
|
|
|
1166
1164
|
[${windowNote}]`;
|
|
1167
|
-
return { content: [{ type: "text", text }] };
|
|
1165
|
+
return { content: [{ type: "text", text: body + footer }] };
|
|
1168
1166
|
}
|
|
1169
1167
|
if (kind === "webhooks") {
|
|
1170
1168
|
const limit = Math.min(Math.max(Number(args2.limit) || 25, 1), 200);
|
|
@@ -1419,9 +1417,7 @@ var TOOL_MODULE_MAP = {
|
|
|
1419
1417
|
"env-store": "ci_cd",
|
|
1420
1418
|
"domain-list": "domains",
|
|
1421
1419
|
"dns-list": "domains",
|
|
1422
|
-
"dns-
|
|
1423
|
-
"dns-update": "domains",
|
|
1424
|
-
"dns-delete": "domains",
|
|
1420
|
+
"dns-record": "domains",
|
|
1425
1421
|
...TRIGGER_TOOL_MODULE_MAP,
|
|
1426
1422
|
...VERCEL_TOOL_MODULE_MAP
|
|
1427
1423
|
};
|
|
@@ -3397,7 +3393,10 @@ function formatDbQueryFooter(output, appliedLimit, maxRows, explainMode) {
|
|
|
3397
3393
|
return "\n\n[explain] Plan returned, no rows executed.";
|
|
3398
3394
|
}
|
|
3399
3395
|
if (!appliedLimit) return "";
|
|
3400
|
-
const rows = output
|
|
3396
|
+
const rows = parseRowCountFromOutput(output);
|
|
3397
|
+
if (rows == null) return `
|
|
3398
|
+
|
|
3399
|
+
[ok] auto-LIMIT ${maxRows} applied (row count not detected).`;
|
|
3401
3400
|
if (rows > maxRows) {
|
|
3402
3401
|
return `
|
|
3403
3402
|
|
|
@@ -3407,6 +3406,21 @@ function formatDbQueryFooter(output, appliedLimit, maxRows, explainMode) {
|
|
|
3407
3406
|
|
|
3408
3407
|
[ok] returned ${rows} row(s), under auto-LIMIT ${maxRows}.`;
|
|
3409
3408
|
}
|
|
3409
|
+
function parseRowCountFromOutput(output) {
|
|
3410
|
+
const lines = output.split("\n");
|
|
3411
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3412
|
+
const l = lines[i]?.trim();
|
|
3413
|
+
if (!l) continue;
|
|
3414
|
+
const m1 = /^\(\s*(\d+)\s+rows?\s*\)$/i.exec(l);
|
|
3415
|
+
if (m1?.[1]) return Number(m1[1]);
|
|
3416
|
+
const m2 = /^(\d+)\s+rows?\s+in\s+set\b/i.exec(l);
|
|
3417
|
+
if (m2?.[1]) return Number(m2[1]);
|
|
3418
|
+
const m3 = /^\(\s*(\d+)\s+rows?\s+affected\s*\)$/i.exec(l);
|
|
3419
|
+
if (m3?.[1]) return Number(m3[1]);
|
|
3420
|
+
if (/[a-zA-Z0-9]/.test(l) && i < lines.length - 3) return null;
|
|
3421
|
+
}
|
|
3422
|
+
return null;
|
|
3423
|
+
}
|
|
3410
3424
|
function assertSafeSql(query) {
|
|
3411
3425
|
const trimmed = query.trim();
|
|
3412
3426
|
for (const pattern of BLOCKED_SQL_PATTERNS) {
|
|
@@ -3821,7 +3835,7 @@ var TOOLS = [
|
|
|
3821
3835
|
// ----- Domains (mijn.host) -----
|
|
3822
3836
|
{
|
|
3823
3837
|
name: "domain-list",
|
|
3824
|
-
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:
|
|
3838
|
+
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: MX target(s) and presence of SPF/DMARC TXT records. Useful as a single-call overview instead of N follow-up dns-list calls. Skipped for inactive/expired domains.",
|
|
3825
3839
|
inputSchema: {
|
|
3826
3840
|
type: "object",
|
|
3827
3841
|
properties: {
|
|
@@ -3843,51 +3857,22 @@ var TOOLS = [
|
|
|
3843
3857
|
}
|
|
3844
3858
|
},
|
|
3845
3859
|
{
|
|
3846
|
-
name: "dns-
|
|
3847
|
-
description:
|
|
3860
|
+
name: "dns-record",
|
|
3861
|
+
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.',
|
|
3848
3862
|
inputSchema: {
|
|
3849
3863
|
type: "object",
|
|
3850
3864
|
properties: {
|
|
3865
|
+
action: { type: "string", enum: ["create", "update", "delete"], description: "Which mutation to perform." },
|
|
3851
3866
|
domain: { type: "string", description: "Domain name (e.g. example.com)" },
|
|
3852
3867
|
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
|
|
3855
|
-
|
|
3856
|
-
|
|
3868
|
+
name: { type: "string", description: "Record name (e.g. @ or subdomain)." },
|
|
3869
|
+
value: { type: "string", description: "Record value. Required for create + delete. Use newValue for update." },
|
|
3870
|
+
oldValue: { type: "string", description: "Current value of the record (update only) \u2014 used to identify which record to replace." },
|
|
3871
|
+
newValue: { type: "string", description: "New value for the record (update only)." },
|
|
3872
|
+
ttl: { type: "number", description: "TTL in seconds (min 60). Default 3600 for create; defaults to existing TTL for update." },
|
|
3873
|
+
dryRun: { type: "boolean", description: "Preview the change (returns proposed diff) without applying. Recommended for MX/SPF/DKIM/DMARC." }
|
|
3857
3874
|
},
|
|
3858
|
-
required: ["
|
|
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.",
|
|
3864
|
-
inputSchema: {
|
|
3865
|
-
type: "object",
|
|
3866
|
-
properties: {
|
|
3867
|
-
domain: { type: "string", description: "Domain name (e.g. example.com)" },
|
|
3868
|
-
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." }
|
|
3874
|
-
},
|
|
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"]
|
|
3875
|
+
required: ["action", "domain", "type", "name"]
|
|
3891
3876
|
}
|
|
3892
3877
|
},
|
|
3893
3878
|
// ----- Trigger.dev -----
|
|
@@ -3895,7 +3880,7 @@ var TOOLS = [
|
|
|
3895
3880
|
// ----- Vercel -----
|
|
3896
3881
|
...VERCEL_TOOLS
|
|
3897
3882
|
];
|
|
3898
|
-
var MCP_VERSION = "
|
|
3883
|
+
var MCP_VERSION = "6.0.1";
|
|
3899
3884
|
async function handleListTools() {
|
|
3900
3885
|
if (!authContext) return { tools: TOOLS };
|
|
3901
3886
|
const accessible = TOOLS.filter((tool) => {
|
|
@@ -5147,15 +5132,14 @@ ${lines2.join("\n")}` }] };
|
|
|
5147
5132
|
`/domains/${encodeURIComponent(domain)}/dns`
|
|
5148
5133
|
);
|
|
5149
5134
|
const recs = r.data.records || [];
|
|
5150
|
-
const ns = recs.filter((x) => x.type === "NS").map((x) => x.value).sort();
|
|
5151
5135
|
const mx = recs.filter((x) => x.type === "MX").map((x) => x.value).sort();
|
|
5152
5136
|
const hasSpf = recs.some((x) => x.type === "TXT" && x.value.toLowerCase().includes("v=spf1"));
|
|
5153
5137
|
const hasDmarc = recs.some(
|
|
5154
5138
|
(x) => x.type === "TXT" && (x.name.toLowerCase().startsWith("_dmarc") || x.value.toLowerCase().includes("v=dmarc1"))
|
|
5155
5139
|
);
|
|
5156
|
-
return {
|
|
5140
|
+
return { mx, hasSpf, hasDmarc };
|
|
5157
5141
|
} catch (err) {
|
|
5158
|
-
return {
|
|
5142
|
+
return { mx: [], hasSpf: false, hasDmarc: false, error: err instanceof Error ? err.message : String(err) };
|
|
5159
5143
|
}
|
|
5160
5144
|
}
|
|
5161
5145
|
const queue = [...activeDomains];
|
|
@@ -5174,12 +5158,9 @@ ${lines2.join("\n")}` }] };
|
|
|
5174
5158
|
if (!s) return head;
|
|
5175
5159
|
if (s.error) return `${head}
|
|
5176
5160
|
dns: error: ${s.error}`;
|
|
5177
|
-
const ns = s.ns.length ? s.ns.join(", ") : "(none)";
|
|
5178
5161
|
const mx = s.mx.length ? s.mx.join(", ") : "(none)";
|
|
5179
|
-
const mail = `mx=${mx} spf=${s.hasSpf ? "yes" : "NO"} dmarc=${s.hasDmarc ? "yes" : "NO"}`;
|
|
5180
5162
|
return `${head}
|
|
5181
|
-
|
|
5182
|
-
${mail}`;
|
|
5163
|
+
mx=${mx} spf=${s.hasSpf ? "yes" : "NO"} dmarc=${s.hasDmarc ? "yes" : "NO"}`;
|
|
5183
5164
|
});
|
|
5184
5165
|
return {
|
|
5185
5166
|
content: [{
|
|
@@ -5211,112 +5192,80 @@ ${header}
|
|
|
5211
5192
|
${sep}
|
|
5212
5193
|
${lines.join("\n")}` }] };
|
|
5213
5194
|
}
|
|
5214
|
-
case "dns-
|
|
5215
|
-
const
|
|
5216
|
-
|
|
5217
|
-
|
|
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 }] };
|
|
5195
|
+
case "dns-record": {
|
|
5196
|
+
const action = String(a.action);
|
|
5197
|
+
if (action !== "create" && action !== "update" && action !== "delete") {
|
|
5198
|
+
throw new Error("action must be one of: create, update, delete");
|
|
5279
5199
|
}
|
|
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
5200
|
const domain = String(a.domain);
|
|
5288
5201
|
const type = String(a.type).toUpperCase();
|
|
5289
5202
|
const dnsName = String(a.name);
|
|
5290
|
-
const value = String(a.value);
|
|
5291
5203
|
const dryRun = a.dryRun === true;
|
|
5292
|
-
if (!domain || !type || !dnsName
|
|
5293
|
-
throw new Error("domain, type,
|
|
5204
|
+
if (!domain || !type || !dnsName) {
|
|
5205
|
+
throw new Error("domain, type, and name are required");
|
|
5294
5206
|
}
|
|
5295
5207
|
const current = await mijnhostFetch(
|
|
5296
5208
|
`/domains/${encodeURIComponent(domain)}/dns`
|
|
5297
5209
|
);
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
)
|
|
5302
|
-
|
|
5303
|
-
(
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5210
|
+
let nextRecords;
|
|
5211
|
+
let diffArgs;
|
|
5212
|
+
let okMessage;
|
|
5213
|
+
if (action === "create") {
|
|
5214
|
+
const value = typeof a.value === "string" ? a.value : "";
|
|
5215
|
+
if (!value) throw new Error('action="create" requires value');
|
|
5216
|
+
const ttl = Number(a.ttl) || 3600;
|
|
5217
|
+
const newRecord = { type, name: dnsName, value, ttl };
|
|
5218
|
+
nextRecords = [...current.data.records, newRecord];
|
|
5219
|
+
diffArgs = { verb: "create", added: [newRecord] };
|
|
5220
|
+
okMessage = `DNS record created: ${type} ${dnsName} \u2192 ${value} (TTL: ${ttl})`;
|
|
5221
|
+
} else if (action === "update") {
|
|
5222
|
+
const oldValue = typeof a.oldValue === "string" ? a.oldValue : "";
|
|
5223
|
+
const newValue = typeof a.newValue === "string" ? a.newValue : "";
|
|
5224
|
+
if (!oldValue || !newValue) throw new Error('action="update" requires oldValue and newValue');
|
|
5225
|
+
const ttlArg = a.ttl !== void 0 ? Number(a.ttl) : void 0;
|
|
5226
|
+
const idx = current.data.records.findIndex(
|
|
5227
|
+
(r) => r.type === type && r.name === dnsName && r.value === oldValue
|
|
5228
|
+
);
|
|
5229
|
+
if (idx === -1) {
|
|
5230
|
+
throw new Error(`No matching DNS record found: ${type} ${dnsName} = ${oldValue}`);
|
|
5231
|
+
}
|
|
5232
|
+
const updated = [...current.data.records];
|
|
5233
|
+
const before = updated[idx];
|
|
5234
|
+
const after = {
|
|
5235
|
+
type,
|
|
5236
|
+
name: dnsName,
|
|
5237
|
+
value: newValue,
|
|
5238
|
+
ttl: ttlArg ?? before.ttl
|
|
5239
|
+
};
|
|
5240
|
+
updated[idx] = after;
|
|
5241
|
+
nextRecords = updated;
|
|
5242
|
+
diffArgs = { verb: "update", removed: [before], added: [after] };
|
|
5243
|
+
okMessage = `DNS record updated: ${type} ${dnsName} \u2192 ${newValue}${ttlArg ? ` (TTL: ${ttlArg})` : ""}`;
|
|
5244
|
+
} else {
|
|
5245
|
+
const value = typeof a.value === "string" ? a.value : "";
|
|
5246
|
+
if (!value) throw new Error('action="delete" requires value');
|
|
5247
|
+
const removed = current.data.records.filter(
|
|
5248
|
+
(r) => r.type === type && r.name === dnsName && r.value === value
|
|
5249
|
+
);
|
|
5250
|
+
const remaining = current.data.records.filter(
|
|
5251
|
+
(r) => !(r.type === type && r.name === dnsName && r.value === value)
|
|
5252
|
+
);
|
|
5253
|
+
if (removed.length === 0) {
|
|
5254
|
+
throw new Error(`No matching DNS record found: ${type} ${dnsName} = ${value}`);
|
|
5255
|
+
}
|
|
5256
|
+
nextRecords = remaining;
|
|
5257
|
+
diffArgs = { verb: "delete", removed };
|
|
5258
|
+
okMessage = `DNS record deleted: ${type} ${dnsName} = ${value} (${remaining.length} records remaining)`;
|
|
5307
5259
|
}
|
|
5308
5260
|
if (dryRun) {
|
|
5309
|
-
const diff = formatDnsDiff(domain, current.data.records,
|
|
5310
|
-
verb: "delete",
|
|
5311
|
-
removed
|
|
5312
|
-
});
|
|
5261
|
+
const diff = formatDnsDiff(domain, current.data.records, nextRecords, diffArgs);
|
|
5313
5262
|
return { content: [{ type: "text", text: diff }] };
|
|
5314
5263
|
}
|
|
5315
5264
|
await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
|
|
5316
5265
|
method: "PUT",
|
|
5317
|
-
body: JSON.stringify({ records:
|
|
5266
|
+
body: JSON.stringify({ records: nextRecords })
|
|
5318
5267
|
});
|
|
5319
|
-
return { content: [{ type: "text", text:
|
|
5268
|
+
return { content: [{ type: "text", text: okMessage }] };
|
|
5320
5269
|
}
|
|
5321
5270
|
default:
|
|
5322
5271
|
if (TRIGGER_TOOL_NAMES.has(name)) {
|