@mgsoftwarebv/mg-dashboard-mcp 3.14.0 → 5.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 +456 -686
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -296,16 +296,21 @@ 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"]
|
|
@@ -489,8 +494,18 @@ ${lines.join("\n")}` }] };
|
|
|
489
494
|
const instance = await discoverInstance(project, conn, proxy, sshExec2);
|
|
490
495
|
const limit = Math.min(Math.max(Number(args2.limit) || 20, 1), 100);
|
|
491
496
|
const queryParts = [`page%5Bsize%5D=${limit}`];
|
|
492
|
-
|
|
493
|
-
|
|
497
|
+
const failedOnly = args2.failedOnly === true;
|
|
498
|
+
const statusArg = failedOnly ? "FAILED,CRASHED,SYSTEM_FAILURE,INTERRUPTED,TIMED_OUT" : args2.status ? String(args2.status) : "";
|
|
499
|
+
if (statusArg) queryParts.push(`filter%5Bstatus%5D=${encodeURIComponent(statusArg)}`);
|
|
500
|
+
if (args2.taskIdentifier) {
|
|
501
|
+
queryParts.push(`filter%5BtaskIdentifier%5D=${encodeURIComponent(String(args2.taskIdentifier))}`);
|
|
502
|
+
}
|
|
503
|
+
const sinceMinutes = Number(args2.sinceMinutes);
|
|
504
|
+
if (Number.isFinite(sinceMinutes) && sinceMinutes > 0) {
|
|
505
|
+
const capped = Math.min(sinceMinutes, 10080);
|
|
506
|
+
const fromMs = Date.now() - capped * 6e4;
|
|
507
|
+
queryParts.push(`filter%5Bfrom%5D=${fromMs}`);
|
|
508
|
+
}
|
|
494
509
|
const rawJson = await triggerApi(
|
|
495
510
|
conn,
|
|
496
511
|
proxy,
|
|
@@ -638,582 +653,6 @@ ${rawJson.substring(0, 500)}`
|
|
|
638
653
|
}
|
|
639
654
|
}
|
|
640
655
|
|
|
641
|
-
// src/agent-tools.ts
|
|
642
|
-
function clamp(val, min, max) {
|
|
643
|
-
return Math.max(min, Math.min(max, val));
|
|
644
|
-
}
|
|
645
|
-
function sanitizeString(val, maxLen) {
|
|
646
|
-
return String(val ?? "").slice(0, maxLen);
|
|
647
|
-
}
|
|
648
|
-
var AGENT_TOOLS = [
|
|
649
|
-
{
|
|
650
|
-
name: "web-search",
|
|
651
|
-
description: "Search the web using DuckDuckGo. Returns a list of results with title, URL, and snippet. Use this to find companies, websites, directories, etc.",
|
|
652
|
-
inputSchema: {
|
|
653
|
-
type: "object",
|
|
654
|
-
properties: {
|
|
655
|
-
query: { type: "string", description: 'Search query (e.g. "logistiek bedrijf MKB nederland")' },
|
|
656
|
-
max_results: { type: "number", description: "Max results to return (default: 15, max: 30)" }
|
|
657
|
-
},
|
|
658
|
-
required: ["query"]
|
|
659
|
-
}
|
|
660
|
-
},
|
|
661
|
-
{
|
|
662
|
-
name: "web-fetch",
|
|
663
|
-
description: "Fetch a web page and return its text content (HTML tags stripped). Automatically extracts emails and phone numbers found on the page. Returns at most 15000 characters of cleaned text.",
|
|
664
|
-
inputSchema: {
|
|
665
|
-
type: "object",
|
|
666
|
-
properties: {
|
|
667
|
-
url: { type: "string", description: 'Full URL to fetch (e.g. "https://example.nl/contact")' },
|
|
668
|
-
extract_links: { type: "boolean", description: "Also extract all links from the page (default: false)" }
|
|
669
|
-
},
|
|
670
|
-
required: ["url"]
|
|
671
|
-
}
|
|
672
|
-
},
|
|
673
|
-
{
|
|
674
|
-
name: "web-find-contacts",
|
|
675
|
-
description: "POWERFUL contact finder. Crawls a company website (homepage + contact/about/team pages), extracts ALL emails, phone numbers, and LinkedIn URLs via regex + mailto/tel parsing. Also searches Google for the company email as fallback. Returns structured contact data. USE THIS for every lead instead of manually browsing contact pages.",
|
|
676
|
-
inputSchema: {
|
|
677
|
-
type: "object",
|
|
678
|
-
properties: {
|
|
679
|
-
url: {
|
|
680
|
-
type: "string",
|
|
681
|
-
description: 'Company website URL or domain (e.g. "https://example.nl" or "example.nl")'
|
|
682
|
-
},
|
|
683
|
-
company_name: {
|
|
684
|
-
type: "string",
|
|
685
|
-
description: "Company name (improves Google search accuracy)"
|
|
686
|
-
},
|
|
687
|
-
include_search: {
|
|
688
|
-
type: "boolean",
|
|
689
|
-
description: "Also search Google for emails (default: true)"
|
|
690
|
-
}
|
|
691
|
-
},
|
|
692
|
-
required: ["url"]
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
];
|
|
696
|
-
var AGENT_TOOL_NAMES = new Set(AGENT_TOOLS.map((t) => t.name));
|
|
697
|
-
var AGENT_TOOL_MODULE_MAP = {
|
|
698
|
-
"web-search": "agent_reporting",
|
|
699
|
-
"web-fetch": "agent_reporting",
|
|
700
|
-
"web-find-contacts": "agent_reporting"
|
|
701
|
-
};
|
|
702
|
-
var WEB_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
703
|
-
async function webSearch(query, maxResults) {
|
|
704
|
-
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
705
|
-
const response = await fetch(url, {
|
|
706
|
-
headers: {
|
|
707
|
-
"User-Agent": WEB_USER_AGENT,
|
|
708
|
-
"Accept": "text/html",
|
|
709
|
-
"Accept-Language": "nl,en;q=0.9"
|
|
710
|
-
},
|
|
711
|
-
redirect: "follow"
|
|
712
|
-
});
|
|
713
|
-
if (!response.ok) throw new Error(`Search failed: HTTP ${response.status}`);
|
|
714
|
-
const html = await response.text();
|
|
715
|
-
const results = [];
|
|
716
|
-
const resultBlocks = html.split(/class="result\s/);
|
|
717
|
-
for (let i = 1; i < resultBlocks.length && results.length < maxResults; i++) {
|
|
718
|
-
const block = resultBlocks[i];
|
|
719
|
-
const titleMatch = block.match(/class="result__a"[^>]*>([^<]+)</);
|
|
720
|
-
const hrefMatch = block.match(/class="result__a"\s+href="([^"]+)"/);
|
|
721
|
-
const snippetMatch = block.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/);
|
|
722
|
-
if (!hrefMatch) continue;
|
|
723
|
-
let href = hrefMatch[1];
|
|
724
|
-
if (href.includes("uddg=")) {
|
|
725
|
-
const uddg = new URL(href, "https://duckduckgo.com").searchParams.get("uddg");
|
|
726
|
-
if (uddg) href = uddg;
|
|
727
|
-
}
|
|
728
|
-
if (!href.startsWith("http")) continue;
|
|
729
|
-
const title = titleMatch ? titleMatch[1].replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/"/g, '"').trim() : href;
|
|
730
|
-
const snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/"/g, '"').replace(/\s+/g, " ").trim() : "";
|
|
731
|
-
results.push({ title, url: href, snippet });
|
|
732
|
-
}
|
|
733
|
-
return results;
|
|
734
|
-
}
|
|
735
|
-
var EMAIL_REGEX = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g;
|
|
736
|
-
var PHONE_NL_REGEX = /(?:\+31|0)[\s\-.]?\(?\d{1,3}\)?[\s\-.]?\d{3,4}[\s\-.]?\d{2,4}/g;
|
|
737
|
-
var LINKEDIN_REGEX = /https?:\/\/(?:www\.)?linkedin\.com\/(?:in|company)\/[a-zA-Z0-9\-_%]+\/?/gi;
|
|
738
|
-
var NOISE_EMAIL_PATTERNS = [
|
|
739
|
-
/noreply@/i,
|
|
740
|
-
/no-reply@/i,
|
|
741
|
-
/mailer-daemon@/i,
|
|
742
|
-
/postmaster@/i,
|
|
743
|
-
/@example\./i,
|
|
744
|
-
/test@/i,
|
|
745
|
-
/@wix\.com$/i,
|
|
746
|
-
/@sentry\.io$/i,
|
|
747
|
-
/@wordpress\./i,
|
|
748
|
-
/@gravatar\.com$/i,
|
|
749
|
-
/@schema\.org$/i,
|
|
750
|
-
/@w3\.org$/i,
|
|
751
|
-
/@facebook\.com$/i,
|
|
752
|
-
/@google\.com$/i,
|
|
753
|
-
/@twitter\.com$/i,
|
|
754
|
-
/@github\.com$/i,
|
|
755
|
-
/@cloudflare\./i,
|
|
756
|
-
/@vercel\./i,
|
|
757
|
-
/@netlify\./i,
|
|
758
|
-
/@cookiebot\./i,
|
|
759
|
-
/@hotjar\./i,
|
|
760
|
-
/@hubspot\./i,
|
|
761
|
-
/@mailchimp\./i,
|
|
762
|
-
/@googleusercontent/i,
|
|
763
|
-
/@gstatic/i,
|
|
764
|
-
/@youtube/i,
|
|
765
|
-
/@recaptcha/i,
|
|
766
|
-
/@privacy/i,
|
|
767
|
-
/@cookie/i,
|
|
768
|
-
/@gdpr/i,
|
|
769
|
-
/@dynamicweb\./i,
|
|
770
|
-
/@placeholder\./i,
|
|
771
|
-
/@yourcompany\./i,
|
|
772
|
-
/@company\./i,
|
|
773
|
-
/smith@/i,
|
|
774
|
-
/doe@/i,
|
|
775
|
-
/demo@/i,
|
|
776
|
-
/sample@/i,
|
|
777
|
-
/naam@/i,
|
|
778
|
-
/voorbeeld/i,
|
|
779
|
-
/your-?email/i,
|
|
780
|
-
/email@/i,
|
|
781
|
-
/@domein\./i,
|
|
782
|
-
/@bedrijf\./i,
|
|
783
|
-
/@domain\./i,
|
|
784
|
-
/@sentry/i,
|
|
785
|
-
/@wixpress/i,
|
|
786
|
-
/@lieferkassen/i,
|
|
787
|
-
/john@/i,
|
|
788
|
-
/jane@/i
|
|
789
|
-
];
|
|
790
|
-
var CONTACT_PATH_KEYWORDS = [
|
|
791
|
-
"contact",
|
|
792
|
-
"about",
|
|
793
|
-
"over-ons",
|
|
794
|
-
"over",
|
|
795
|
-
"team",
|
|
796
|
-
"medewerker",
|
|
797
|
-
"impressum",
|
|
798
|
-
"imprint",
|
|
799
|
-
"wie-zijn",
|
|
800
|
-
"ons-team",
|
|
801
|
-
"werknemers",
|
|
802
|
-
"organisatie",
|
|
803
|
-
"zakelijk",
|
|
804
|
-
"bedrijfsinfo",
|
|
805
|
-
"neem-contact",
|
|
806
|
-
"footer"
|
|
807
|
-
];
|
|
808
|
-
var CONTACT_PATHS_TO_TRY = [
|
|
809
|
-
"/contact",
|
|
810
|
-
"/over-ons",
|
|
811
|
-
"/about",
|
|
812
|
-
"/about-us",
|
|
813
|
-
"/team",
|
|
814
|
-
"/contact-us",
|
|
815
|
-
"/over",
|
|
816
|
-
"/wie-zijn-wij",
|
|
817
|
-
"/ons-team",
|
|
818
|
-
"/zakelijk/over-ons",
|
|
819
|
-
"/medewerkers",
|
|
820
|
-
"/organisatie",
|
|
821
|
-
"/bedrijfsinformatie",
|
|
822
|
-
"/impressum"
|
|
823
|
-
];
|
|
824
|
-
function decodeUnicodeEscapes(text) {
|
|
825
|
-
return text.replace(
|
|
826
|
-
/\\u([0-9a-fA-F]{4})/g,
|
|
827
|
-
(_, hex) => String.fromCharCode(parseInt(hex, 16))
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
function decodeHtmlEntities(text) {
|
|
831
|
-
return text.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num))).replace(
|
|
832
|
-
/&#x([0-9a-fA-F]+);/g,
|
|
833
|
-
(_, hex) => String.fromCharCode(parseInt(hex, 16))
|
|
834
|
-
);
|
|
835
|
-
}
|
|
836
|
-
function extractEmailsFromHtml(html) {
|
|
837
|
-
const emails = /* @__PURE__ */ new Set();
|
|
838
|
-
const decoded = decodeHtmlEntities(decodeUnicodeEscapes(html));
|
|
839
|
-
const mailtoRegex = /mailto:([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})/gi;
|
|
840
|
-
let m;
|
|
841
|
-
for (const src of [html, decoded]) {
|
|
842
|
-
while ((m = mailtoRegex.exec(src)) !== null) emails.add(m[1].toLowerCase());
|
|
843
|
-
}
|
|
844
|
-
const noStyles = decoded.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
845
|
-
const textEmails = noStyles.match(EMAIL_REGEX) || [];
|
|
846
|
-
for (const e of textEmails) {
|
|
847
|
-
const lower = e.toLowerCase();
|
|
848
|
-
if (!/\.(png|jpg|jpeg|gif|svg|webp|css|js|woff|ttf|eot|ico)$/i.test(lower)) {
|
|
849
|
-
emails.add(lower);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
return [...emails].filter((e) => !NOISE_EMAIL_PATTERNS.some((p) => p.test(e)));
|
|
853
|
-
}
|
|
854
|
-
function extractPhonesFromHtml(html) {
|
|
855
|
-
const phones = /* @__PURE__ */ new Set();
|
|
856
|
-
const decoded = decodeHtmlEntities(decodeUnicodeEscapes(html));
|
|
857
|
-
const telRegex = /href="tel:([^"]+)"/gi;
|
|
858
|
-
let m;
|
|
859
|
-
for (const src of [html, decoded]) {
|
|
860
|
-
while ((m = telRegex.exec(src)) !== null) {
|
|
861
|
-
const clean = m[1].replace(/[\s\-().]/g, "");
|
|
862
|
-
if (clean.length >= 10 && clean.length <= 14) phones.add(m[1].trim());
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
const stripped = decoded.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ");
|
|
866
|
-
const textPhones = stripped.match(PHONE_NL_REGEX) || [];
|
|
867
|
-
for (const p of textPhones) {
|
|
868
|
-
const clean = p.replace(/[\s\-().]/g, "");
|
|
869
|
-
if (clean.length >= 10 && clean.length <= 14) phones.add(p.trim());
|
|
870
|
-
}
|
|
871
|
-
return [...phones].slice(0, 10);
|
|
872
|
-
}
|
|
873
|
-
function extractLinkedInFromHtml(html) {
|
|
874
|
-
const links = /* @__PURE__ */ new Set();
|
|
875
|
-
const matches = html.match(LINKEDIN_REGEX) || [];
|
|
876
|
-
for (const l of matches) links.add(l.replace(/\/$/, ""));
|
|
877
|
-
return [...links];
|
|
878
|
-
}
|
|
879
|
-
function discoverContactPages(html, baseUrl) {
|
|
880
|
-
let base;
|
|
881
|
-
try {
|
|
882
|
-
base = new URL(baseUrl);
|
|
883
|
-
} catch {
|
|
884
|
-
return [];
|
|
885
|
-
}
|
|
886
|
-
const pages = /* @__PURE__ */ new Set();
|
|
887
|
-
for (const path of CONTACT_PATHS_TO_TRY) {
|
|
888
|
-
pages.add(`${base.origin}${path}`);
|
|
889
|
-
}
|
|
890
|
-
const linkRegex = /<a\s[^>]*href="([^"#]*)"[^>]*>/gi;
|
|
891
|
-
let m;
|
|
892
|
-
while ((m = linkRegex.exec(html)) !== null) {
|
|
893
|
-
try {
|
|
894
|
-
const url = new URL(m[1], baseUrl);
|
|
895
|
-
if (url.hostname !== base.hostname) continue;
|
|
896
|
-
const path = url.pathname.toLowerCase();
|
|
897
|
-
if (CONTACT_PATH_KEYWORDS.some((kw) => path.includes(kw))) {
|
|
898
|
-
pages.add(url.origin + url.pathname);
|
|
899
|
-
}
|
|
900
|
-
} catch {
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
pages.delete(base.origin + base.pathname);
|
|
904
|
-
return [...pages].slice(0, 10);
|
|
905
|
-
}
|
|
906
|
-
function guessCommonEmails(domain) {
|
|
907
|
-
const d = domain.replace(/^www\./, "");
|
|
908
|
-
return [`info@${d}`, `contact@${d}`, `hello@${d}`, `administratie@${d}`, `verkoop@${d}`];
|
|
909
|
-
}
|
|
910
|
-
var BOT_CHALLENGE_INDICATORS = [
|
|
911
|
-
"sgcaptcha",
|
|
912
|
-
"challenge-platform",
|
|
913
|
-
"cf-browser-verification",
|
|
914
|
-
"Just a moment",
|
|
915
|
-
"Checking your browser",
|
|
916
|
-
"Enable JavaScript and cookies",
|
|
917
|
-
"Attention Required",
|
|
918
|
-
"DDoS protection by"
|
|
919
|
-
];
|
|
920
|
-
function isBotChallengePage(html) {
|
|
921
|
-
if (html.length > 2e3) return false;
|
|
922
|
-
const lower = html.toLowerCase();
|
|
923
|
-
return BOT_CHALLENGE_INDICATORS.some((ind) => lower.includes(ind.toLowerCase()));
|
|
924
|
-
}
|
|
925
|
-
async function fetchRawHtml(url, timeoutMs = 1e4) {
|
|
926
|
-
const controller = new AbortController();
|
|
927
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
928
|
-
try {
|
|
929
|
-
const res = await fetch(url, {
|
|
930
|
-
headers: {
|
|
931
|
-
"User-Agent": WEB_USER_AGENT,
|
|
932
|
-
"Accept": "text/html,application/xhtml+xml",
|
|
933
|
-
"Accept-Language": "nl,en;q=0.9"
|
|
934
|
-
},
|
|
935
|
-
redirect: "follow",
|
|
936
|
-
signal: controller.signal
|
|
937
|
-
});
|
|
938
|
-
const ct = res.headers.get("content-type") || "";
|
|
939
|
-
if (!ct.includes("text/html") && !ct.includes("text/plain") && !ct.includes("xhtml")) return null;
|
|
940
|
-
if (!res.ok && res.status !== 403) return null;
|
|
941
|
-
const html = await res.text();
|
|
942
|
-
if (isBotChallengePage(html)) return null;
|
|
943
|
-
return html;
|
|
944
|
-
} catch {
|
|
945
|
-
return null;
|
|
946
|
-
} finally {
|
|
947
|
-
clearTimeout(timer);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
async function fetchWaybackHtml(url, timeoutMs = 15e3) {
|
|
951
|
-
const cleanUrl = url.replace(/^https?:\/\//, "");
|
|
952
|
-
const wbUrl = `https://web.archive.org/web/2024/${cleanUrl}`;
|
|
953
|
-
const controller = new AbortController();
|
|
954
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
955
|
-
try {
|
|
956
|
-
const res = await fetch(wbUrl, {
|
|
957
|
-
headers: { "User-Agent": WEB_USER_AGENT },
|
|
958
|
-
redirect: "follow",
|
|
959
|
-
signal: controller.signal
|
|
960
|
-
});
|
|
961
|
-
if (!res.ok) return null;
|
|
962
|
-
const html = await res.text();
|
|
963
|
-
if (html.length < 500) return null;
|
|
964
|
-
return html;
|
|
965
|
-
} catch {
|
|
966
|
-
return null;
|
|
967
|
-
} finally {
|
|
968
|
-
clearTimeout(timer);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
async function webFetch(url, extractLinks) {
|
|
972
|
-
const controller = new AbortController();
|
|
973
|
-
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
974
|
-
try {
|
|
975
|
-
const response = await fetch(url, {
|
|
976
|
-
headers: {
|
|
977
|
-
"User-Agent": WEB_USER_AGENT,
|
|
978
|
-
"Accept": "text/html,application/xhtml+xml",
|
|
979
|
-
"Accept-Language": "nl,en;q=0.9"
|
|
980
|
-
},
|
|
981
|
-
redirect: "follow",
|
|
982
|
-
signal: controller.signal
|
|
983
|
-
});
|
|
984
|
-
if (!response.ok) throw new Error(`Fetch failed: HTTP ${response.status}`);
|
|
985
|
-
const contentType = response.headers.get("content-type") || "";
|
|
986
|
-
if (!contentType.includes("text/html") && !contentType.includes("text/plain") && !contentType.includes("application/xhtml")) {
|
|
987
|
-
throw new Error(`Unsupported content type: ${contentType}. Only HTML/text pages supported.`);
|
|
988
|
-
}
|
|
989
|
-
const html = await response.text();
|
|
990
|
-
const links = [];
|
|
991
|
-
if (extractLinks) {
|
|
992
|
-
const linkRegex = /<a\s[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
993
|
-
let match;
|
|
994
|
-
while ((match = linkRegex.exec(html)) !== null) {
|
|
995
|
-
const href = match[1];
|
|
996
|
-
const text2 = match[2].replace(/<[^>]+>/g, "").trim();
|
|
997
|
-
if (href && href.startsWith("http")) {
|
|
998
|
-
links.push({ href, text: text2.slice(0, 100) });
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "").replace(/<nav[\s\S]*?<\/nav>/gi, " [NAV] ").replace(/<header[\s\S]*?<\/header>/gi, " [HEADER] ").replace(/<footer[\s\S]*?<\/footer>/gi, " [FOOTER] ").replace(/<(br|hr)\s*\/?>/gi, "\n").replace(/<\/(p|div|h[1-6]|li|tr|section|article)>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'").replace(/"/g, '"').replace(/[ \t]+/g, " ").replace(/\n\s*\n/g, "\n").trim();
|
|
1003
|
-
const MAX_TEXT = 15e3;
|
|
1004
|
-
if (text.length > MAX_TEXT) {
|
|
1005
|
-
text = text.slice(0, MAX_TEXT) + "\n\n[... truncated at 15000 chars ...]";
|
|
1006
|
-
}
|
|
1007
|
-
return { text, rawHtml: html, links };
|
|
1008
|
-
} finally {
|
|
1009
|
-
clearTimeout(timeout);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
async function handleAgentTool(name, args2) {
|
|
1013
|
-
switch (name) {
|
|
1014
|
-
case "web-search": {
|
|
1015
|
-
const query = sanitizeString(args2.query, 500);
|
|
1016
|
-
if (!query) throw new Error("query is required");
|
|
1017
|
-
const maxResults = clamp(Number(args2.max_results) || 15, 1, 30);
|
|
1018
|
-
const results = await webSearch(query, maxResults);
|
|
1019
|
-
if (results.length === 0) {
|
|
1020
|
-
return { content: [{ type: "text", text: `No search results found for: "${query}"` }] };
|
|
1021
|
-
}
|
|
1022
|
-
const formatted = results.map(
|
|
1023
|
-
(r, i) => `${i + 1}. ${r.title}
|
|
1024
|
-
URL: ${r.url}
|
|
1025
|
-
${r.snippet}`
|
|
1026
|
-
).join("\n\n");
|
|
1027
|
-
return {
|
|
1028
|
-
content: [{
|
|
1029
|
-
type: "text",
|
|
1030
|
-
text: `Search results for "${query}" (${results.length} results):
|
|
1031
|
-
|
|
1032
|
-
${formatted}`
|
|
1033
|
-
}]
|
|
1034
|
-
};
|
|
1035
|
-
}
|
|
1036
|
-
case "web-fetch": {
|
|
1037
|
-
const url = sanitizeString(args2.url, 2e3);
|
|
1038
|
-
if (!url) throw new Error("url is required");
|
|
1039
|
-
if (!/^https?:\/\//i.test(url)) throw new Error("url must start with http:// or https://");
|
|
1040
|
-
const extractLinks = Boolean(args2.extract_links);
|
|
1041
|
-
const result = await webFetch(url, extractLinks);
|
|
1042
|
-
let text = `Content from ${url} (${result.text.length} chars):
|
|
1043
|
-
|
|
1044
|
-
${result.text}`;
|
|
1045
|
-
if (extractLinks && result.links.length > 0) {
|
|
1046
|
-
const linkList = result.links.slice(0, 50).map(
|
|
1047
|
-
(l) => ` ${l.text ? l.text + ": " : ""}${l.href}`
|
|
1048
|
-
).join("\n");
|
|
1049
|
-
text += `
|
|
1050
|
-
|
|
1051
|
-
--- Links (${result.links.length} found, showing max 50) ---
|
|
1052
|
-
${linkList}`;
|
|
1053
|
-
}
|
|
1054
|
-
const pageEmails = extractEmailsFromHtml(result.rawHtml);
|
|
1055
|
-
const pagePhones = extractPhonesFromHtml(result.rawHtml);
|
|
1056
|
-
const pageLinkedIn = extractLinkedInFromHtml(result.rawHtml);
|
|
1057
|
-
if (pageEmails.length > 0 || pagePhones.length > 0 || pageLinkedIn.length > 0) {
|
|
1058
|
-
text += "\n\n--- Auto-extracted Contact Info ---";
|
|
1059
|
-
if (pageEmails.length > 0) text += `
|
|
1060
|
-
Emails: ${pageEmails.join(", ")}`;
|
|
1061
|
-
if (pagePhones.length > 0) text += `
|
|
1062
|
-
Phones: ${pagePhones.join(", ")}`;
|
|
1063
|
-
if (pageLinkedIn.length > 0) text += `
|
|
1064
|
-
LinkedIn: ${pageLinkedIn.join(", ")}`;
|
|
1065
|
-
}
|
|
1066
|
-
return { content: [{ type: "text", text }] };
|
|
1067
|
-
}
|
|
1068
|
-
case "web-find-contacts": {
|
|
1069
|
-
const inputUrl = sanitizeString(args2.url, 2e3);
|
|
1070
|
-
if (!inputUrl) throw new Error("url is required");
|
|
1071
|
-
const companyName = sanitizeString(args2.company_name, 500);
|
|
1072
|
-
const includeSearch = args2.include_search !== false;
|
|
1073
|
-
const fullUrl = inputUrl.startsWith("http") ? inputUrl : `https://${inputUrl}`;
|
|
1074
|
-
let baseUrl;
|
|
1075
|
-
try {
|
|
1076
|
-
baseUrl = new URL(fullUrl);
|
|
1077
|
-
} catch {
|
|
1078
|
-
throw new Error(`Invalid URL: ${inputUrl}`);
|
|
1079
|
-
}
|
|
1080
|
-
const domain = baseUrl.hostname.replace(/^www\./, "");
|
|
1081
|
-
const urlsToTry = [fullUrl];
|
|
1082
|
-
if (!fullUrl.includes("www.")) urlsToTry.push(fullUrl.replace("://", "://www."));
|
|
1083
|
-
if (fullUrl.startsWith("https")) {
|
|
1084
|
-
urlsToTry.push(fullUrl.replace("https", "http"));
|
|
1085
|
-
if (!fullUrl.includes("www.")) urlsToTry.push(fullUrl.replace("https://", "http://www."));
|
|
1086
|
-
}
|
|
1087
|
-
let html = null;
|
|
1088
|
-
let usedWayback = false;
|
|
1089
|
-
for (const tryUrl of urlsToTry) {
|
|
1090
|
-
html = await fetchRawHtml(tryUrl, 12e3);
|
|
1091
|
-
if (html) break;
|
|
1092
|
-
}
|
|
1093
|
-
if (!html) {
|
|
1094
|
-
html = await fetchWaybackHtml(`https://${domain}`, 15e3);
|
|
1095
|
-
if (html) usedWayback = true;
|
|
1096
|
-
}
|
|
1097
|
-
if (!html) throw new Error(`Could not fetch ${fullUrl} (site may be down or blocking)`);
|
|
1098
|
-
const contactPages = usedWayback ? [] : discoverContactPages(html, fullUrl);
|
|
1099
|
-
const pagePromises = contactPages.map(async (pageUrl) => {
|
|
1100
|
-
const pageHtml = await fetchRawHtml(pageUrl, 8e3);
|
|
1101
|
-
return { url: pageUrl, html: pageHtml };
|
|
1102
|
-
});
|
|
1103
|
-
const pageResults = await Promise.allSettled(pagePromises);
|
|
1104
|
-
const successPages = [usedWayback ? `(wayback) ${domain}` : fullUrl];
|
|
1105
|
-
const allHtmls = [html];
|
|
1106
|
-
for (const result of pageResults) {
|
|
1107
|
-
if (result.status === "fulfilled" && result.value.html) {
|
|
1108
|
-
allHtmls.push(result.value.html);
|
|
1109
|
-
successPages.push(result.value.url);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
if (usedWayback) {
|
|
1113
|
-
const waybackContactPaths = ["/contact", "/over-ons", "/about", "/team"];
|
|
1114
|
-
const wbPromises = waybackContactPaths.map(async (path) => {
|
|
1115
|
-
const wbHtml = await fetchWaybackHtml(`https://${domain}${path}`, 12e3);
|
|
1116
|
-
return { path, html: wbHtml };
|
|
1117
|
-
});
|
|
1118
|
-
const wbResults = await Promise.allSettled(wbPromises);
|
|
1119
|
-
for (const wr of wbResults) {
|
|
1120
|
-
if (wr.status === "fulfilled" && wr.value.html) {
|
|
1121
|
-
allHtmls.push(wr.value.html);
|
|
1122
|
-
successPages.push(`(wayback) ${domain}${wr.value.path}`);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
const allEmails = /* @__PURE__ */ new Set();
|
|
1127
|
-
const allPhones = /* @__PURE__ */ new Set();
|
|
1128
|
-
const allLinkedIn = /* @__PURE__ */ new Set();
|
|
1129
|
-
for (const pageHtml of allHtmls) {
|
|
1130
|
-
for (const e of extractEmailsFromHtml(pageHtml)) allEmails.add(e);
|
|
1131
|
-
for (const p of extractPhonesFromHtml(pageHtml)) allPhones.add(p);
|
|
1132
|
-
for (const l of extractLinkedInFromHtml(pageHtml)) allLinkedIn.add(l);
|
|
1133
|
-
}
|
|
1134
|
-
let searchNote = "";
|
|
1135
|
-
if (includeSearch) {
|
|
1136
|
-
try {
|
|
1137
|
-
const queries = companyName ? [`"${companyName}" "${domain}" email`, `"@${domain}" email`] : [`"${domain}" email contact`, `"@${domain}" email`, `site:${domain} "@"`];
|
|
1138
|
-
let totalSearchResults = 0;
|
|
1139
|
-
for (const searchQuery of queries) {
|
|
1140
|
-
const searchResults = await webSearch(searchQuery, 8);
|
|
1141
|
-
totalSearchResults += searchResults.length;
|
|
1142
|
-
for (const r of searchResults) {
|
|
1143
|
-
const combined = `${r.title} ${r.snippet}`;
|
|
1144
|
-
const decoded = decodeHtmlEntities(decodeUnicodeEscapes(combined));
|
|
1145
|
-
const snippetEmails = decoded.match(EMAIL_REGEX) || [];
|
|
1146
|
-
for (const e of snippetEmails) {
|
|
1147
|
-
const lower = e.toLowerCase();
|
|
1148
|
-
const domainBase = domain.split(".")[0] ?? domain;
|
|
1149
|
-
if (lower.includes(domainBase) && !NOISE_EMAIL_PATTERNS.some((p) => p.test(lower))) {
|
|
1150
|
-
allEmails.add(lower);
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
const snippetPhones = decoded.match(PHONE_NL_REGEX) || [];
|
|
1154
|
-
for (const p of snippetPhones) {
|
|
1155
|
-
const clean = p.replace(/[\s\-().]/g, "");
|
|
1156
|
-
if (clean.length >= 10 && clean.length <= 14) allPhones.add(p.trim());
|
|
1157
|
-
}
|
|
1158
|
-
const snippetLi = decoded.match(LINKEDIN_REGEX) || [];
|
|
1159
|
-
for (const l of snippetLi) allLinkedIn.add(l.replace(/\/$/, ""));
|
|
1160
|
-
}
|
|
1161
|
-
if (allEmails.size > 0) break;
|
|
1162
|
-
}
|
|
1163
|
-
searchNote = ` + ${totalSearchResults} search results`;
|
|
1164
|
-
} catch {
|
|
1165
|
-
searchNote = " (search failed)";
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
const foundEmails = [...allEmails];
|
|
1169
|
-
const generalPatterns = /^(info|contact|hello|admin|office|receptie|secretariaat|verkoop|administratie|support|service|boekingen|reserveringen)@/i;
|
|
1170
|
-
const generalEmails = foundEmails.filter((e) => generalPatterns.test(e));
|
|
1171
|
-
const personalEmails = foundEmails.filter((e) => !generalPatterns.test(e));
|
|
1172
|
-
const guessed = guessCommonEmails(domain);
|
|
1173
|
-
const newGuesses = guessed.filter((g) => !allEmails.has(g));
|
|
1174
|
-
const lines = [
|
|
1175
|
-
`=== CONTACT SCAN: ${domain} ===`,
|
|
1176
|
-
`Pages scanned: ${successPages.length}${searchNote}`,
|
|
1177
|
-
""
|
|
1178
|
-
];
|
|
1179
|
-
if (foundEmails.length > 0) {
|
|
1180
|
-
lines.push(`EMAILS FOUND (${foundEmails.length}):`);
|
|
1181
|
-
if (generalEmails.length > 0) {
|
|
1182
|
-
lines.push(" General:");
|
|
1183
|
-
for (const e of generalEmails) lines.push(` * ${e}`);
|
|
1184
|
-
}
|
|
1185
|
-
if (personalEmails.length > 0) {
|
|
1186
|
-
lines.push(" Personal/Department:");
|
|
1187
|
-
for (const e of personalEmails) lines.push(` * ${e}`);
|
|
1188
|
-
}
|
|
1189
|
-
} else {
|
|
1190
|
-
lines.push("EMAILS FOUND: None on website");
|
|
1191
|
-
lines.push("SUGGESTED EMAILS (common patterns, verify before use):");
|
|
1192
|
-
for (const e of newGuesses) lines.push(` ? ${e}`);
|
|
1193
|
-
}
|
|
1194
|
-
lines.push("");
|
|
1195
|
-
if ([...allPhones].length > 0) {
|
|
1196
|
-
lines.push(`PHONE NUMBERS (${allPhones.size}):`);
|
|
1197
|
-
for (const p of allPhones) lines.push(` ${p}`);
|
|
1198
|
-
} else {
|
|
1199
|
-
lines.push("PHONE NUMBERS: None found");
|
|
1200
|
-
}
|
|
1201
|
-
lines.push("");
|
|
1202
|
-
if ([...allLinkedIn].length > 0) {
|
|
1203
|
-
lines.push(`LINKEDIN (${allLinkedIn.size}):`);
|
|
1204
|
-
for (const l of allLinkedIn) lines.push(` ${l}`);
|
|
1205
|
-
}
|
|
1206
|
-
lines.push("", "PAGES CHECKED:");
|
|
1207
|
-
for (const p of successPages) lines.push(` [OK] ${p}`);
|
|
1208
|
-
const failedPages = contactPages.filter((p) => !successPages.includes(p));
|
|
1209
|
-
for (const p of failedPages) lines.push(` [--] ${p}`);
|
|
1210
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1211
|
-
}
|
|
1212
|
-
default:
|
|
1213
|
-
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
656
|
// src/vercel-tools.ts
|
|
1218
657
|
var VERCEL_API = "https://api.vercel.com";
|
|
1219
658
|
var VERCEL_TOOLS = [
|
|
@@ -1245,6 +684,41 @@ var VERCEL_TOOLS = [
|
|
|
1245
684
|
required: ["project"]
|
|
1246
685
|
}
|
|
1247
686
|
},
|
|
687
|
+
{
|
|
688
|
+
name: "vercel-domains",
|
|
689
|
+
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.',
|
|
690
|
+
inputSchema: {
|
|
691
|
+
type: "object",
|
|
692
|
+
properties: {
|
|
693
|
+
action: {
|
|
694
|
+
type: "string",
|
|
695
|
+
enum: ["list", "add", "remove", "verify"],
|
|
696
|
+
description: "Which operation to perform (default: list)."
|
|
697
|
+
},
|
|
698
|
+
project: {
|
|
699
|
+
type: "string",
|
|
700
|
+
description: "Vercel project ID or name (from vercel-projects)."
|
|
701
|
+
},
|
|
702
|
+
domain: {
|
|
703
|
+
type: "string",
|
|
704
|
+
description: 'Domain name (required for action="add", "remove", or "verify").'
|
|
705
|
+
},
|
|
706
|
+
gitBranch: {
|
|
707
|
+
type: "string",
|
|
708
|
+
description: 'Optional git branch this domain should deploy from (action="add" only).'
|
|
709
|
+
},
|
|
710
|
+
redirect: {
|
|
711
|
+
type: "string",
|
|
712
|
+
description: 'Optional redirect target domain (action="add" only).'
|
|
713
|
+
},
|
|
714
|
+
redirectStatusCode: {
|
|
715
|
+
type: "number",
|
|
716
|
+
description: 'Optional redirect status code, e.g. 301 / 302 / 307 / 308 (action="add" only).'
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
required: ["project"]
|
|
720
|
+
}
|
|
721
|
+
},
|
|
1248
722
|
{
|
|
1249
723
|
name: "vercel-logs",
|
|
1250
724
|
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.',
|
|
@@ -1274,7 +748,7 @@ var VERCEL_TOOLS = [
|
|
|
1274
748
|
},
|
|
1275
749
|
sinceMinutes: {
|
|
1276
750
|
type: "number",
|
|
1277
|
-
description:
|
|
751
|
+
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.`
|
|
1278
752
|
},
|
|
1279
753
|
limit: {
|
|
1280
754
|
type: "number",
|
|
@@ -1288,11 +762,14 @@ var VERCEL_TOOL_NAMES = new Set(VERCEL_TOOLS.map((t) => t.name));
|
|
|
1288
762
|
var VERCEL_TOOL_MODULE_MAP = {
|
|
1289
763
|
"vercel-projects": "ci_cd",
|
|
1290
764
|
"vercel-deployments": "ci_cd",
|
|
1291
|
-
"vercel-logs": "ci_cd"
|
|
765
|
+
"vercel-logs": "ci_cd",
|
|
766
|
+
"vercel-domains": "ci_cd"
|
|
1292
767
|
};
|
|
1293
|
-
async function vercelFetch(token, path) {
|
|
768
|
+
async function vercelFetch(token, path, init) {
|
|
1294
769
|
const res = await fetch(`${VERCEL_API}${path}`, {
|
|
1295
|
-
|
|
770
|
+
method: init?.method ?? "GET",
|
|
771
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
772
|
+
body: init?.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
1296
773
|
});
|
|
1297
774
|
const body = await res.text();
|
|
1298
775
|
let parsed = null;
|
|
@@ -1376,6 +853,14 @@ async function getDeploymentBuildEvents(token, deploymentId, limit) {
|
|
|
1376
853
|
}
|
|
1377
854
|
return { events, error: null };
|
|
1378
855
|
}
|
|
856
|
+
async function getDeploymentCreatedMs(token, deploymentId) {
|
|
857
|
+
const res = await vercelFetch(
|
|
858
|
+
token,
|
|
859
|
+
`/v13/deployments/${encodeURIComponent(deploymentId)}`
|
|
860
|
+
);
|
|
861
|
+
if (res.error || !res.data) return null;
|
|
862
|
+
return res.data.createdAt ?? res.data.created ?? null;
|
|
863
|
+
}
|
|
1379
864
|
async function getRuntimeLogs(token, projectIdOrName, deploymentId, limit, sinceMs) {
|
|
1380
865
|
const params = new URLSearchParams({
|
|
1381
866
|
limit: String(Math.min(Math.max(limit, 1), 1e3))
|
|
@@ -1416,6 +901,47 @@ async function getRuntimeLogs(token, projectIdOrName, deploymentId, limit, since
|
|
|
1416
901
|
}
|
|
1417
902
|
return { logs, error: null };
|
|
1418
903
|
}
|
|
904
|
+
async function listProjectDomains(token, projectId) {
|
|
905
|
+
const res = await vercelFetch(
|
|
906
|
+
token,
|
|
907
|
+
`/v9/projects/${encodeURIComponent(projectId)}/domains?limit=100`
|
|
908
|
+
);
|
|
909
|
+
if (res.error) return { domains: [], error: res.error };
|
|
910
|
+
return { domains: res.data?.domains ?? [], error: null };
|
|
911
|
+
}
|
|
912
|
+
async function addProjectDomain(token, projectId, body) {
|
|
913
|
+
const res = await vercelFetch(
|
|
914
|
+
token,
|
|
915
|
+
`/v10/projects/${encodeURIComponent(projectId)}/domains`,
|
|
916
|
+
{ method: "POST", body }
|
|
917
|
+
);
|
|
918
|
+
if (res.error) return { domain: null, error: res.error };
|
|
919
|
+
return { domain: res.data, error: null };
|
|
920
|
+
}
|
|
921
|
+
async function removeProjectDomain(token, projectId, domain) {
|
|
922
|
+
const res = await vercelFetch(
|
|
923
|
+
token,
|
|
924
|
+
`/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}`,
|
|
925
|
+
{ method: "DELETE" }
|
|
926
|
+
);
|
|
927
|
+
return { error: res.error };
|
|
928
|
+
}
|
|
929
|
+
async function getProjectDomain(token, projectId, domain) {
|
|
930
|
+
const res = await vercelFetch(
|
|
931
|
+
token,
|
|
932
|
+
`/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}`
|
|
933
|
+
);
|
|
934
|
+
if (res.error) return { domain: null, error: res.error };
|
|
935
|
+
return { domain: res.data, error: null };
|
|
936
|
+
}
|
|
937
|
+
async function getDomainConfig(token, domain) {
|
|
938
|
+
const res = await vercelFetch(
|
|
939
|
+
token,
|
|
940
|
+
`/v6/domains/${encodeURIComponent(domain)}/config`
|
|
941
|
+
);
|
|
942
|
+
if (res.error) return { config: null, error: res.error };
|
|
943
|
+
return { config: res.data, error: null };
|
|
944
|
+
}
|
|
1419
945
|
async function getVercelToken(deps) {
|
|
1420
946
|
const { data, error } = await deps.supabase.from("app_setting").select("vercel_token_encrypted").maybeSingle();
|
|
1421
947
|
if (error) throw new Error(`Could not read app_setting: ${error.message}`);
|
|
@@ -1511,6 +1037,42 @@ function formatWebhookHistory(rows) {
|
|
|
1511
1037
|
${"-".repeat(header.length)}
|
|
1512
1038
|
${lines.join("\n")}`;
|
|
1513
1039
|
}
|
|
1040
|
+
function formatDomainsTable(domains) {
|
|
1041
|
+
if (domains.length === 0) return "No domains attached to this project";
|
|
1042
|
+
const lines = domains.map((d) => {
|
|
1043
|
+
const verified = d.verified ? "yes" : "no";
|
|
1044
|
+
const branch = d.gitBranch ?? "";
|
|
1045
|
+
const redirect = d.redirect ? `${d.redirect}${d.redirectStatusCode ? ` (${d.redirectStatusCode})` : ""}` : "";
|
|
1046
|
+
return `${d.name.padEnd(45)} ${verified.padEnd(9)} ${branch.padEnd(20)} ${redirect.padEnd(35)} ${formatTimestamp(d.createdAt)}`;
|
|
1047
|
+
});
|
|
1048
|
+
const header = `${"DOMAIN".padEnd(45)} ${"VERIFIED".padEnd(9)} ${"GIT BRANCH".padEnd(20)} ${"REDIRECT".padEnd(35)} CREATED`;
|
|
1049
|
+
return `${header}
|
|
1050
|
+
${"-".repeat(header.length)}
|
|
1051
|
+
${lines.join("\n")}`;
|
|
1052
|
+
}
|
|
1053
|
+
function formatDomainStatus(domain, config) {
|
|
1054
|
+
const lines = [];
|
|
1055
|
+
lines.push(`Domain: ${domain.name}`);
|
|
1056
|
+
lines.push(`Verified: ${domain.verified ? "yes" : "no"}`);
|
|
1057
|
+
if (config) {
|
|
1058
|
+
lines.push(`Misconfigured: ${config.misconfigured ? "yes" : "no"}`);
|
|
1059
|
+
if (config.configuredBy) lines.push(`Configured by: ${config.configuredBy}`);
|
|
1060
|
+
}
|
|
1061
|
+
if (domain.gitBranch) lines.push(`Git branch: ${domain.gitBranch}`);
|
|
1062
|
+
if (domain.redirect) {
|
|
1063
|
+
lines.push(
|
|
1064
|
+
`Redirect: ${domain.redirect}${domain.redirectStatusCode ? ` (${domain.redirectStatusCode})` : ""}`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
if (domain.verification && domain.verification.length > 0) {
|
|
1068
|
+
lines.push("");
|
|
1069
|
+
lines.push("Required DNS records to verify ownership:");
|
|
1070
|
+
for (const v of domain.verification) {
|
|
1071
|
+
lines.push(` ${v.type.padEnd(6)} ${v.domain.padEnd(45)} ${v.value}${v.reason ? ` // ${v.reason}` : ""}`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return lines.join("\n");
|
|
1075
|
+
}
|
|
1514
1076
|
async function handleVercelTool(name, args2, deps) {
|
|
1515
1077
|
switch (name) {
|
|
1516
1078
|
case "vercel-projects": {
|
|
@@ -1561,9 +1123,33 @@ async function handleVercelTool(name, args2, deps) {
|
|
|
1561
1123
|
const projectInput = String(args2.project);
|
|
1562
1124
|
const deploymentId = String(args2.deploymentId);
|
|
1563
1125
|
const limit = Math.min(Math.max(Number(args2.limit) || 200, 1), 1e3);
|
|
1564
|
-
const
|
|
1565
|
-
const
|
|
1566
|
-
const projectId = await
|
|
1126
|
+
const sinceMinutesRaw = Number(args2.sinceMinutes);
|
|
1127
|
+
const sinceExplicit = Number.isFinite(sinceMinutesRaw) && sinceMinutesRaw > 0;
|
|
1128
|
+
const [projectId, deploymentCreatedMs] = await Promise.all([
|
|
1129
|
+
resolveProjectId(token, projectInput),
|
|
1130
|
+
sinceExplicit ? Promise.resolve(null) : getDeploymentCreatedMs(token, deploymentId)
|
|
1131
|
+
]);
|
|
1132
|
+
const maxWindowMin = 7 * 24 * 60;
|
|
1133
|
+
let sinceMs;
|
|
1134
|
+
let windowNote = "";
|
|
1135
|
+
if (sinceExplicit) {
|
|
1136
|
+
const capped = Math.min(sinceMinutesRaw, maxWindowMin);
|
|
1137
|
+
sinceMs = Date.now() - capped * 6e4;
|
|
1138
|
+
windowNote = `window: last ${capped} min (caller-specified)`;
|
|
1139
|
+
} else if (deploymentCreatedMs) {
|
|
1140
|
+
const bufferMs = 5 * 6e4;
|
|
1141
|
+
sinceMs = deploymentCreatedMs - bufferMs;
|
|
1142
|
+
const ageMin = Math.max(1, Math.round((Date.now() - sinceMs) / 6e4));
|
|
1143
|
+
if (ageMin > maxWindowMin) {
|
|
1144
|
+
sinceMs = Date.now() - maxWindowMin * 6e4;
|
|
1145
|
+
windowNote = `window: capped to ${maxWindowMin} min (deployment is older than 7 days)`;
|
|
1146
|
+
} else {
|
|
1147
|
+
windowNote = `window: auto ${ageMin} min from deployment createdAt - 5 min buffer`;
|
|
1148
|
+
}
|
|
1149
|
+
} else {
|
|
1150
|
+
sinceMs = Date.now() - 60 * 6e4;
|
|
1151
|
+
windowNote = "window: last 60 min (deployment metadata unavailable, used fallback)";
|
|
1152
|
+
}
|
|
1567
1153
|
const { logs, error } = await getRuntimeLogs(
|
|
1568
1154
|
token,
|
|
1569
1155
|
projectId,
|
|
@@ -1575,7 +1161,10 @@ async function handleVercelTool(name, args2, deps) {
|
|
|
1575
1161
|
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.' : "";
|
|
1576
1162
|
return { content: [{ type: "text", text: `Error: ${error}${hint}` }] };
|
|
1577
1163
|
}
|
|
1578
|
-
|
|
1164
|
+
const text = `${formatRuntimeLogs(logs)}
|
|
1165
|
+
|
|
1166
|
+
[${windowNote}]`;
|
|
1167
|
+
return { content: [{ type: "text", text }] };
|
|
1579
1168
|
}
|
|
1580
1169
|
if (kind === "webhooks") {
|
|
1581
1170
|
const limit = Math.min(Math.max(Number(args2.limit) || 25, 1), 200);
|
|
@@ -1596,6 +1185,67 @@ async function handleVercelTool(name, args2, deps) {
|
|
|
1596
1185
|
]
|
|
1597
1186
|
};
|
|
1598
1187
|
}
|
|
1188
|
+
case "vercel-domains": {
|
|
1189
|
+
const action = (args2.action ? String(args2.action) : "list").toLowerCase();
|
|
1190
|
+
if (!args2.project) {
|
|
1191
|
+
return { content: [{ type: "text", text: 'Error: vercel-domains requires "project".' }] };
|
|
1192
|
+
}
|
|
1193
|
+
const token = await getVercelToken(deps);
|
|
1194
|
+
const projectInput = String(args2.project);
|
|
1195
|
+
const projectId = await resolveProjectId(token, projectInput);
|
|
1196
|
+
if (action === "list") {
|
|
1197
|
+
const { domains, error } = await listProjectDomains(token, projectId);
|
|
1198
|
+
if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
|
|
1199
|
+
return { content: [{ type: "text", text: formatDomainsTable(domains) }] };
|
|
1200
|
+
}
|
|
1201
|
+
if (action === "add") {
|
|
1202
|
+
if (!args2.domain) {
|
|
1203
|
+
return { content: [{ type: "text", text: 'Error: action="add" requires "domain".' }] };
|
|
1204
|
+
}
|
|
1205
|
+
const body = { name: String(args2.domain) };
|
|
1206
|
+
if (args2.gitBranch) body.gitBranch = String(args2.gitBranch);
|
|
1207
|
+
if (args2.redirect) body.redirect = String(args2.redirect);
|
|
1208
|
+
if (args2.redirectStatusCode) body.redirectStatusCode = Number(args2.redirectStatusCode);
|
|
1209
|
+
const { domain, error } = await addProjectDomain(token, projectId, body);
|
|
1210
|
+
if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
|
|
1211
|
+
if (!domain) {
|
|
1212
|
+
return { content: [{ type: "text", text: `Domain ${body.name} added (no detail returned).` }] };
|
|
1213
|
+
}
|
|
1214
|
+
const { config } = await getDomainConfig(token, domain.name);
|
|
1215
|
+
const status = formatDomainStatus(domain, config);
|
|
1216
|
+
const headline = domain.verified ? `Domain ${domain.name} added and verified.` : `Domain ${domain.name} added. DNS verification still pending.`;
|
|
1217
|
+
return { content: [{ type: "text", text: `${headline}
|
|
1218
|
+
|
|
1219
|
+
${status}` }] };
|
|
1220
|
+
}
|
|
1221
|
+
if (action === "verify") {
|
|
1222
|
+
if (!args2.domain) {
|
|
1223
|
+
return { content: [{ type: "text", text: 'Error: action="verify" requires "domain".' }] };
|
|
1224
|
+
}
|
|
1225
|
+
const domainName = String(args2.domain);
|
|
1226
|
+
const { domain, error } = await getProjectDomain(token, projectId, domainName);
|
|
1227
|
+
if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
|
|
1228
|
+
if (!domain) {
|
|
1229
|
+
return { content: [{ type: "text", text: `Domain ${domainName} not found on this project.` }] };
|
|
1230
|
+
}
|
|
1231
|
+
const { config } = await getDomainConfig(token, domainName);
|
|
1232
|
+
return { content: [{ type: "text", text: formatDomainStatus(domain, config) }] };
|
|
1233
|
+
}
|
|
1234
|
+
if (action === "remove") {
|
|
1235
|
+
if (!args2.domain) {
|
|
1236
|
+
return { content: [{ type: "text", text: 'Error: action="remove" requires "domain".' }] };
|
|
1237
|
+
}
|
|
1238
|
+
const domainName = String(args2.domain);
|
|
1239
|
+
const { error } = await removeProjectDomain(token, projectId, domainName);
|
|
1240
|
+
if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
|
|
1241
|
+
return { content: [{ type: "text", text: `Domain ${domainName} removed from project.` }] };
|
|
1242
|
+
}
|
|
1243
|
+
return {
|
|
1244
|
+
content: [
|
|
1245
|
+
{ type: "text", text: `Error: unknown action "${action}". Use list, add, verify, or remove.` }
|
|
1246
|
+
]
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1599
1249
|
default:
|
|
1600
1250
|
return { content: [{ type: "text", text: `Unknown vercel tool: ${name}` }] };
|
|
1601
1251
|
}
|
|
@@ -1711,8 +1361,7 @@ var MODULE_KEYS = [
|
|
|
1711
1361
|
"wiki",
|
|
1712
1362
|
"ci_cd",
|
|
1713
1363
|
"domains",
|
|
1714
|
-
"settings"
|
|
1715
|
-
"agent_reporting"
|
|
1364
|
+
"settings"
|
|
1716
1365
|
];
|
|
1717
1366
|
var FULL_PERMISSIONS = {
|
|
1718
1367
|
modules: Object.fromEntries(MODULE_KEYS.map((k) => [k, true])),
|
|
@@ -1763,7 +1412,6 @@ var TOOL_MODULE_MAP = {
|
|
|
1763
1412
|
"wait-for": "ssh_servers",
|
|
1764
1413
|
"db-discover": "ssh_servers",
|
|
1765
1414
|
"db-tables": "ssh_servers",
|
|
1766
|
-
"db-describe": "ssh_servers",
|
|
1767
1415
|
"db-query": "ssh_servers",
|
|
1768
1416
|
"cache-purge": "ssh_servers",
|
|
1769
1417
|
"env-list": "ci_cd",
|
|
@@ -1775,7 +1423,6 @@ var TOOL_MODULE_MAP = {
|
|
|
1775
1423
|
"dns-update": "domains",
|
|
1776
1424
|
"dns-delete": "domains",
|
|
1777
1425
|
...TRIGGER_TOOL_MODULE_MAP,
|
|
1778
|
-
...AGENT_TOOL_MODULE_MAP,
|
|
1779
1426
|
...VERCEL_TOOL_MODULE_MAP
|
|
1780
1427
|
};
|
|
1781
1428
|
var authContext = null;
|
|
@@ -3724,6 +3371,42 @@ var BLOCKED_SQL_PATTERNS = [
|
|
|
3724
3371
|
/\bALTER\s+TABLE\s+\w+\s+DROP\b/i,
|
|
3725
3372
|
/\bDELETE\s+FROM\s+\w+\s*$/i
|
|
3726
3373
|
];
|
|
3374
|
+
function buildDescribeSql(target, engine) {
|
|
3375
|
+
if (target === "*" || target === "") {
|
|
3376
|
+
if (engine === "postgres") {
|
|
3377
|
+
return "SELECT schemaname, tablename FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema') ORDER BY schemaname, tablename";
|
|
3378
|
+
}
|
|
3379
|
+
if (engine === "mssql") {
|
|
3380
|
+
return "SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' ORDER BY TABLE_SCHEMA, TABLE_NAME";
|
|
3381
|
+
}
|
|
3382
|
+
return "SHOW TABLES";
|
|
3383
|
+
}
|
|
3384
|
+
const safe = target.replace(/[^a-zA-Z0-9_]/g, "");
|
|
3385
|
+
if (!safe) throw new Error(`Invalid describe target: ${target}`);
|
|
3386
|
+
if (engine === "postgres") {
|
|
3387
|
+
return `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name='${safe}' ORDER BY ordinal_position;
|
|
3388
|
+
SELECT indexname, indexdef FROM pg_indexes WHERE tablename='${safe}'`;
|
|
3389
|
+
}
|
|
3390
|
+
if (engine === "mssql") {
|
|
3391
|
+
return `EXEC sp_help '${safe}'`;
|
|
3392
|
+
}
|
|
3393
|
+
return `DESCRIBE \`${safe}\`; SHOW INDEX FROM \`${safe}\``;
|
|
3394
|
+
}
|
|
3395
|
+
function formatDbQueryFooter(output, appliedLimit, maxRows, explainMode) {
|
|
3396
|
+
if (explainMode) {
|
|
3397
|
+
return "\n\n[explain] Plan returned, no rows executed.";
|
|
3398
|
+
}
|
|
3399
|
+
if (!appliedLimit) return "";
|
|
3400
|
+
const rows = output.split("\n").filter((l) => l.trim() && !/^\s*[-+|]/.test(l)).length;
|
|
3401
|
+
if (rows > maxRows) {
|
|
3402
|
+
return `
|
|
3403
|
+
|
|
3404
|
+
[truncated] auto-LIMIT ${maxRows} hit \u2014 more rows available. Refine WHERE / ORDER BY or raise maxRows (max 10000).`;
|
|
3405
|
+
}
|
|
3406
|
+
return `
|
|
3407
|
+
|
|
3408
|
+
[ok] returned ${rows} row(s), under auto-LIMIT ${maxRows}.`;
|
|
3409
|
+
}
|
|
3727
3410
|
function assertSafeSql(query) {
|
|
3728
3411
|
const trimmed = query.trim();
|
|
3729
3412
|
for (const pattern of BLOCKED_SQL_PATTERNS) {
|
|
@@ -3795,6 +3478,36 @@ function requireMijnhostApiKey() {
|
|
|
3795
3478
|
}
|
|
3796
3479
|
return mijnhostApiKey;
|
|
3797
3480
|
}
|
|
3481
|
+
function formatDnsDiff(domain, before, after, change) {
|
|
3482
|
+
const fmt = (r) => `${r.type.padEnd(6)} ${r.name.padEnd(30)} ttl=${String(r.ttl).padEnd(5)} ${r.value}`;
|
|
3483
|
+
const lines = [];
|
|
3484
|
+
lines.push(`[dryRun] ${change.verb.toUpperCase()} on ${domain} \u2014 no changes applied.`);
|
|
3485
|
+
lines.push(`Records before: ${before.length} \u2192 after: ${after.length}`);
|
|
3486
|
+
if (change.removed?.length) {
|
|
3487
|
+
lines.push("", "REMOVED:");
|
|
3488
|
+
for (const r of change.removed) lines.push(` - ${fmt(r)}`);
|
|
3489
|
+
}
|
|
3490
|
+
if (change.added?.length) {
|
|
3491
|
+
lines.push("", "ADDED:");
|
|
3492
|
+
for (const r of change.added) lines.push(` + ${fmt(r)}`);
|
|
3493
|
+
}
|
|
3494
|
+
const sensitive = [
|
|
3495
|
+
...change.added ?? [],
|
|
3496
|
+
...change.removed ?? []
|
|
3497
|
+
].filter((r) => {
|
|
3498
|
+
if (r.type === "MX") return true;
|
|
3499
|
+
if (r.type === "TXT") {
|
|
3500
|
+
const v = r.value.toLowerCase();
|
|
3501
|
+
return v.includes("v=spf1") || v.includes("v=dmarc1") || v.includes("_domainkey") || r.name.toLowerCase().includes("_domainkey");
|
|
3502
|
+
}
|
|
3503
|
+
return false;
|
|
3504
|
+
});
|
|
3505
|
+
if (sensitive.length > 0) {
|
|
3506
|
+
lines.push("", "! WARNING: touches mail-auth records (MX / SPF / DMARC / DKIM). Double-check before re-running without dryRun.");
|
|
3507
|
+
}
|
|
3508
|
+
lines.push("", "Re-run without `dryRun: true` to apply.");
|
|
3509
|
+
return lines.join("\n");
|
|
3510
|
+
}
|
|
3798
3511
|
async function mijnhostFetch(path, options = {}) {
|
|
3799
3512
|
const key = requireMijnhostApiKey();
|
|
3800
3513
|
const res = await fetch(`${MIJNHOST_BASE_URL}${path}`, {
|
|
@@ -4035,35 +3748,25 @@ var TOOLS = [
|
|
|
4035
3748
|
required: ["serverId", "sitePath"]
|
|
4036
3749
|
}
|
|
4037
3750
|
},
|
|
4038
|
-
{
|
|
4039
|
-
name: "db-describe",
|
|
4040
|
-
description: "Show the structure of a database table (columns, types, keys, defaults). Credentials are auto-discovered from site config files.",
|
|
4041
|
-
inputSchema: {
|
|
4042
|
-
type: "object",
|
|
4043
|
-
properties: {
|
|
4044
|
-
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
4045
|
-
sitePath: { type: "string", description: "Site root path (e.g. /var/www/example.com)" },
|
|
4046
|
-
table: { type: "string", description: "Table name" }
|
|
4047
|
-
},
|
|
4048
|
-
required: ["serverId", "sitePath", "table"]
|
|
4049
|
-
}
|
|
4050
|
-
},
|
|
4051
3751
|
{
|
|
4052
3752
|
name: "db-query",
|
|
4053
|
-
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.',
|
|
3753
|
+
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.',
|
|
4054
3754
|
inputSchema: {
|
|
4055
3755
|
type: "object",
|
|
4056
3756
|
properties: {
|
|
4057
3757
|
serverId: { type: "string", description: "UUID of the SSH server" },
|
|
4058
|
-
query: { type: "string", description: "SQL query to execute" },
|
|
3758
|
+
query: { type: "string", description: "SQL query to execute (ignored when describe is set)" },
|
|
3759
|
+
describe: { type: "string", description: 'Schema introspection shortcut. Pass a table name for columns + indexes, or "*" to list all tables. Works for mysql / postgres / mssql.' },
|
|
4059
3760
|
sitePath: { type: "string", description: "Site root path (e.g. /var/www/example.com). Used only with engine=mysql autodiscover." },
|
|
4060
3761
|
containerName: { type: "string", description: 'Docker container running the DB server (e.g. "supabase-db", "trigger-postgres"). Activates direct-query mode.' },
|
|
4061
3762
|
engine: { type: "string", enum: ["mysql", "postgres", "mssql"], description: 'DB engine. Defaults to "mysql" with sitePath, "postgres" with containerName.' },
|
|
4062
3763
|
dbName: { type: "string", description: 'Database name (containerName mode). Defaults: postgres \u2192 "postgres", mysql \u2192 server default, mssql \u2192 server default.' },
|
|
4063
3764
|
dbUser: { type: "string", description: 'Database user (containerName mode). Defaults: postgres \u2192 "postgres", mysql \u2192 "root", mssql \u2192 "sa".' },
|
|
4064
|
-
dbPass: { type: "string", description: "Database password (containerName mode). Required for mssql; optional for mysql; postgres uses trust auth by default." }
|
|
3765
|
+
dbPass: { type: "string", description: "Database password (containerName mode). Required for mssql; optional for mysql; postgres uses trust auth by default." },
|
|
3766
|
+
maxRows: { type: "number", description: "Auto-inject LIMIT for unbounded SELECTs. Default 1000, max 10000, set 0 to disable." },
|
|
3767
|
+
explain: { type: "boolean", description: "Prepend EXPLAIN (postgres/mysql) or SHOWPLAN (mssql) instead of executing. Useful for slow-query diagnosis." }
|
|
4065
3768
|
},
|
|
4066
|
-
required: ["serverId"
|
|
3769
|
+
required: ["serverId"]
|
|
4067
3770
|
}
|
|
4068
3771
|
},
|
|
4069
3772
|
{
|
|
@@ -4118,8 +3821,15 @@ var TOOLS = [
|
|
|
4118
3821
|
// ----- Domains (mijn.host) -----
|
|
4119
3822
|
{
|
|
4120
3823
|
name: "domain-list",
|
|
4121
|
-
description: "List all domains from the mijn.host account. Returns domain name, status, renewal date, and tags. Requires MIJNHOST_API_KEY.",
|
|
4122
|
-
inputSchema: {
|
|
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: 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.",
|
|
3825
|
+
inputSchema: {
|
|
3826
|
+
type: "object",
|
|
3827
|
+
properties: {
|
|
3828
|
+
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." },
|
|
3829
|
+
concurrency: { type: "number", description: "Max concurrent DNS lookups when details=true (default 8, max 20)." }
|
|
3830
|
+
},
|
|
3831
|
+
required: []
|
|
3832
|
+
}
|
|
4123
3833
|
},
|
|
4124
3834
|
{
|
|
4125
3835
|
name: "dns-list",
|
|
@@ -4134,7 +3844,7 @@ var TOOLS = [
|
|
|
4134
3844
|
},
|
|
4135
3845
|
{
|
|
4136
3846
|
name: "dns-create",
|
|
4137
|
-
description: "Add a new DNS record to a domain. Uses PATCH to add without replacing existing records.",
|
|
3847
|
+
description: "Add a new DNS record to a domain. Uses PATCH to add without replacing existing records.\n\nPass `dryRun: true` to preview the proposed change (full before/after diff) without calling the mijn.host PUT \u2014 recommended before touching MX, SPF, DKIM, or DMARC records.",
|
|
4138
3848
|
inputSchema: {
|
|
4139
3849
|
type: "object",
|
|
4140
3850
|
properties: {
|
|
@@ -4142,14 +3852,15 @@ var TOOLS = [
|
|
|
4142
3852
|
type: { type: "string", description: "Record type: A, AAAA, CNAME, MX, TXT, NS, SRV, CAA, or TLSA" },
|
|
4143
3853
|
name: { type: "string", description: "Record name (e.g. @ or subdomain)" },
|
|
4144
3854
|
value: { type: "string", description: "Record value (e.g. IP address, hostname)" },
|
|
4145
|
-
ttl: { type: "number", description: "TTL in seconds (min 60, default 3600)" }
|
|
3855
|
+
ttl: { type: "number", description: "TTL in seconds (min 60, default 3600)" },
|
|
3856
|
+
dryRun: { type: "boolean", description: "Preview the change (returns proposed diff) without applying." }
|
|
4146
3857
|
},
|
|
4147
3858
|
required: ["domain", "type", "name", "value"]
|
|
4148
3859
|
}
|
|
4149
3860
|
},
|
|
4150
3861
|
{
|
|
4151
3862
|
name: "dns-update",
|
|
4152
|
-
description: "Update an existing DNS record. Identifies the record by type+name+oldValue, then replaces it with new values via PATCH.",
|
|
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.",
|
|
4153
3864
|
inputSchema: {
|
|
4154
3865
|
type: "object",
|
|
4155
3866
|
properties: {
|
|
@@ -4158,33 +3869,33 @@ var TOOLS = [
|
|
|
4158
3869
|
name: { type: "string", description: "Record name" },
|
|
4159
3870
|
oldValue: { type: "string", description: "Current value of the record to update" },
|
|
4160
3871
|
newValue: { type: "string", description: "New value for the record" },
|
|
4161
|
-
ttl: { type: "number", description: "New TTL in seconds (min 60)" }
|
|
3872
|
+
ttl: { type: "number", description: "New TTL in seconds (min 60)" },
|
|
3873
|
+
dryRun: { type: "boolean", description: "Preview the change (returns proposed diff) without applying." }
|
|
4162
3874
|
},
|
|
4163
3875
|
required: ["domain", "type", "name", "oldValue", "newValue"]
|
|
4164
3876
|
}
|
|
4165
3877
|
},
|
|
4166
3878
|
{
|
|
4167
3879
|
name: "dns-delete",
|
|
4168
|
-
description: "Delete a DNS record by type, name, and value. Fetches all records, removes the matching one, then replaces the full set.",
|
|
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.",
|
|
4169
3881
|
inputSchema: {
|
|
4170
3882
|
type: "object",
|
|
4171
3883
|
properties: {
|
|
4172
3884
|
domain: { type: "string", description: "Domain name (e.g. example.com)" },
|
|
4173
3885
|
type: { type: "string", description: "Record type to delete" },
|
|
4174
3886
|
name: { type: "string", description: "Record name to delete" },
|
|
4175
|
-
value: { type: "string", description: "Record value to delete (must match exactly)" }
|
|
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." }
|
|
4176
3889
|
},
|
|
4177
3890
|
required: ["domain", "type", "name", "value"]
|
|
4178
3891
|
}
|
|
4179
3892
|
},
|
|
4180
3893
|
// ----- Trigger.dev -----
|
|
4181
3894
|
...TRIGGER_TOOLS,
|
|
4182
|
-
// ----- Agent Reporting -----
|
|
4183
|
-
...AGENT_TOOLS,
|
|
4184
3895
|
// ----- Vercel -----
|
|
4185
3896
|
...VERCEL_TOOLS
|
|
4186
3897
|
];
|
|
4187
|
-
var MCP_VERSION = "
|
|
3898
|
+
var MCP_VERSION = "5.0.1";
|
|
4188
3899
|
async function handleListTools() {
|
|
4189
3900
|
if (!authContext) return { tools: TOOLS };
|
|
4190
3901
|
const accessible = TOOLS.filter((tool) => {
|
|
@@ -5148,29 +4859,46 @@ ${trail.join("\n")}` }] };
|
|
|
5148
4859
|
const output = await execSiteMysql(conn, String(a.sitePath), sql, proxy);
|
|
5149
4860
|
return { content: [{ type: "text", text: output || "No tables found" }] };
|
|
5150
4861
|
}
|
|
5151
|
-
case "db-describe": {
|
|
5152
|
-
const table = String(a.table).replace(/[^a-zA-Z0-9_]/g, "");
|
|
5153
|
-
const { conn, proxy } = await getServerConnection(String(a.serverId));
|
|
5154
|
-
const output = await execSiteMysql(
|
|
5155
|
-
conn,
|
|
5156
|
-
String(a.sitePath),
|
|
5157
|
-
`DESCRIBE \`${table}\`; SHOW INDEX FROM \`${table}\``,
|
|
5158
|
-
proxy
|
|
5159
|
-
);
|
|
5160
|
-
return { content: [{ type: "text", text: output }] };
|
|
5161
|
-
}
|
|
5162
4862
|
case "db-query": {
|
|
5163
|
-
const query = String(a.query).trim();
|
|
5164
|
-
if (!query) return { content: [{ type: "text", text: "Error: query is required" }] };
|
|
5165
|
-
assertSafeSql(query);
|
|
5166
4863
|
const containerName = typeof a.containerName === "string" && a.containerName ? a.containerName.replace(/[^a-zA-Z0-9._-]/g, "") : "";
|
|
5167
4864
|
const explicitEngine = a.engine === "mysql" || a.engine === "postgres" || a.engine === "mssql" ? a.engine : null;
|
|
5168
4865
|
const engine = explicitEngine || (containerName ? "postgres" : "mysql");
|
|
4866
|
+
const describeArg = typeof a.describe === "string" ? a.describe.trim() : "";
|
|
4867
|
+
const explainMode = a.explain === true;
|
|
4868
|
+
const maxRowsRaw = a.maxRows === void 0 ? 1e3 : Number(a.maxRows);
|
|
4869
|
+
const maxRows = Math.min(Math.max(Number.isFinite(maxRowsRaw) ? maxRowsRaw : 0, 0), 1e4);
|
|
4870
|
+
let rawQuery;
|
|
4871
|
+
if (describeArg) {
|
|
4872
|
+
rawQuery = buildDescribeSql(describeArg, engine);
|
|
4873
|
+
} else {
|
|
4874
|
+
rawQuery = String(a.query ?? "").trim();
|
|
4875
|
+
if (!rawQuery) return { content: [{ type: "text", text: "Error: query (or describe) is required" }] };
|
|
4876
|
+
}
|
|
4877
|
+
assertSafeSql(rawQuery);
|
|
4878
|
+
let query = rawQuery.replace(/;\s*$/, "");
|
|
4879
|
+
let appliedLimit = false;
|
|
4880
|
+
if (explainMode && !describeArg) {
|
|
4881
|
+
if (engine === "mssql") {
|
|
4882
|
+
query = `SET SHOWPLAN_TEXT ON;
|
|
4883
|
+
${query}`;
|
|
4884
|
+
} else {
|
|
4885
|
+
query = `EXPLAIN ${query}`;
|
|
4886
|
+
}
|
|
4887
|
+
} else if (maxRows > 0 && !describeArg) {
|
|
4888
|
+
const isSelect = /^\s*(with\b[\s\S]+?\bselect\b|select\b)/i.test(query);
|
|
4889
|
+
const alreadyLimited = /\blimit\s+\d+\b/i.test(query) || /\btop\s*\(?\s*\d+/i.test(query);
|
|
4890
|
+
if (isSelect && !alreadyLimited && engine !== "mssql") {
|
|
4891
|
+
query = `${query}
|
|
4892
|
+
LIMIT ${maxRows + 1}`;
|
|
4893
|
+
appliedLimit = true;
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
5169
4896
|
if (!containerName && engine === "mysql") {
|
|
5170
4897
|
if (!a.sitePath) return { content: [{ type: "text", text: "Error: sitePath is required for mysql autodiscover (or pass containerName + engine for direct DB queries)" }] };
|
|
5171
4898
|
const { conn: conn2, proxy: proxy2 } = await getServerConnection(String(a.serverId));
|
|
5172
4899
|
const output2 = await execSiteMysql(conn2, String(a.sitePath), query, proxy2);
|
|
5173
|
-
|
|
4900
|
+
const footer2 = formatDbQueryFooter(output2, appliedLimit, maxRows, explainMode);
|
|
4901
|
+
return { content: [{ type: "text", text: (output2 || "Query executed successfully (no output)") + footer2 }] };
|
|
5174
4902
|
}
|
|
5175
4903
|
if (!containerName) {
|
|
5176
4904
|
return { content: [{ type: "text", text: `Error: engine=${engine} requires containerName (the docker container running the DB server, e.g. "supabase-db" or "trigger-postgres")` }] };
|
|
@@ -5209,7 +4937,8 @@ GO
|
|
|
5209
4937
|
if (result.exitCode !== 0 && !result.stdout) {
|
|
5210
4938
|
return { content: [{ type: "text", text: `Error (exit ${result.exitCode}, ${engine}): ${output}` }] };
|
|
5211
4939
|
}
|
|
5212
|
-
|
|
4940
|
+
const footer = formatDbQueryFooter(output, appliedLimit, maxRows, explainMode);
|
|
4941
|
+
return { content: [{ type: "text", text: output + footer }] };
|
|
5213
4942
|
}
|
|
5214
4943
|
// ----- Env Config -----
|
|
5215
4944
|
case "env-list": {
|
|
@@ -5398,13 +5127,68 @@ echo -e "$R"
|
|
|
5398
5127
|
if (!domains.length) {
|
|
5399
5128
|
return { content: [{ type: "text", text: "No domains found" }] };
|
|
5400
5129
|
}
|
|
5130
|
+
const details = a.details === true;
|
|
5131
|
+
if (!details) {
|
|
5132
|
+
const lines2 = domains.map((d) => {
|
|
5133
|
+
const tags = d.tags?.length ? ` [${d.tags.join(", ")}]` : "";
|
|
5134
|
+
return `${d.domain} status=${d.status} renewal=${d.renewal_date}${tags}`;
|
|
5135
|
+
});
|
|
5136
|
+
return { content: [{ type: "text", text: `${domains.length} domain(s):
|
|
5137
|
+
|
|
5138
|
+
${lines2.join("\n")}` }] };
|
|
5139
|
+
}
|
|
5140
|
+
const concurrency = Math.min(Math.max(Number(a.concurrency) || 8, 1), 20);
|
|
5141
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
5142
|
+
const skipStatuses = /* @__PURE__ */ new Set(["expired", "redemptionperiod", "pendingdelete", "inactive"]);
|
|
5143
|
+
const activeDomains = domains.filter((d) => !skipStatuses.has((d.status || "").toLowerCase()));
|
|
5144
|
+
async function fetchSummary(domain) {
|
|
5145
|
+
try {
|
|
5146
|
+
const r = await mijnhostFetch(
|
|
5147
|
+
`/domains/${encodeURIComponent(domain)}/dns`
|
|
5148
|
+
);
|
|
5149
|
+
const recs = r.data.records || [];
|
|
5150
|
+
const ns = recs.filter((x) => x.type === "NS").map((x) => x.value).sort();
|
|
5151
|
+
const mx = recs.filter((x) => x.type === "MX").map((x) => x.value).sort();
|
|
5152
|
+
const hasSpf = recs.some((x) => x.type === "TXT" && x.value.toLowerCase().includes("v=spf1"));
|
|
5153
|
+
const hasDmarc = recs.some(
|
|
5154
|
+
(x) => x.type === "TXT" && (x.name.toLowerCase().startsWith("_dmarc") || x.value.toLowerCase().includes("v=dmarc1"))
|
|
5155
|
+
);
|
|
5156
|
+
return { ns, mx, hasSpf, hasDmarc };
|
|
5157
|
+
} catch (err) {
|
|
5158
|
+
return { ns: [], mx: [], hasSpf: false, hasDmarc: false, error: err instanceof Error ? err.message : String(err) };
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
const queue = [...activeDomains];
|
|
5162
|
+
async function worker() {
|
|
5163
|
+
while (queue.length > 0) {
|
|
5164
|
+
const d = queue.shift();
|
|
5165
|
+
if (!d) return;
|
|
5166
|
+
summaries.set(d.domain, await fetchSummary(d.domain));
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, worker));
|
|
5401
5170
|
const lines = domains.map((d) => {
|
|
5402
5171
|
const tags = d.tags?.length ? ` [${d.tags.join(", ")}]` : "";
|
|
5403
|
-
|
|
5172
|
+
const head = `${d.domain} status=${d.status} expires=${d.renewal_date}${tags}`;
|
|
5173
|
+
const s = summaries.get(d.domain);
|
|
5174
|
+
if (!s) return head;
|
|
5175
|
+
if (s.error) return `${head}
|
|
5176
|
+
dns: error: ${s.error}`;
|
|
5177
|
+
const ns = s.ns.length ? s.ns.join(", ") : "(none)";
|
|
5178
|
+
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
|
+
return `${head}
|
|
5181
|
+
ns: ${ns}
|
|
5182
|
+
${mail}`;
|
|
5404
5183
|
});
|
|
5405
|
-
return {
|
|
5184
|
+
return {
|
|
5185
|
+
content: [{
|
|
5186
|
+
type: "text",
|
|
5187
|
+
text: `${domains.length} domain(s) (${activeDomains.length} with DNS lookup):
|
|
5406
5188
|
|
|
5407
|
-
${lines.join("\n")}`
|
|
5189
|
+
${lines.join("\n")}`
|
|
5190
|
+
}]
|
|
5191
|
+
};
|
|
5408
5192
|
}
|
|
5409
5193
|
case "dns-list": {
|
|
5410
5194
|
const domain = String(a.domain);
|
|
@@ -5433,13 +5217,22 @@ ${lines.join("\n")}` }] };
|
|
|
5433
5217
|
const dnsName = String(a.name);
|
|
5434
5218
|
const value = String(a.value);
|
|
5435
5219
|
const ttl = Number(a.ttl) || 3600;
|
|
5220
|
+
const dryRun = a.dryRun === true;
|
|
5436
5221
|
if (!domain || !type || !dnsName || !value) {
|
|
5437
5222
|
throw new Error("domain, type, name, and value are required");
|
|
5438
5223
|
}
|
|
5439
5224
|
const current = await mijnhostFetch(
|
|
5440
5225
|
`/domains/${encodeURIComponent(domain)}/dns`
|
|
5441
5226
|
);
|
|
5442
|
-
const
|
|
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
|
+
}
|
|
5443
5236
|
await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
|
|
5444
5237
|
method: "PUT",
|
|
5445
5238
|
body: JSON.stringify({ records })
|
|
@@ -5453,6 +5246,7 @@ ${lines.join("\n")}` }] };
|
|
|
5453
5246
|
const oldValue = String(a.oldValue);
|
|
5454
5247
|
const newValue = String(a.newValue);
|
|
5455
5248
|
const ttl = Number(a.ttl) || void 0;
|
|
5249
|
+
const dryRun = a.dryRun === true;
|
|
5456
5250
|
if (!domain || !type || !dnsName || !oldValue || !newValue) {
|
|
5457
5251
|
throw new Error("domain, type, name, oldValue, and newValue are required");
|
|
5458
5252
|
}
|
|
@@ -5467,12 +5261,22 @@ ${lines.join("\n")}` }] };
|
|
|
5467
5261
|
}
|
|
5468
5262
|
const updated = [...current.data.records];
|
|
5469
5263
|
const existingTtl = updated[idx].ttl;
|
|
5470
|
-
updated[idx]
|
|
5264
|
+
const before = updated[idx];
|
|
5265
|
+
const after = {
|
|
5471
5266
|
type,
|
|
5472
5267
|
name: dnsName,
|
|
5473
5268
|
value: newValue,
|
|
5474
5269
|
ttl: ttl ?? existingTtl
|
|
5475
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 }] };
|
|
5279
|
+
}
|
|
5476
5280
|
await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
|
|
5477
5281
|
method: "PUT",
|
|
5478
5282
|
body: JSON.stringify({ records: updated })
|
|
@@ -5484,6 +5288,7 @@ ${lines.join("\n")}` }] };
|
|
|
5484
5288
|
const type = String(a.type).toUpperCase();
|
|
5485
5289
|
const dnsName = String(a.name);
|
|
5486
5290
|
const value = String(a.value);
|
|
5291
|
+
const dryRun = a.dryRun === true;
|
|
5487
5292
|
if (!domain || !type || !dnsName || !value) {
|
|
5488
5293
|
throw new Error("domain, type, name, and value are required");
|
|
5489
5294
|
}
|
|
@@ -5491,12 +5296,22 @@ ${lines.join("\n")}` }] };
|
|
|
5491
5296
|
`/domains/${encodeURIComponent(domain)}/dns`
|
|
5492
5297
|
);
|
|
5493
5298
|
const before = current.data.records.length;
|
|
5299
|
+
const removed = current.data.records.filter(
|
|
5300
|
+
(r) => r.type === type && r.name === dnsName && r.value === value
|
|
5301
|
+
);
|
|
5494
5302
|
const remaining = current.data.records.filter(
|
|
5495
5303
|
(r) => !(r.type === type && r.name === dnsName && r.value === value)
|
|
5496
5304
|
);
|
|
5497
5305
|
if (remaining.length === before) {
|
|
5498
5306
|
throw new Error(`No matching DNS record found: ${type} ${dnsName} = ${value}`);
|
|
5499
5307
|
}
|
|
5308
|
+
if (dryRun) {
|
|
5309
|
+
const diff = formatDnsDiff(domain, current.data.records, remaining, {
|
|
5310
|
+
verb: "delete",
|
|
5311
|
+
removed
|
|
5312
|
+
});
|
|
5313
|
+
return { content: [{ type: "text", text: diff }] };
|
|
5314
|
+
}
|
|
5500
5315
|
await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
|
|
5501
5316
|
method: "PUT",
|
|
5502
5317
|
body: JSON.stringify({ records: remaining })
|
|
@@ -5507,9 +5322,6 @@ ${lines.join("\n")}` }] };
|
|
|
5507
5322
|
if (TRIGGER_TOOL_NAMES.has(name)) {
|
|
5508
5323
|
return handleTriggerTool(name, a, { sshExec, getServerConnection });
|
|
5509
5324
|
}
|
|
5510
|
-
if (AGENT_TOOL_NAMES.has(name)) {
|
|
5511
|
-
return handleAgentTool(name, a);
|
|
5512
|
-
}
|
|
5513
5325
|
if (VERCEL_TOOL_NAMES.has(name)) {
|
|
5514
5326
|
return handleVercelTool(name, a, { supabase, decrypt });
|
|
5515
5327
|
}
|
|
@@ -5547,50 +5359,8 @@ async function main() {
|
|
|
5547
5359
|
if (httpMode) {
|
|
5548
5360
|
console.error(`API key validated. Starting Streamable HTTP transport on port ${httpPort}...`);
|
|
5549
5361
|
const transports = /* @__PURE__ */ new Map();
|
|
5550
|
-
const REST_TOOL_MAP = {
|
|
5551
|
-
"/api/web-search": "web-search",
|
|
5552
|
-
"/api/web-fetch": "web-fetch"
|
|
5553
|
-
};
|
|
5554
5362
|
const httpServer = createServer(async (req, res) => {
|
|
5555
5363
|
const url = new URL(req.url ?? "/", `http://localhost:${httpPort}`);
|
|
5556
|
-
const restToolName = REST_TOOL_MAP[url.pathname];
|
|
5557
|
-
if (restToolName && req.method === "POST") {
|
|
5558
|
-
if (!authContext) {
|
|
5559
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
5560
|
-
res.end(JSON.stringify({ error: "Not authenticated" }));
|
|
5561
|
-
return;
|
|
5562
|
-
}
|
|
5563
|
-
const requiredModule = TOOL_MODULE_MAP[restToolName];
|
|
5564
|
-
if (requiredModule && authContext.permissions.modules[requiredModule] !== true) {
|
|
5565
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
5566
|
-
res.end(JSON.stringify({ error: `Access denied: no permission for "${requiredModule}" module` }));
|
|
5567
|
-
return;
|
|
5568
|
-
}
|
|
5569
|
-
const chunks = [];
|
|
5570
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
5571
|
-
let toolArgs;
|
|
5572
|
-
try {
|
|
5573
|
-
toolArgs = JSON.parse(Buffer.concat(chunks).toString());
|
|
5574
|
-
} catch {
|
|
5575
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
5576
|
-
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
5577
|
-
return;
|
|
5578
|
-
}
|
|
5579
|
-
try {
|
|
5580
|
-
const result = await handleAgentTool(restToolName, toolArgs);
|
|
5581
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5582
|
-
res.end(JSON.stringify({ ok: true, result }));
|
|
5583
|
-
} catch (err) {
|
|
5584
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
5585
|
-
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
5586
|
-
}
|
|
5587
|
-
return;
|
|
5588
|
-
}
|
|
5589
|
-
if (url.pathname === "/api/tools" && req.method === "GET") {
|
|
5590
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5591
|
-
res.end(JSON.stringify({ tools: Object.keys(REST_TOOL_MAP) }));
|
|
5592
|
-
return;
|
|
5593
|
-
}
|
|
5594
5364
|
if (url.pathname !== "/mcp") {
|
|
5595
5365
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
5596
5366
|
res.end("Not found");
|