@opennous/mcp 0.21.0 → 0.23.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 +54 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.21.0",
3
+ "version": "0.23.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
@@ -30,13 +30,14 @@
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
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")
33
34
  */
34
35
 
35
36
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
36
37
  import { z } from "zod";
37
38
  import { get, post } from "./client.js";
38
39
 
39
- export const SERVER_VERSION = "0.21.0";
40
+ export const SERVER_VERSION = "0.23.0";
40
41
 
41
42
  // ─── helpers ──────────────────────────────────────────────────────────────────
42
43
 
@@ -520,6 +521,8 @@ export function createServer() {
520
521
 
521
522
  const ws = s.workspace ?? {};
522
523
  lines.push(`WORKSPACE: ${ws.name || "(unnamed)"}${ws.website ? ` · ${ws.website}` : ""}${ws.business_type ? ` · ${ws.business_type}` : ""}`);
524
+ const pl = s.plan ?? {};
525
+ lines.push(`PLAN: ${pl.name || pl.id || "free"}${pl.crm_sync === false ? " (CRM sync not included — do not offer it)" : ""}`);
523
526
  lines.push("");
524
527
 
525
528
  const mark = (b) => (b ? "✓" : "✗");
@@ -529,7 +532,11 @@ export function createServer() {
529
532
  const ints = setup.integrations?.connected ?? [];
530
533
  lines.push(` ${mark((setup.integrations?.count ?? 0) > 0)} Integrations (${setup.integrations?.count ?? 0})${ints.length ? `: ${ints.map((i) => i.name).join(", ")}` : ""}`);
531
534
  const crm = setup.crm_sync ?? {};
532
- lines.push(` ${mark(crm.configured)} CRM sync${crm.configured ? `: ${(crm.providers ?? []).map((p) => p.provider).join(", ")}` : ""}${crm.pending_hygiene_proposals ? ` · ${crm.pending_hygiene_proposals} hygiene proposal(s) to review` : ""}`);
535
+ if (crm.available === false) {
536
+ lines.push(` – CRM sync (not on the ${pl.name || pl.id || "current"} plan)`);
537
+ } else {
538
+ lines.push(` ${mark(crm.configured)} CRM sync${crm.configured ? `: ${(crm.providers ?? []).map((p) => p.provider).join(", ")}` : ""}${crm.pending_hygiene_proposals ? ` · ${crm.pending_hygiene_proposals} hygiene proposal(s) to review` : ""}`);
539
+ }
533
540
  lines.push(` ${mark(setup.enrichment?.connected)} Enrichment${setup.enrichment?.provider ? `: ${setup.enrichment.provider}` : ""}`);
534
541
  lines.push(` ${mark((setup.webhooks?.count ?? 0) > 0 || (setup.triggers?.count ?? 0) > 0)} Events — ${setup.webhooks?.count ?? 0} webhook(s), ${setup.triggers?.count ?? 0} trigger(s)`);
535
542
 
@@ -606,7 +613,10 @@ export function createServer() {
606
613
  "update_gtm_profile, then call this to translate that context into a weighted set of scoring " +
607
614
  "signals so accounts get scored for fit. If a model already exists it is left alone unless you " +
608
615
  "pass force:true (use that when the GTM context has changed and the model should be rebuilt). If " +
609
- "it reports no GTM context yet, record some with update_gtm_profile first, then call this again.",
616
+ "it reports no GTM context yet, record some with update_gtm_profile first, then call this again. " +
617
+ "When building the playbook during onboarding, also ASK the user for a few closed-WON customer " +
618
+ "domains and closed-LOST domains — real outcomes sharpen the ICP. Record them with update_gtm_profile " +
619
+ "(e.g. section 'ICP': 'Closed-won: acme.com, globex.com; closed-lost: tinyco.io') so the model reflects who actually buys.",
610
620
  {
611
621
  force: z.boolean().optional()
612
622
  .describe("Rebuild the model even if one already exists — use when the GTM context has changed."),
@@ -867,5 +877,46 @@ export function createServer() {
867
877
  }
868
878
  );
869
879
 
880
+ // ===========================================================================
881
+ // TOOL: lead_coverage — GET /v2/people/coverage
882
+ // The attribute-based planning check: "how many <agency founders> do we already
883
+ // have, and how fresh?" — answered WITHOUT pasting identifiers. Use it before
884
+ // building a list elsewhere to see how much you already cover.
885
+ // ===========================================================================
886
+ server.tool(
887
+ "lead_coverage",
888
+ "Estimate how many people you ALREADY have matching a role/keyword, bucketed by enrichment " +
889
+ "freshness — the planning question before building a list (no identifiers to paste). E.g. " +
890
+ "title='founder', keyword='agency' → how many agency founders are already in your workspace, " +
891
+ "how many were never enriched or are stale (>90d, so re-enrich), and how many have a fresh " +
892
+ "verified email. Rough by design (title is precise; keyword matches title/company/department). " +
893
+ "For an exact net-new check against specific candidates, use check_leads with their identifiers.",
894
+ {
895
+ title: z.string().optional().describe("Role match, e.g. 'founder', 'VP Sales' (matches job_title)."),
896
+ keyword: z.string().optional().describe("Extra match across title/company/department, e.g. 'agency'."),
897
+ stale_days: z.number().optional().describe("Days after which enrichment counts as stale (default 90)."),
898
+ },
899
+ async ({ title, keyword, stale_days }) => {
900
+ if (!title && !keyword) {
901
+ return { content: [{ type: "text", text: "Pass a title and/or keyword, e.g. title='founder', keyword='agency'." }] };
902
+ }
903
+ const r = await get("/v2/people/coverage", { title, keyword, stale_days });
904
+ const lines = [
905
+ `COVERAGE — ${[title && `title~"${title}"`, keyword && `keyword~"${keyword}"`].filter(Boolean).join(" + ")}`,
906
+ ` ${r.total ?? 0} already in your workspace`,
907
+ ` ${r.needs_enrichment ?? 0} need (re-)enrichment (${r.never_enriched ?? 0} never enriched · ${r.stale ?? 0} stale >90d)`,
908
+ ` ${r.fresh_verified ?? 0} have a fresh verified email`,
909
+ ];
910
+ const sample = r.sample || [];
911
+ if (sample.length) {
912
+ lines.push("", "SAMPLE (oldest first):");
913
+ for (const s of sample.slice(0, 12)) {
914
+ lines.push(` ${[s.job_title, s.company].filter(Boolean).join(" @ ") || s.entity_id} [${s.enriched_at ? `enriched ${relAge(s.enriched_at)}` : "never enriched"}]`);
915
+ }
916
+ }
917
+ return { content: [{ type: "text", text: lines.join("\n") }] };
918
+ }
919
+ );
920
+
870
921
  return server;
871
922
  }