@mgsoftwarebv/mg-dashboard-mcp 3.14.0 → 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 +2 -627
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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(/&/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
641
|
// src/vercel-tools.ts
|
|
1218
642
|
var VERCEL_API = "https://api.vercel.com";
|
|
1219
643
|
var VERCEL_TOOLS = [
|
|
@@ -1711,8 +1135,7 @@ var MODULE_KEYS = [
|
|
|
1711
1135
|
"wiki",
|
|
1712
1136
|
"ci_cd",
|
|
1713
1137
|
"domains",
|
|
1714
|
-
"settings"
|
|
1715
|
-
"agent_reporting"
|
|
1138
|
+
"settings"
|
|
1716
1139
|
];
|
|
1717
1140
|
var FULL_PERMISSIONS = {
|
|
1718
1141
|
modules: Object.fromEntries(MODULE_KEYS.map((k) => [k, true])),
|
|
@@ -1775,7 +1198,6 @@ var TOOL_MODULE_MAP = {
|
|
|
1775
1198
|
"dns-update": "domains",
|
|
1776
1199
|
"dns-delete": "domains",
|
|
1777
1200
|
...TRIGGER_TOOL_MODULE_MAP,
|
|
1778
|
-
...AGENT_TOOL_MODULE_MAP,
|
|
1779
1201
|
...VERCEL_TOOL_MODULE_MAP
|
|
1780
1202
|
};
|
|
1781
1203
|
var authContext = null;
|
|
@@ -4179,12 +3601,10 @@ var TOOLS = [
|
|
|
4179
3601
|
},
|
|
4180
3602
|
// ----- Trigger.dev -----
|
|
4181
3603
|
...TRIGGER_TOOLS,
|
|
4182
|
-
// ----- Agent Reporting -----
|
|
4183
|
-
...AGENT_TOOLS,
|
|
4184
3604
|
// ----- Vercel -----
|
|
4185
3605
|
...VERCEL_TOOLS
|
|
4186
3606
|
];
|
|
4187
|
-
var MCP_VERSION = "
|
|
3607
|
+
var MCP_VERSION = "4.0.0";
|
|
4188
3608
|
async function handleListTools() {
|
|
4189
3609
|
if (!authContext) return { tools: TOOLS };
|
|
4190
3610
|
const accessible = TOOLS.filter((tool) => {
|
|
@@ -5507,9 +4927,6 @@ ${lines.join("\n")}` }] };
|
|
|
5507
4927
|
if (TRIGGER_TOOL_NAMES.has(name)) {
|
|
5508
4928
|
return handleTriggerTool(name, a, { sshExec, getServerConnection });
|
|
5509
4929
|
}
|
|
5510
|
-
if (AGENT_TOOL_NAMES.has(name)) {
|
|
5511
|
-
return handleAgentTool(name, a);
|
|
5512
|
-
}
|
|
5513
4930
|
if (VERCEL_TOOL_NAMES.has(name)) {
|
|
5514
4931
|
return handleVercelTool(name, a, { supabase, decrypt });
|
|
5515
4932
|
}
|
|
@@ -5547,50 +4964,8 @@ async function main() {
|
|
|
5547
4964
|
if (httpMode) {
|
|
5548
4965
|
console.error(`API key validated. Starting Streamable HTTP transport on port ${httpPort}...`);
|
|
5549
4966
|
const transports = /* @__PURE__ */ new Map();
|
|
5550
|
-
const REST_TOOL_MAP = {
|
|
5551
|
-
"/api/web-search": "web-search",
|
|
5552
|
-
"/api/web-fetch": "web-fetch"
|
|
5553
|
-
};
|
|
5554
4967
|
const httpServer = createServer(async (req, res) => {
|
|
5555
4968
|
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
4969
|
if (url.pathname !== "/mcp") {
|
|
5595
4970
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
5596
4971
|
res.end("Not found");
|