@mgsoftwarebv/mg-dashboard-mcp 3.13.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -638,582 +638,6 @@ ${rawJson.substring(0, 500)}`
638
638
  }
639
639
  }
640
640
 
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(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#x27;/g, "'").replace(/&quot;/g, '"').trim() : href;
730
- const snippet = snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#x27;/g, "'").replace(/&quot;/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(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#x27;/g, "'").replace(/&quot;/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
641
  // src/vercel-tools.ts
1218
642
  var VERCEL_API = "https://api.vercel.com";
1219
643
  var VERCEL_TOOLS = [
@@ -1230,7 +654,7 @@ var VERCEL_TOOLS = [
1230
654
  },
1231
655
  {
1232
656
  name: "vercel-deployments",
1233
- description: "List recent deployments for a Vercel project. Returns deployment ID, state, target, branch, commit and timestamps. Use the deployment ID with vercel-build-log or vercel-runtime-log.",
657
+ description: "List recent deployments for a Vercel project. Returns deployment ID, state, target, branch, commit and timestamps. Use the deployment ID with vercel-logs.",
1234
658
  inputSchema: {
1235
659
  type: "object",
1236
660
  properties: {
@@ -1246,46 +670,40 @@ var VERCEL_TOOLS = [
1246
670
  }
1247
671
  },
1248
672
  {
1249
- name: "vercel-build-log",
1250
- description: "Get build / deployment console events (stdout, stderr, command, exit) for a Vercel deployment. Use this when a deployment failed at build time.",
1251
- inputSchema: {
1252
- type: "object",
1253
- properties: {
1254
- deploymentId: { type: "string", description: "Vercel deployment ID (from vercel-deployments)" },
1255
- limit: { type: "number", description: "Max events to return (default 500, max 5000)" }
1256
- },
1257
- required: ["deploymentId"]
1258
- }
1259
- },
1260
- {
1261
- name: "vercel-runtime-log",
1262
- description: "Get runtime / function logs for a deployment (the application output after the build succeeds). Requires both project and deployment IDs.",
1263
- inputSchema: {
1264
- type: "object",
1265
- properties: {
1266
- project: { type: "string", description: "Vercel project ID or name" },
1267
- deploymentId: { type: "string", description: "Vercel deployment ID" },
1268
- limit: { type: "number", description: "Max log entries to return (default 200, max 1000)" },
1269
- sinceMinutes: {
1270
- type: "number",
1271
- description: "Only return logs from the last N minutes (default 60)"
1272
- }
1273
- },
1274
- required: ["project", "deploymentId"]
1275
- }
1276
- },
1277
- {
1278
- name: "vercel-webhook-history",
1279
- description: "List recent Vercel deployment webhook deliveries (from the vercel_webhook_logs table). Useful to confirm Telegram / push notifications were sent and to see the original payload metadata.",
673
+ name: "vercel-logs",
674
+ 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.',
1280
675
  inputSchema: {
1281
676
  type: "object",
1282
677
  properties: {
1283
- projectName: { type: "string", description: "Optional filter on project name" },
678
+ kind: {
679
+ type: "string",
680
+ enum: ["build", "runtime", "webhooks"],
681
+ description: "Which log stream to fetch (default: build)."
682
+ },
683
+ project: {
684
+ type: "string",
685
+ description: 'Vercel project ID or name (required for kind="runtime").'
686
+ },
687
+ deploymentId: {
688
+ type: "string",
689
+ description: 'Vercel deployment ID (required for kind="build" or "runtime").'
690
+ },
691
+ projectName: {
692
+ type: "string",
693
+ description: 'Optional project_name filter (kind="webhooks" only).'
694
+ },
1284
695
  status: {
1285
696
  type: "string",
1286
- description: "Optional status filter: sent, skipped, error"
697
+ description: 'Optional status filter (kind="webhooks" only): sent, skipped, error.'
698
+ },
699
+ sinceMinutes: {
700
+ type: "number",
701
+ description: 'Time window in minutes (kind="runtime" only, default 60, max 7 days).'
1287
702
  },
1288
- limit: { type: "number", description: "Max rows to return (default 25, max 200)" }
703
+ limit: {
704
+ type: "number",
705
+ description: "Max entries to return. Defaults per kind: build=500 (max 5000), runtime=200 (max 1000), webhooks=25 (max 200)."
706
+ }
1289
707
  }
1290
708
  }
1291
709
  }
@@ -1294,9 +712,7 @@ var VERCEL_TOOL_NAMES = new Set(VERCEL_TOOLS.map((t) => t.name));
1294
712
  var VERCEL_TOOL_MODULE_MAP = {
1295
713
  "vercel-projects": "ci_cd",
1296
714
  "vercel-deployments": "ci_cd",
1297
- "vercel-build-log": "ci_cd",
1298
- "vercel-runtime-log": "ci_cd",
1299
- "vercel-webhook-history": "ci_cd"
715
+ "vercel-logs": "ci_cd"
1300
716
  };
1301
717
  async function vercelFetch(token, path) {
1302
718
  const res = await fetch(`${VERCEL_API}${path}`, {
@@ -1544,46 +960,64 @@ async function handleVercelTool(name, args2, deps) {
1544
960
  if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
1545
961
  return { content: [{ type: "text", text: formatDeploymentsTable(deployments) }] };
1546
962
  }
1547
- case "vercel-build-log": {
1548
- const token = await getVercelToken(deps);
1549
- const deploymentId = String(args2.deploymentId);
1550
- const limit = Math.min(Math.max(Number(args2.limit) || 500, 1), 5e3);
1551
- const { events, error } = await getDeploymentBuildEvents(token, deploymentId, limit);
1552
- if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
1553
- return { content: [{ type: "text", text: formatBuildEvents(events) }] };
1554
- }
1555
- case "vercel-runtime-log": {
1556
- const token = await getVercelToken(deps);
1557
- const projectInput = String(args2.project);
1558
- const deploymentId = String(args2.deploymentId);
1559
- const limit = Math.min(Math.max(Number(args2.limit) || 200, 1), 1e3);
1560
- const sinceMinutes = Math.min(Math.max(Number(args2.sinceMinutes) || 60, 1), 7 * 24 * 60);
1561
- const sinceMs = Date.now() - sinceMinutes * 6e4;
1562
- const projectId = await resolveProjectId(token, projectInput);
1563
- const { logs, error } = await getRuntimeLogs(
1564
- token,
1565
- projectId,
1566
- deploymentId,
1567
- limit,
1568
- sinceMs
1569
- );
1570
- if (error) {
1571
- 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 vercel-webhook-history or the supabase MCP (vercel_deployment_log table) for archived runtime logs." : "";
1572
- return { content: [{ type: "text", text: `Error: ${error}${hint}` }] };
963
+ case "vercel-logs": {
964
+ const kind = (args2.kind ? String(args2.kind) : "build").toLowerCase();
965
+ if (kind === "build") {
966
+ if (!args2.deploymentId) {
967
+ return { content: [{ type: "text", text: 'Error: kind="build" requires deploymentId.' }] };
968
+ }
969
+ const token = await getVercelToken(deps);
970
+ const deploymentId = String(args2.deploymentId);
971
+ const limit = Math.min(Math.max(Number(args2.limit) || 500, 1), 5e3);
972
+ const { events, error } = await getDeploymentBuildEvents(token, deploymentId, limit);
973
+ if (error) return { content: [{ type: "text", text: `Error: ${error}` }] };
974
+ return { content: [{ type: "text", text: formatBuildEvents(events) }] };
975
+ }
976
+ if (kind === "runtime") {
977
+ if (!args2.project || !args2.deploymentId) {
978
+ return {
979
+ content: [
980
+ { type: "text", text: 'Error: kind="runtime" requires both project and deploymentId.' }
981
+ ]
982
+ };
983
+ }
984
+ const token = await getVercelToken(deps);
985
+ const projectInput = String(args2.project);
986
+ const deploymentId = String(args2.deploymentId);
987
+ const limit = Math.min(Math.max(Number(args2.limit) || 200, 1), 1e3);
988
+ const sinceMinutes = Math.min(Math.max(Number(args2.sinceMinutes) || 60, 1), 7 * 24 * 60);
989
+ const sinceMs = Date.now() - sinceMinutes * 6e4;
990
+ const projectId = await resolveProjectId(token, projectInput);
991
+ const { logs, error } = await getRuntimeLogs(
992
+ token,
993
+ projectId,
994
+ deploymentId,
995
+ limit,
996
+ sinceMs
997
+ );
998
+ if (error) {
999
+ const hint = error.includes("404") || error.includes("400") ? '\n\nThis endpoint requires both project ID and deployment ID and may not be available on every Vercel plan. Use kind="webhooks" or the supabase MCP (vercel_deployment_log table) for archived runtime logs.' : "";
1000
+ return { content: [{ type: "text", text: `Error: ${error}${hint}` }] };
1001
+ }
1002
+ return { content: [{ type: "text", text: formatRuntimeLogs(logs) }] };
1003
+ }
1004
+ if (kind === "webhooks") {
1005
+ const limit = Math.min(Math.max(Number(args2.limit) || 25, 1), 200);
1006
+ let query = deps.supabase.from("vercel_webhook_logs").select(
1007
+ "id, event_type, status, project_name, deployment_id, target, message, error_message, created_at"
1008
+ ).order("created_at", { ascending: false }).limit(limit);
1009
+ if (args2.projectName) query = query.eq("project_name", String(args2.projectName));
1010
+ if (args2.status) query = query.eq("status", String(args2.status));
1011
+ const { data, error } = await query;
1012
+ if (error) return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1013
+ return {
1014
+ content: [{ type: "text", text: formatWebhookHistory(data ?? []) }]
1015
+ };
1573
1016
  }
1574
- return { content: [{ type: "text", text: formatRuntimeLogs(logs) }] };
1575
- }
1576
- case "vercel-webhook-history": {
1577
- const limit = Math.min(Math.max(Number(args2.limit) || 25, 1), 200);
1578
- let query = deps.supabase.from("vercel_webhook_logs").select(
1579
- "id, event_type, status, project_name, deployment_id, target, message, error_message, created_at"
1580
- ).order("created_at", { ascending: false }).limit(limit);
1581
- if (args2.projectName) query = query.eq("project_name", String(args2.projectName));
1582
- if (args2.status) query = query.eq("status", String(args2.status));
1583
- const { data, error } = await query;
1584
- if (error) return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1585
1017
  return {
1586
- content: [{ type: "text", text: formatWebhookHistory(data ?? []) }]
1018
+ content: [
1019
+ { type: "text", text: `Error: unknown kind "${kind}". Use build, runtime, or webhooks.` }
1020
+ ]
1587
1021
  };
1588
1022
  }
1589
1023
  default:
@@ -1701,8 +1135,7 @@ var MODULE_KEYS = [
1701
1135
  "wiki",
1702
1136
  "ci_cd",
1703
1137
  "domains",
1704
- "settings",
1705
- "agent_reporting"
1138
+ "settings"
1706
1139
  ];
1707
1140
  var FULL_PERMISSIONS = {
1708
1141
  modules: Object.fromEntries(MODULE_KEYS.map((k) => [k, true])),
@@ -1765,7 +1198,6 @@ var TOOL_MODULE_MAP = {
1765
1198
  "dns-update": "domains",
1766
1199
  "dns-delete": "domains",
1767
1200
  ...TRIGGER_TOOL_MODULE_MAP,
1768
- ...AGENT_TOOL_MODULE_MAP,
1769
1201
  ...VERCEL_TOOL_MODULE_MAP
1770
1202
  };
1771
1203
  var authContext = null;
@@ -4169,12 +3601,10 @@ var TOOLS = [
4169
3601
  },
4170
3602
  // ----- Trigger.dev -----
4171
3603
  ...TRIGGER_TOOLS,
4172
- // ----- Agent Reporting -----
4173
- ...AGENT_TOOLS,
4174
3604
  // ----- Vercel -----
4175
3605
  ...VERCEL_TOOLS
4176
3606
  ];
4177
- var MCP_VERSION = "3.13.1";
3607
+ var MCP_VERSION = "4.0.0";
4178
3608
  async function handleListTools() {
4179
3609
  if (!authContext) return { tools: TOOLS };
4180
3610
  const accessible = TOOLS.filter((tool) => {
@@ -5497,9 +4927,6 @@ ${lines.join("\n")}` }] };
5497
4927
  if (TRIGGER_TOOL_NAMES.has(name)) {
5498
4928
  return handleTriggerTool(name, a, { sshExec, getServerConnection });
5499
4929
  }
5500
- if (AGENT_TOOL_NAMES.has(name)) {
5501
- return handleAgentTool(name, a);
5502
- }
5503
4930
  if (VERCEL_TOOL_NAMES.has(name)) {
5504
4931
  return handleVercelTool(name, a, { supabase, decrypt });
5505
4932
  }
@@ -5537,50 +4964,8 @@ async function main() {
5537
4964
  if (httpMode) {
5538
4965
  console.error(`API key validated. Starting Streamable HTTP transport on port ${httpPort}...`);
5539
4966
  const transports = /* @__PURE__ */ new Map();
5540
- const REST_TOOL_MAP = {
5541
- "/api/web-search": "web-search",
5542
- "/api/web-fetch": "web-fetch"
5543
- };
5544
4967
  const httpServer = createServer(async (req, res) => {
5545
4968
  const url = new URL(req.url ?? "/", `http://localhost:${httpPort}`);
5546
- const restToolName = REST_TOOL_MAP[url.pathname];
5547
- if (restToolName && req.method === "POST") {
5548
- if (!authContext) {
5549
- res.writeHead(401, { "Content-Type": "application/json" });
5550
- res.end(JSON.stringify({ error: "Not authenticated" }));
5551
- return;
5552
- }
5553
- const requiredModule = TOOL_MODULE_MAP[restToolName];
5554
- if (requiredModule && authContext.permissions.modules[requiredModule] !== true) {
5555
- res.writeHead(403, { "Content-Type": "application/json" });
5556
- res.end(JSON.stringify({ error: `Access denied: no permission for "${requiredModule}" module` }));
5557
- return;
5558
- }
5559
- const chunks = [];
5560
- for await (const chunk of req) chunks.push(chunk);
5561
- let toolArgs;
5562
- try {
5563
- toolArgs = JSON.parse(Buffer.concat(chunks).toString());
5564
- } catch {
5565
- res.writeHead(400, { "Content-Type": "application/json" });
5566
- res.end(JSON.stringify({ error: "Invalid JSON" }));
5567
- return;
5568
- }
5569
- try {
5570
- const result = await handleAgentTool(restToolName, toolArgs);
5571
- res.writeHead(200, { "Content-Type": "application/json" });
5572
- res.end(JSON.stringify({ ok: true, result }));
5573
- } catch (err) {
5574
- res.writeHead(500, { "Content-Type": "application/json" });
5575
- res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
5576
- }
5577
- return;
5578
- }
5579
- if (url.pathname === "/api/tools" && req.method === "GET") {
5580
- res.writeHead(200, { "Content-Type": "application/json" });
5581
- res.end(JSON.stringify({ tools: Object.keys(REST_TOOL_MAP) }));
5582
- return;
5583
- }
5584
4969
  if (url.pathname !== "/mcp") {
5585
4970
  res.writeHead(404, { "Content-Type": "text/plain" });
5586
4971
  res.end("Not found");