@mgsoftwarebv/mg-dashboard-mcp 2.4.2 → 2.4.4

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
@@ -388,7 +388,12 @@ var VALID_FINDING_TYPES = /* @__PURE__ */ new Set([
388
388
  "slow_endpoint",
389
389
  "memory_leak",
390
390
  "missing_memoization",
391
- "large_dependency"
391
+ "large_dependency",
392
+ "dead_code",
393
+ "unused_dependency",
394
+ "env_leak",
395
+ "i18n_missing_key",
396
+ "i18n_unused_key"
392
397
  ]);
393
398
  var VALID_SEVERITIES = /* @__PURE__ */ new Set(["info", "warning", "critical"]);
394
399
  var VALID_SCOPES = /* @__PURE__ */ new Set([
@@ -398,7 +403,9 @@ var VALID_SCOPES = /* @__PURE__ */ new Set([
398
403
  "api",
399
404
  "package",
400
405
  "security",
401
- "server_audit"
406
+ "server_audit",
407
+ "changelog",
408
+ "api-reference"
402
409
  ]);
403
410
  var VALID_REVIEW_STATUSES = /* @__PURE__ */ new Set([
404
411
  "pending",
@@ -406,6 +413,9 @@ var VALID_REVIEW_STATUSES = /* @__PURE__ */ new Set([
406
413
  "agent_flagged",
407
414
  "human_approved"
408
415
  ]);
416
+ function normalizeCompanyName(name) {
417
+ return name.toLowerCase().replace(/\b(b\.?v\.?|n\.?v\.?|v\.?o\.?f\.?|c\.?v\.?|holding|groep|group|nederland|netherlands)\b/gi, "").replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, " ").trim();
418
+ }
409
419
  var AGENT_TOOLS = [
410
420
  {
411
421
  name: "agent-report-coverage",
@@ -439,7 +449,7 @@ var AGENT_TOOLS = [
439
449
  }
440
450
  }
441
451
  },
442
- required: ["repo_slug", "refront_project_id", "entries"]
452
+ required: ["repo_slug", "entries"]
443
453
  }
444
454
  },
445
455
  {
@@ -494,7 +504,7 @@ var AGENT_TOOLS = [
494
504
  }
495
505
  }
496
506
  },
497
- required: ["repo_slug", "refront_project_id", "category", "findings"]
507
+ required: ["repo_slug", "category", "findings"]
498
508
  }
499
509
  },
500
510
  {
@@ -504,7 +514,7 @@ var AGENT_TOOLS = [
504
514
  type: "object",
505
515
  properties: {
506
516
  repo_slug: { type: "string", description: "Repository slug" },
507
- refront_project_id: { type: "string", description: "Refront project UUID" },
517
+ refront_project_id: { type: "string", description: "Refront project UUID (optional for standalone repos)" },
508
518
  scope: {
509
519
  type: "string",
510
520
  enum: [...VALID_SCOPES],
@@ -522,7 +532,7 @@ var AGENT_TOOLS = [
522
532
  description: 'Review status (default: "pending")'
523
533
  }
524
534
  },
525
- required: ["repo_slug", "refront_project_id", "scope", "path", "title", "content"]
535
+ required: ["repo_slug", "scope", "path", "title", "content"]
526
536
  }
527
537
  },
528
538
  {
@@ -578,7 +588,7 @@ var AGENT_TOOLS = [
578
588
  },
579
589
  {
580
590
  name: "agent-validate-suggestions",
581
- description: "Validate existing open suggestions against the actual codebase. For each suggestion, report whether it is valid, invalid (should be dismissed), or needs adjustment. Invalid suggestions are auto-dismissed with a reason.",
591
+ description: "Validate existing open suggestions against the actual codebase. For each suggestion, report whether it is resolved (fixed), valid (still open), invalid (should be dismissed), or needs adjustment.",
582
592
  inputSchema: {
583
593
  type: "object",
584
594
  properties: {
@@ -598,8 +608,8 @@ var AGENT_TOOLS = [
598
608
  },
599
609
  verdict: {
600
610
  type: "string",
601
- enum: ["valid", "invalid", "adjusted"],
602
- description: "Validation verdict: valid (keep as-is), invalid (dismiss), adjusted (update fields)"
611
+ enum: ["valid", "invalid", "adjusted", "resolved"],
612
+ description: "resolved = fix applied (sets status resolved), valid = still relevant (keep open), invalid = dismiss, adjusted = update fields"
603
613
  },
604
614
  reason: {
605
615
  type: "string",
@@ -625,6 +635,79 @@ var AGENT_TOOLS = [
625
635
  },
626
636
  required: ["repo_slug", "results"]
627
637
  }
638
+ },
639
+ // -- Lead Generation tools --------------------------------------------------
640
+ {
641
+ name: "agent-check-lead-exists",
642
+ description: "Check if a lead already exists by website URL or company name. Call this BEFORE visiting a website to avoid wasting time on duplicates.",
643
+ inputSchema: {
644
+ type: "object",
645
+ properties: {
646
+ website_url: {
647
+ type: "string",
648
+ description: 'Website URL to check (e.g. "https://example.nl")'
649
+ },
650
+ company_name: {
651
+ type: "string",
652
+ description: "Company name to fuzzy-match against existing leads"
653
+ }
654
+ },
655
+ required: []
656
+ }
657
+ },
658
+ {
659
+ name: "agent-save-lead",
660
+ description: "Save a discovered lead (company) to the database. Handles dedup on website_url + fuzzy match on company name. Returns the lead ID and whether it already existed.",
661
+ inputSchema: {
662
+ type: "object",
663
+ properties: {
664
+ company_name: { type: "string", description: "Company name" },
665
+ website_url: { type: "string", description: "Company website URL" },
666
+ industry: { type: "string", description: "Industry sector" },
667
+ region: { type: "string", description: 'Geographic region (e.g. "amsterdam", "rotterdam")' },
668
+ description: { type: "string", description: "AI-generated summary of what the company does (max 2000 chars)" },
669
+ potential_fit: { type: "string", description: "Why MG Software could help this company (max 2000 chars)" },
670
+ fit_score: { type: "number", description: "Fit score 1-10 for MG Software partnership" },
671
+ estimated_company_size: { type: "string", description: 'Estimated employee count range (e.g. "1-10", "10-50", "50-200")' },
672
+ kvk_number: { type: "string", description: "KvK (Chamber of Commerce) number if found" },
673
+ contact_name: { type: "string", description: "Primary contact person name" },
674
+ contact_role: { type: "string", description: "Contact person role/title" },
675
+ contact_email: { type: "string", description: "Contact person email" },
676
+ contact_phone: { type: "string", description: "Contact person phone" },
677
+ contact_linkedin: { type: "string", description: "Contact person LinkedIn URL" },
678
+ general_email: { type: "string", description: "General company email (info@...)" },
679
+ general_phone: { type: "string", description: "General company phone number" },
680
+ source_url: { type: "string", description: "URL where this lead was found (Google result, directory, etc.)" },
681
+ target_id: { type: "string", description: "UUID of the lead_generation_target this lead was found for" }
682
+ },
683
+ required: ["company_name", "website_url"]
684
+ }
685
+ },
686
+ {
687
+ name: "agent-save-email-draft",
688
+ description: "Save a cold email draft for a lead. The email will be shown in the backoffice for manual review and copy. Do NOT use em-dashes or en-dashes in the email.",
689
+ inputSchema: {
690
+ type: "object",
691
+ properties: {
692
+ lead_id: { type: "string", description: "UUID of the lead this email is for" },
693
+ subject: { type: "string", description: "Email subject line (Dutch, max 200 chars)" },
694
+ body: { type: "string", description: "Email body text (Dutch, max 5000 chars, NO em-dashes or en-dashes)" },
695
+ tone: { type: "string", description: "Tone of the email (default: professional)" }
696
+ },
697
+ required: ["lead_id", "subject", "body"]
698
+ }
699
+ },
700
+ {
701
+ name: "agent-complete-target",
702
+ description: "Mark a lead_generation_target as completed with the number of leads found.",
703
+ inputSchema: {
704
+ type: "object",
705
+ properties: {
706
+ target_id: { type: "string", description: "UUID of the lead_generation_target" },
707
+ results_count: { type: "number", description: "Number of new leads saved for this target" }
708
+ },
709
+ required: ["target_id", "results_count"]
710
+ }
628
711
  }
629
712
  ];
630
713
  var AGENT_TOOL_NAMES = new Set(AGENT_TOOLS.map((t) => t.name));
@@ -634,7 +717,11 @@ var AGENT_TOOL_MODULE_MAP = {
634
717
  "agent-save-documentation": "agent_reporting",
635
718
  "agent-list-findings": "agent_reporting",
636
719
  "agent-get-documentation": "agent_reporting",
637
- "agent-validate-suggestions": "agent_reporting"
720
+ "agent-validate-suggestions": "agent_reporting",
721
+ "agent-check-lead-exists": "agent_reporting",
722
+ "agent-save-lead": "agent_reporting",
723
+ "agent-save-email-draft": "agent_reporting",
724
+ "agent-complete-target": "agent_reporting"
638
725
  };
639
726
  function clamp(val, min, max) {
640
727
  return Math.max(min, Math.min(max, val));
@@ -670,7 +757,6 @@ async function handleAgentTool(name, args2, deps) {
670
757
  const refrontProjectId = sanitizeString(args2.refront_project_id, 100);
671
758
  const entries = Array.isArray(args2.entries) ? args2.entries : [];
672
759
  if (!repoSlug) throw new Error("repo_slug is required");
673
- if (!refrontProjectId) throw new Error("refront_project_id is required");
674
760
  if (entries.length === 0) throw new Error("entries array must not be empty");
675
761
  const wsId = deps.workspaceId;
676
762
  const scanCommit = wsId ? `agent-scan-${wsId.slice(0, 8)}` : `agent-scan-${Date.now().toString(36)}`;
@@ -710,14 +796,13 @@ async function handleAgentTool(name, args2, deps) {
710
796
  const category = args2.category;
711
797
  const findings = Array.isArray(args2.findings) ? args2.findings : [];
712
798
  if (!repoSlug) throw new Error("repo_slug is required");
713
- if (!refrontProjectId) throw new Error("refront_project_id is required");
714
799
  if (!category || !["scan_findings", "perf_audit"].includes(category)) {
715
800
  throw new Error('category must be "scan_findings" or "perf_audit"');
716
801
  }
717
802
  if (findings.length === 0) throw new Error("findings array must not be empty");
718
803
  const SIMILARITY_THRESHOLD = 0.55;
719
804
  const MAX_OPEN_PER_TYPE = 30;
720
- const { data: existingFindings } = await supabase2.from("doc_suggestion").select("id, type, description, file_path, severity, category, status").eq("repo_slug", repoSlug).in("status", ["open", "dismissed"]).limit(500);
805
+ const { data: existingFindings } = await supabase2.from("doc_suggestion").select("id, type, description, file_path, severity, category, status").eq("repo_slug", repoSlug).in("status", ["open", "dismissed", "resolved", "ticket_created"]).limit(500);
721
806
  const existing = existingFindings ?? [];
722
807
  const typeCountMap = /* @__PURE__ */ new Map();
723
808
  for (const e of existing) {
@@ -738,6 +823,9 @@ async function handleAgentTool(name, args2, deps) {
738
823
  if (!description) continue;
739
824
  const isDuplicate = existing.some((e) => {
740
825
  if (e.type !== findingType) return false;
826
+ if (filePath && e.file_path === filePath && ["dismissed", "resolved"].includes(e.status)) {
827
+ return true;
828
+ }
741
829
  if (filePath && e.file_path === filePath) {
742
830
  return textSimilarity(e.description ?? "", description) > 0.4;
743
831
  }
@@ -812,7 +900,6 @@ ${items.map((i) => `\u2022 ${i}`).join("\n")}`;
812
900
  const content = sanitizeString(args2.content, 1e5);
813
901
  const reviewStatus = VALID_REVIEW_STATUSES.has(args2.review_status) ? args2.review_status : "pending";
814
902
  if (!repoSlug) throw new Error("repo_slug is required");
815
- if (!refrontProjectId) throw new Error("refront_project_id is required");
816
903
  if (!path) throw new Error("path is required");
817
904
  if (!title) throw new Error("title is required");
818
905
  if (!content) throw new Error("content is required");
@@ -895,13 +982,16 @@ ${summary}`
895
982
  return { content: [{ type: "text", text: `No documentation found for repo "${repoSlug}"` }] };
896
983
  }
897
984
  const output = docs.map((d) => {
898
- const contentPreview = String(d.content || "").slice(0, 500);
985
+ const fullContent = String(d.content || "");
986
+ const isChangelog = d.scope === "changelog";
987
+ const maxPreview = isChangelog ? 5e4 : 500;
988
+ const preview = fullContent.slice(0, maxPreview);
899
989
  return [
900
990
  `## ${d.title} (${d.scope}/${d.path})`,
901
991
  `Status: ${d.review_status} | By: ${d.generated_by || "unknown"}`,
902
992
  `Updated: ${d.updated_at || d.created_at}`,
903
993
  "",
904
- contentPreview + (String(d.content || "").length > 500 ? "\n...(truncated)" : ""),
994
+ preview + (fullContent.length > maxPreview ? "\n...(truncated)" : ""),
905
995
  ""
906
996
  ].join("\n");
907
997
  }).join("\n---\n\n");
@@ -923,15 +1013,24 @@ ${output}` }]
923
1013
  let adjusted = 0;
924
1014
  let validated = 0;
925
1015
  let errors = 0;
1016
+ let resolved = 0;
926
1017
  for (const r of results) {
927
1018
  const id = sanitizeString(r.suggestion_id, 100);
928
1019
  const verdict = r.verdict;
929
1020
  const reason = sanitizeString(r.reason, 2e3);
930
- if (!id || !["valid", "invalid", "adjusted"].includes(verdict)) {
1021
+ if (!id || !["valid", "invalid", "adjusted", "resolved"].includes(verdict)) {
931
1022
  errors++;
932
1023
  continue;
933
1024
  }
934
- if (verdict === "invalid") {
1025
+ if (verdict === "resolved") {
1026
+ const { error } = await supabase2.from("doc_suggestion").update({
1027
+ status: "resolved",
1028
+ validated_at: now,
1029
+ validated_by: validatedBy
1030
+ }).eq("id", id).eq("repo_slug", repoSlug);
1031
+ if (error) errors++;
1032
+ else resolved++;
1033
+ } else if (verdict === "invalid") {
935
1034
  const { error } = await supabase2.from("doc_suggestion").update({
936
1035
  status: "dismissed",
937
1036
  dismissed_reason: reason || "Dismissed by validation agent",
@@ -964,6 +1063,7 @@ ${output}` }]
964
1063
  }
965
1064
  }
966
1065
  const parts = [];
1066
+ if (resolved > 0) parts.push(`${resolved} resolved`);
967
1067
  if (validated > 0) parts.push(`${validated} valid`);
968
1068
  if (dismissed > 0) parts.push(`${dismissed} dismissed`);
969
1069
  if (adjusted > 0) parts.push(`${adjusted} adjusted`);
@@ -976,6 +1076,151 @@ ${output}` }]
976
1076
  };
977
1077
  }
978
1078
  // -----------------------------------------------------------------
1079
+ // Lead Generation tools
1080
+ // -----------------------------------------------------------------
1081
+ case "agent-check-lead-exists": {
1082
+ const websiteUrl = sanitizeString(args2.website_url, 500).replace(/\/+$/, "");
1083
+ const companyName = sanitizeString(args2.company_name, 500);
1084
+ if (!websiteUrl && !companyName) {
1085
+ throw new Error("At least one of website_url or company_name is required");
1086
+ }
1087
+ const checks = [];
1088
+ if (websiteUrl) {
1089
+ const { data: urlMatch } = await supabase2.from("lead").select("id, company_name, website_url").eq("website_url", websiteUrl).maybeSingle();
1090
+ if (urlMatch) {
1091
+ return {
1092
+ content: [{
1093
+ type: "text",
1094
+ text: `DUPLICATE: Lead already exists (URL match). ID: ${urlMatch.id}, Company: ${urlMatch.company_name}. SKIP this company.`
1095
+ }]
1096
+ };
1097
+ }
1098
+ checks.push(`URL "${websiteUrl}" not found`);
1099
+ }
1100
+ if (companyName) {
1101
+ const normalized = normalizeCompanyName(companyName);
1102
+ if (normalized.length >= 3) {
1103
+ const { data: nameMatches } = await supabase2.from("lead").select("id, company_name, website_url").ilike("company_name", `%${normalized.split(" ")[0]}%`).limit(20);
1104
+ if (nameMatches) {
1105
+ for (const match of nameMatches) {
1106
+ const matchNorm = normalizeCompanyName(match.company_name);
1107
+ if (textSimilarity(normalized, matchNorm) > 0.6) {
1108
+ return {
1109
+ content: [{
1110
+ type: "text",
1111
+ text: `DUPLICATE: Similar company found. ID: ${match.id}, Name: "${match.company_name}" (${match.website_url}). SKIP this company.`
1112
+ }]
1113
+ };
1114
+ }
1115
+ }
1116
+ }
1117
+ }
1118
+ checks.push(`Name "${companyName}" has no similar matches`);
1119
+ }
1120
+ return {
1121
+ content: [{
1122
+ type: "text",
1123
+ text: `NOT FOUND: No duplicate detected. ${checks.join(". ")}. Safe to proceed.`
1124
+ }]
1125
+ };
1126
+ }
1127
+ case "agent-save-lead": {
1128
+ const companyName = sanitizeString(args2.company_name, 500);
1129
+ const websiteUrl = sanitizeString(args2.website_url, 500).replace(/\/+$/, "");
1130
+ if (!companyName) throw new Error("company_name is required");
1131
+ if (!websiteUrl) throw new Error("website_url is required");
1132
+ const { data: existing } = await supabase2.from("lead").select("id, company_name").eq("website_url", websiteUrl).maybeSingle();
1133
+ if (existing) {
1134
+ return {
1135
+ content: [{
1136
+ type: "text",
1137
+ text: `Lead already exists (URL match). ID: ${existing.id}, Company: "${existing.company_name}". Skipped.`
1138
+ }]
1139
+ };
1140
+ }
1141
+ const normalized = normalizeCompanyName(companyName);
1142
+ if (normalized.length >= 3) {
1143
+ const { data: nameMatches } = await supabase2.from("lead").select("id, company_name").ilike("company_name", `%${normalized.split(" ")[0]}%`).limit(20);
1144
+ if (nameMatches) {
1145
+ for (const match of nameMatches) {
1146
+ if (textSimilarity(normalized, normalizeCompanyName(match.company_name)) > 0.6) {
1147
+ return {
1148
+ content: [{
1149
+ type: "text",
1150
+ text: `Lead already exists (name match). ID: ${match.id}, Name: "${match.company_name}". Skipped.`
1151
+ }]
1152
+ };
1153
+ }
1154
+ }
1155
+ }
1156
+ }
1157
+ const fitScore = args2.fit_score ? clamp(Number(args2.fit_score), 1, 10) : null;
1158
+ const { data: inserted, error } = await supabase2.from("lead").insert({
1159
+ company_name: companyName,
1160
+ website_url: websiteUrl,
1161
+ industry: args2.industry ? sanitizeString(args2.industry, 200) : null,
1162
+ region: args2.region ? sanitizeString(args2.region, 200) : null,
1163
+ description: args2.description ? sanitizeString(args2.description, 2e3) : null,
1164
+ potential_fit: args2.potential_fit ? sanitizeString(args2.potential_fit, 2e3) : null,
1165
+ fit_score: fitScore,
1166
+ estimated_company_size: args2.estimated_company_size ? sanitizeString(args2.estimated_company_size, 50) : null,
1167
+ kvk_number: args2.kvk_number ? sanitizeString(args2.kvk_number, 20) : null,
1168
+ contact_name: args2.contact_name ? sanitizeString(args2.contact_name, 200) : null,
1169
+ contact_role: args2.contact_role ? sanitizeString(args2.contact_role, 200) : null,
1170
+ contact_email: args2.contact_email ? sanitizeString(args2.contact_email, 200) : null,
1171
+ contact_phone: args2.contact_phone ? sanitizeString(args2.contact_phone, 50) : null,
1172
+ contact_linkedin: args2.contact_linkedin ? sanitizeString(args2.contact_linkedin, 500) : null,
1173
+ general_email: args2.general_email ? sanitizeString(args2.general_email, 200) : null,
1174
+ general_phone: args2.general_phone ? sanitizeString(args2.general_phone, 50) : null,
1175
+ source_url: args2.source_url ? sanitizeString(args2.source_url, 500) : null,
1176
+ target_id: args2.target_id ? sanitizeString(args2.target_id, 100) : null,
1177
+ status: "new"
1178
+ }).select("id").single();
1179
+ if (error) throw new Error(`Failed to save lead: ${error.message}`);
1180
+ return {
1181
+ content: [{
1182
+ type: "text",
1183
+ text: `Lead saved: "${companyName}" (${websiteUrl}). ID: ${inserted.id}. Fit score: ${fitScore ?? "N/A"}.`
1184
+ }]
1185
+ };
1186
+ }
1187
+ case "agent-save-email-draft": {
1188
+ const leadId = sanitizeString(args2.lead_id, 100);
1189
+ const subject = sanitizeString(args2.subject, 200);
1190
+ let body = sanitizeString(args2.body, 5e3);
1191
+ const tone = sanitizeString(args2.tone, 50) || "professional";
1192
+ if (!leadId) throw new Error("lead_id is required");
1193
+ if (!subject) throw new Error("subject is required");
1194
+ if (!body) throw new Error("body is required");
1195
+ body = body.replace(/[\u2013\u2014]/g, ",");
1196
+ const { data: inserted, error } = await supabase2.from("lead_email_draft").insert({ lead_id: leadId, subject, body, tone, status: "draft" }).select("id").single();
1197
+ if (error) throw new Error(`Failed to save email draft: ${error.message}`);
1198
+ await supabase2.from("lead").update({ status: "email_drafted", updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", leadId).eq("status", "new");
1199
+ return {
1200
+ content: [{
1201
+ type: "text",
1202
+ text: `Email draft saved for lead ${leadId}. Draft ID: ${inserted.id}. Subject: "${subject}".`
1203
+ }]
1204
+ };
1205
+ }
1206
+ case "agent-complete-target": {
1207
+ const targetId = sanitizeString(args2.target_id, 100);
1208
+ const resultsCount = clamp(Number(args2.results_count) || 0, 0, 9999);
1209
+ if (!targetId) throw new Error("target_id is required");
1210
+ const { error } = await supabase2.from("lead_generation_target").update({
1211
+ status: "completed",
1212
+ results_count: resultsCount,
1213
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
1214
+ }).eq("id", targetId);
1215
+ if (error) throw new Error(`Failed to complete target: ${error.message}`);
1216
+ return {
1217
+ content: [{
1218
+ type: "text",
1219
+ text: `Target ${targetId} marked completed. ${resultsCount} leads found.`
1220
+ }]
1221
+ };
1222
+ }
1223
+ // -----------------------------------------------------------------
979
1224
  default:
980
1225
  return { content: [{ type: "text", text: `Unknown agent tool: ${name}` }] };
981
1226
  }
@@ -2767,7 +3012,11 @@ async function main() {
2767
3012
  "/api/save-documentation": "agent-save-documentation",
2768
3013
  "/api/list-findings": "agent-list-findings",
2769
3014
  "/api/get-documentation": "agent-get-documentation",
2770
- "/api/validate-suggestions": "agent-validate-suggestions"
3015
+ "/api/validate-suggestions": "agent-validate-suggestions",
3016
+ "/api/check-lead-exists": "agent-check-lead-exists",
3017
+ "/api/save-lead": "agent-save-lead",
3018
+ "/api/save-email-draft": "agent-save-email-draft",
3019
+ "/api/complete-target": "agent-complete-target"
2771
3020
  };
2772
3021
  const httpServer = createServer(async (req, res) => {
2773
3022
  const url = new URL(req.url ?? "/", `http://localhost:${httpPort}`);