@opennous/mcp 0.20.0 → 0.22.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +98 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Nous MCP Server — Customer graph for GTM agents.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
package/src/server.js CHANGED
@@ -29,13 +29,15 @@
29
29
  * set_trigger — create an outbound event trigger (webhook); list_triggers reads them
30
30
  * list_triggers — list the workspace's event triggers + available events
31
31
  * lead_list_operations — the operations trail of a lead list (imports/enrich/push/replies), filterable
32
+ * check_leads — pre-spend coverage check: which candidates you already own / should re-enrich
33
+ * lead_coverage — attribute coverage estimate ("how many agency founders do we have, by freshness")
32
34
  */
33
35
 
34
36
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
35
37
  import { z } from "zod";
36
38
  import { get, post } from "./client.js";
37
39
 
38
- export const SERVER_VERSION = "0.20.0";
40
+ export const SERVER_VERSION = "0.22.0";
39
41
 
40
42
  // ─── helpers ──────────────────────────────────────────────────────────────────
41
43
 
@@ -812,5 +814,100 @@ export function createServer() {
812
814
  }
813
815
  );
814
816
 
817
+ // ===========================================================================
818
+ // TOOL: check_leads — POST /v2/dedup
819
+ // The pre-spend coverage check. Run this BEFORE building/buying a lead list in
820
+ // Apollo, Sales Navigator, DeepSearch, Clay, etc. Paste the candidate emails /
821
+ // LinkedIn URLs / company domains (all visible for free in the tool's preview)
822
+ // and find out what you already own — so you never re-buy a record you have,
823
+ // and you re-enrich stale ones instead of paying to acquire them again.
824
+ // ===========================================================================
825
+ server.tool(
826
+ "check_leads",
827
+ "Pre-spend coverage check — call this BEFORE building or buying a lead list elsewhere (Apollo, " +
828
+ "Sales Navigator, DeepSearch, Clay). Paste the candidate emails, LinkedIn URLs, or company " +
829
+ "domains (free in any tool's preview) and learn what you already have, so you don't pay twice. " +
830
+ "Returns counts: net_new (acquire + enrich these), needs_enrichment (you already OWN these but " +
831
+ "they're stale / not enriched in 90 days — re-enrich instead of re-buying), reusable (you have a " +
832
+ "fresh verified email — reuse, spend nothing), plus engaged/recent/bounced/unsubscribed/known to " +
833
+ "skip. Each result carries entity_id, email_status, enriched_at, and stale so you can act per-lead.",
834
+ {
835
+ emails: z.array(z.string()).optional().describe("Candidate email addresses (up to 50,000)."),
836
+ linkedin_urls: z.array(z.string()).optional().describe("Candidate LinkedIn profile URLs (up to 50,000)."),
837
+ domains: z.array(z.string()).optional().describe("Company domains — 'do I already have anyone here?' (up to 50,000)."),
838
+ },
839
+ async ({ emails, linkedin_urls, domains }) => {
840
+ const body = {};
841
+ if (emails?.length) body.emails = emails;
842
+ if (linkedin_urls?.length) body.linkedin_urls = linkedin_urls;
843
+ if (domains?.length) body.domains = domains;
844
+ if (!Object.keys(body).length) {
845
+ return { content: [{ type: "text", text: "Pass at least one of: emails, linkedin_urls, domains." }] };
846
+ }
847
+ const r = await post("/v2/dedup", body);
848
+ const s = r.summary || {};
849
+ const lines = [
850
+ `COVERAGE (${s.total ?? 0} checked)`,
851
+ ` net_new ${s.net_new ?? 0} → acquire + enrich`,
852
+ ` needs_enrichment ${s.needs_enrichment ?? 0} → you OWN these but stale (>90d) → re-enrich, don't re-buy`,
853
+ ` reusable ${s.reusable ?? 0} → fresh verified email on file → reuse, spend nothing`,
854
+ ` engaged ${s.engaged ?? 0} → in an active conversation, don't cold-send`,
855
+ ` recent ${s.recent ?? 0} → contacted <30d, defer`,
856
+ ` known ${s.known ?? 0} → company already in the workspace`,
857
+ ` bounced/unsub ${(s.bounced ?? 0) + (s.unsubscribed ?? 0) + (s.suppressed ?? 0)} → skip`,
858
+ ];
859
+ // Surface a few stale entities the caller should re-enrich (with their last date).
860
+ const stale = (r.results || []).filter(x => x.entity_id && x.stale).slice(0, 15);
861
+ if (stale.length) {
862
+ lines.push("", "RE-ENRICH (sample):");
863
+ for (const x of stale) {
864
+ lines.push(` ${x.value} [${x.enriched_at ? `last enriched ${relAge(x.enriched_at)}` : "never enriched"}] ${x.entity_id}`);
865
+ }
866
+ }
867
+ return { content: [{ type: "text", text: lines.join("\n") }] };
868
+ }
869
+ );
870
+
871
+ // ===========================================================================
872
+ // TOOL: lead_coverage — GET /v2/people/coverage
873
+ // The attribute-based planning check: "how many <agency founders> do we already
874
+ // have, and how fresh?" — answered WITHOUT pasting identifiers. Use it before
875
+ // building a list elsewhere to see how much you already cover.
876
+ // ===========================================================================
877
+ server.tool(
878
+ "lead_coverage",
879
+ "Estimate how many people you ALREADY have matching a role/keyword, bucketed by enrichment " +
880
+ "freshness — the planning question before building a list (no identifiers to paste). E.g. " +
881
+ "title='founder', keyword='agency' → how many agency founders are already in your workspace, " +
882
+ "how many were never enriched or are stale (>90d, so re-enrich), and how many have a fresh " +
883
+ "verified email. Rough by design (title is precise; keyword matches title/company/department). " +
884
+ "For an exact net-new check against specific candidates, use check_leads with their identifiers.",
885
+ {
886
+ title: z.string().optional().describe("Role match, e.g. 'founder', 'VP Sales' (matches job_title)."),
887
+ keyword: z.string().optional().describe("Extra match across title/company/department, e.g. 'agency'."),
888
+ stale_days: z.number().optional().describe("Days after which enrichment counts as stale (default 90)."),
889
+ },
890
+ async ({ title, keyword, stale_days }) => {
891
+ if (!title && !keyword) {
892
+ return { content: [{ type: "text", text: "Pass a title and/or keyword, e.g. title='founder', keyword='agency'." }] };
893
+ }
894
+ const r = await get("/v2/people/coverage", { title, keyword, stale_days });
895
+ const lines = [
896
+ `COVERAGE — ${[title && `title~"${title}"`, keyword && `keyword~"${keyword}"`].filter(Boolean).join(" + ")}`,
897
+ ` ${r.total ?? 0} already in your workspace`,
898
+ ` ${r.needs_enrichment ?? 0} need (re-)enrichment (${r.never_enriched ?? 0} never enriched · ${r.stale ?? 0} stale >90d)`,
899
+ ` ${r.fresh_verified ?? 0} have a fresh verified email`,
900
+ ];
901
+ const sample = r.sample || [];
902
+ if (sample.length) {
903
+ lines.push("", "SAMPLE (oldest first):");
904
+ for (const s of sample.slice(0, 12)) {
905
+ lines.push(` ${[s.job_title, s.company].filter(Boolean).join(" @ ") || s.entity_id} [${s.enriched_at ? `enriched ${relAge(s.enriched_at)}` : "never enriched"}]`);
906
+ }
907
+ }
908
+ return { content: [{ type: "text", text: lines.join("\n") }] };
909
+ }
910
+ );
911
+
815
912
  return server;
816
913
  }