@mgsoftwarebv/mg-dashboard-mcp 2.4.4 → 2.5.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 CHANGED
@@ -708,6 +708,30 @@ var AGENT_TOOLS = [
708
708
  },
709
709
  required: ["target_id", "results_count"]
710
710
  }
711
+ },
712
+ {
713
+ name: "web-search",
714
+ description: "Search the web using DuckDuckGo. Returns a list of results with title, URL, and snippet. Use this to find companies, websites, directories, etc.",
715
+ inputSchema: {
716
+ type: "object",
717
+ properties: {
718
+ query: { type: "string", description: 'Search query (e.g. "logistiek bedrijf MKB nederland")' },
719
+ max_results: { type: "number", description: "Max results to return (default: 15, max: 30)" }
720
+ },
721
+ required: ["query"]
722
+ }
723
+ },
724
+ {
725
+ name: "web-fetch",
726
+ description: "Fetch a web page and return its text content (HTML tags stripped). Use this to read company websites, extract contact info, about pages, etc. Returns at most 15000 characters of cleaned text.",
727
+ inputSchema: {
728
+ type: "object",
729
+ properties: {
730
+ url: { type: "string", description: 'Full URL to fetch (e.g. "https://example.nl/contact")' },
731
+ extract_links: { type: "boolean", description: "Also extract all links from the page (default: false)" }
732
+ },
733
+ required: ["url"]
734
+ }
711
735
  }
712
736
  ];
713
737
  var AGENT_TOOL_NAMES = new Set(AGENT_TOOLS.map((t) => t.name));
@@ -721,7 +745,9 @@ var AGENT_TOOL_MODULE_MAP = {
721
745
  "agent-check-lead-exists": "agent_reporting",
722
746
  "agent-save-lead": "agent_reporting",
723
747
  "agent-save-email-draft": "agent_reporting",
724
- "agent-complete-target": "agent_reporting"
748
+ "agent-complete-target": "agent_reporting",
749
+ "web-search": "agent_reporting",
750
+ "web-fetch": "agent_reporting"
725
751
  };
726
752
  function clamp(val, min, max) {
727
753
  return Math.max(min, Math.min(max, val));
@@ -729,6 +755,80 @@ function clamp(val, min, max) {
729
755
  function sanitizeString(val, maxLen) {
730
756
  return String(val ?? "").slice(0, maxLen);
731
757
  }
758
+ 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";
759
+ async function webSearch(query, maxResults) {
760
+ const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
761
+ const response = await fetch(url, {
762
+ headers: {
763
+ "User-Agent": WEB_USER_AGENT,
764
+ "Accept": "text/html",
765
+ "Accept-Language": "nl,en;q=0.9"
766
+ },
767
+ redirect: "follow"
768
+ });
769
+ if (!response.ok) throw new Error(`Search failed: HTTP ${response.status}`);
770
+ const html = await response.text();
771
+ const results = [];
772
+ const resultBlocks = html.split(/class="result\s/);
773
+ for (let i = 1; i < resultBlocks.length && results.length < maxResults; i++) {
774
+ const block = resultBlocks[i];
775
+ const titleMatch = block.match(/class="result__a"[^>]*>([^<]+)</);
776
+ const hrefMatch = block.match(/class="result__a"\s+href="([^"]+)"/);
777
+ const snippetMatch = block.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/);
778
+ if (!hrefMatch) continue;
779
+ let href = hrefMatch[1];
780
+ if (href.includes("uddg=")) {
781
+ const uddg = new URL(href, "https://duckduckgo.com").searchParams.get("uddg");
782
+ if (uddg) href = uddg;
783
+ }
784
+ if (!href.startsWith("http")) continue;
785
+ const title = titleMatch ? titleMatch[1].replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&#x27;/g, "'").replace(/&quot;/g, '"').trim() : href;
786
+ 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() : "";
787
+ results.push({ title, url: href, snippet });
788
+ }
789
+ return results;
790
+ }
791
+ async function webFetch(url, extractLinks) {
792
+ const controller = new AbortController();
793
+ const timeout = setTimeout(() => controller.abort(), 15e3);
794
+ try {
795
+ const response = await fetch(url, {
796
+ headers: {
797
+ "User-Agent": WEB_USER_AGENT,
798
+ "Accept": "text/html,application/xhtml+xml",
799
+ "Accept-Language": "nl,en;q=0.9"
800
+ },
801
+ redirect: "follow",
802
+ signal: controller.signal
803
+ });
804
+ if (!response.ok) throw new Error(`Fetch failed: HTTP ${response.status}`);
805
+ const contentType = response.headers.get("content-type") || "";
806
+ if (!contentType.includes("text/html") && !contentType.includes("text/plain") && !contentType.includes("application/xhtml")) {
807
+ throw new Error(`Unsupported content type: ${contentType}. Only HTML/text pages supported.`);
808
+ }
809
+ const html = await response.text();
810
+ const links = [];
811
+ if (extractLinks) {
812
+ const linkRegex = /<a\s[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
813
+ let match;
814
+ while ((match = linkRegex.exec(html)) !== null) {
815
+ const href = match[1];
816
+ const text2 = match[2].replace(/<[^>]+>/g, "").trim();
817
+ if (href && href.startsWith("http")) {
818
+ links.push({ href, text: text2.slice(0, 100) });
819
+ }
820
+ }
821
+ }
822
+ 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();
823
+ const MAX_TEXT = 15e3;
824
+ if (text.length > MAX_TEXT) {
825
+ text = text.slice(0, MAX_TEXT) + "\n\n[... truncated at 15000 chars ...]";
826
+ }
827
+ return { text, links };
828
+ } finally {
829
+ clearTimeout(timeout);
830
+ }
831
+ }
732
832
  function textSimilarity(a, b) {
733
833
  if (a === b) return 1;
734
834
  const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
@@ -1221,6 +1321,51 @@ ${output}` }]
1221
1321
  };
1222
1322
  }
1223
1323
  // -----------------------------------------------------------------
1324
+ // Web Tools
1325
+ // -----------------------------------------------------------------
1326
+ case "web-search": {
1327
+ const query = sanitizeString(args2.query, 500);
1328
+ if (!query) throw new Error("query is required");
1329
+ const maxResults = clamp(Number(args2.max_results) || 15, 1, 30);
1330
+ const results = await webSearch(query, maxResults);
1331
+ if (results.length === 0) {
1332
+ return { content: [{ type: "text", text: `No search results found for: "${query}"` }] };
1333
+ }
1334
+ const formatted = results.map(
1335
+ (r, i) => `${i + 1}. ${r.title}
1336
+ URL: ${r.url}
1337
+ ${r.snippet}`
1338
+ ).join("\n\n");
1339
+ return {
1340
+ content: [{
1341
+ type: "text",
1342
+ text: `Search results for "${query}" (${results.length} results):
1343
+
1344
+ ${formatted}`
1345
+ }]
1346
+ };
1347
+ }
1348
+ case "web-fetch": {
1349
+ const url = sanitizeString(args2.url, 2e3);
1350
+ if (!url) throw new Error("url is required");
1351
+ if (!/^https?:\/\//i.test(url)) throw new Error("url must start with http:// or https://");
1352
+ const extractLinks = Boolean(args2.extract_links);
1353
+ const result = await webFetch(url, extractLinks);
1354
+ let text = `Content from ${url} (${result.text.length} chars):
1355
+
1356
+ ${result.text}`;
1357
+ if (extractLinks && result.links.length > 0) {
1358
+ const linkList = result.links.slice(0, 50).map(
1359
+ (l) => ` ${l.text ? l.text + ": " : ""}${l.href}`
1360
+ ).join("\n");
1361
+ text += `
1362
+
1363
+ --- Links (${result.links.length} found, showing max 50) ---
1364
+ ${linkList}`;
1365
+ }
1366
+ return { content: [{ type: "text", text }] };
1367
+ }
1368
+ // -----------------------------------------------------------------
1224
1369
  default:
1225
1370
  return { content: [{ type: "text", text: `Unknown agent tool: ${name}` }] };
1226
1371
  }
@@ -3016,7 +3161,9 @@ async function main() {
3016
3161
  "/api/check-lead-exists": "agent-check-lead-exists",
3017
3162
  "/api/save-lead": "agent-save-lead",
3018
3163
  "/api/save-email-draft": "agent-save-email-draft",
3019
- "/api/complete-target": "agent-complete-target"
3164
+ "/api/complete-target": "agent-complete-target",
3165
+ "/api/web-search": "web-search",
3166
+ "/api/web-fetch": "web-fetch"
3020
3167
  };
3021
3168
  const httpServer = createServer(async (req, res) => {
3022
3169
  const url = new URL(req.url ?? "/", `http://localhost:${httpPort}`);