@opennous/mcp 0.42.0 → 0.44.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 +89 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.42.0",
3
+ "version": "0.44.0",
4
4
  "description": "Nous — the Context Graph for AI Agents.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
package/src/server.js CHANGED
@@ -23,8 +23,8 @@
23
23
  * search_notes — semantic search over saved notes & documents
24
24
  * get_workspace_status — what's set up in this workspace + a ranked next_steps list (call first)
25
25
  * set_workspace_profile— agent-driven onboarding: set the workspace's name, site, type, ICP
26
- * build_scoring_model — build/rebuild the ICP scoring model from the recorded GTM context
27
- * record_closed_deals — build the ICP model from real closed-won/lost deals (contrastive lift)
26
+ * build_icp_model — build/rebuild the ICP scoring model from the recorded GTM context
27
+ * train_icp_model — build the ICP model from real closed-won/lost deals (contrastive lift)
28
28
  * sync_icp — sync the user's EXISTING ICP/positioning files into Nous (file → graph)
29
29
  * export_icp_model — get the learned ICP model as a block to write back into their ICP file (graph → file)
30
30
  * connect_integration — connect a key-based integration (Apollo, Prospeo, HubSpot, …)
@@ -34,7 +34,7 @@
34
34
  * list_triggers — list the workspace's event triggers + available events
35
35
  * get_routing_preferences — Claude Code routing prefs to default GTM to Nous (write to CLAUDE.md)
36
36
  * lead_list_operations — the operations trail of a lead list (imports/enrich/push/replies), filterable
37
- * coverage — pre-spend coverage: exact per-lead check (identifiers) or attribute estimate (title/keyword)
37
+ * get_coverage — pre-spend coverage: exact per-lead check (identifiers) or attribute estimate (title/keyword)
38
38
  * enrich_leads — find missing emails for a lead list (two-step: dry-run cost preview, then confirm)
39
39
  * verify_leads — validate email deliverability for a lead list (two-step preview, then confirm)
40
40
  */
@@ -89,9 +89,9 @@ const pct = (c) => `${Math.round((c ?? 0) * 100)}%`;
89
89
  // Gong/Granola). Returned by get_routing_preferences; the agent writes it into
90
90
  // the user's CLAUDE.md (Claude Code) or the user pastes it into claude.ai org
91
91
  // preferences. Mirrors the Install page's "short" version.
92
- const ROUTING_PREFERENCES = `# Nous is this workspace's customer graph
92
+ const ROUTING_PREFERENCES = `# Nous is this workspace's GTM context graph
93
93
 
94
- Nous is our customer graph for GTM. It resolves every person, conversation, and
94
+ Nous is our context graph for GTM. It resolves every person, conversation, and
95
95
  touchpoint across our GTM tool stack into one account record, with each fact's
96
96
  confidence and freshness, the full interaction timeline, a 0-100 ICP fit score on
97
97
  every account, plus our own ICP, positioning, and pricing. Agents read engineered
@@ -249,11 +249,27 @@ export function createServer() {
249
249
  "get_account",
250
250
  "Get the full account record for a person or company — the durable FACTS we've learned about them " +
251
251
  "(their atomic memory: budget, authority, pain, stack, plans), every attribute (claim) with its " +
252
- "confidence and freshness, plus the recent activity timeline. Pass an email or entity UUID. " +
253
- "For a task-specific, ranked view, prefer get_context.",
254
- { id: z.string().describe("Email address or entity UUID") },
255
- async ({ id }) => {
256
- const rec = await get(`/v2/accounts/${encodeURIComponent(id)}`);
252
+ "confidence and freshness, plus what they actually SAID and did, ranked by how much it tells you. " +
253
+ "Pass an email or entity UUID, and the intent you're working toward so the record is shaped for it.",
254
+ {
255
+ id: z.string().describe("Email address or entity UUID"),
256
+ intent: z
257
+ .enum(["meeting_prep", "call_prep", "account_review", "follow_up", "draft_email"])
258
+ .optional()
259
+ .describe(
260
+ "What you're about to do. Shapes how much of their history comes back: a meeting brief wants " +
261
+ "the conversation in detail, an email draft wants one hook. Defaults to account_review.",
262
+ ),
263
+ },
264
+ async ({ id, intent }) => {
265
+ // Ask for the RANKED record, not the raw one.
266
+ //
267
+ // The timeline this tool used to print was chronological and contentless —
268
+ // "3d ago email_sent" — which tells an agent that something happened and
269
+ // nothing about what. Ranked activity carries the source and the substance,
270
+ // so the model reads what was actually said instead of a list of event names.
271
+ const q = new URLSearchParams({ intent: intent ?? "account_review", compress: "1" });
272
+ const rec = await get(`/v2/accounts/${encodeURIComponent(id)}?${q}`);
257
273
  const lines = [`${rec.type} · ${rec.entity_id}`, ""];
258
274
 
259
275
  if (rec.icp) {
@@ -275,13 +291,37 @@ export function createServer() {
275
291
  }
276
292
  lines.push("");
277
293
  }
278
- const obs = rec.recent_observations ?? [];
279
- if (obs.length) {
294
+ // What they actually said and did — the most telling first, each with the
295
+ // system it came from, so a claim in the answer can always be traced back.
296
+ const activity = rec.key_activity ?? [];
297
+ if (activity.length) {
298
+ lines.push(`WHAT HAPPENED (${activity.length} most telling):`);
299
+ for (const a of activity) {
300
+ const when = a.when ? relAge(a.when) : "";
301
+ const head = ` ${a.what}${a.source ? ` · ${a.source}` : ""}${when ? ` · ${when}` : ""}`;
302
+ lines.push(a.detail ? `${head}\n ${a.detail}` : head);
303
+ }
304
+ lines.push("");
305
+ }
306
+
307
+ // Say what was left out, and why. An agent that is handed 18 of 300
308
+ // interactions and does not know it will happily conclude that nothing else
309
+ // ever happened.
310
+ const sum = rec.activity_summary;
311
+ if (sum?.note) lines.push(sum.note);
312
+ else if (sum?.total_observations) {
313
+ lines.push(`${sum.total_observations} interactions on record.`);
314
+ }
315
+
316
+ // Fall back to the raw timeline if an older API didn't rank anything.
317
+ if (!activity.length && rec.recent_observations?.length) {
318
+ const obs = rec.recent_observations;
280
319
  lines.push(`TIMELINE (${obs.length}):`);
281
320
  for (const o of obs.slice(0, 30)) {
282
321
  lines.push(` ${whenLabel(o.property, o.observed_at)} ${fmtType(o.property)}`);
283
322
  }
284
323
  }
324
+
285
325
  return { content: [{ type: "text", text: lines.join("\n").trim() }] };
286
326
  }
287
327
  );
@@ -507,9 +547,15 @@ export function createServer() {
507
547
  return { content: [{ type: "text", text: "Nothing needs attention right now." }] };
508
548
  }
509
549
  // Upcoming meetings carry a `when` — render the absolute local date+time.
550
+ //
551
+ // Each item also names where it came from: the calendar holding the call, the
552
+ // transcript the promise was captured from. An agent that can cite the call
553
+ // someone made a promise ON is making an argument; one that just asserts the
554
+ // promise is asking to be trusted.
510
555
  const lines = r.items.map(it => {
511
556
  const when = it.when ? `${fmtWhen(it.when)} — ` : "";
512
- return ` ${when}${it.entity_name ?? it.entity_id} ${it.what}\n → ${it.suggested_action}`;
557
+ const from = it.source ? ` [${it.source}]` : "";
558
+ return ` ${when}${it.entity_name ?? it.entity_id} — ${it.what}${from}\n → ${it.suggested_action}`;
513
559
  });
514
560
  return { content: [{ type: "text", text: `Needs attention (${r.items.length}):\n${lines.join("\n")}` }] };
515
561
  }
@@ -638,6 +684,11 @@ export function createServer() {
638
684
  "whenever you edit a policy file in the repo (e.g. references/voice.md, outreach rules), passing the " +
639
685
  "file's new content and its path, so Nous mirrors it and every other agent obeys the same rules. An " +
640
686
  "edited playbook file that isn't synced is silently inert — other agents keep reading the old rules. " +
687
+ "MIRROR, DO NOT REWRITE: when the user already has a playbook file, sync it AS-IS. Their file is the " +
688
+ "author and Nous is the mirror — always pass file_path so the next sync knows where an in-app edit " +
689
+ "lands. 'Improving' their wording on the way through means the copy in Nous silently disagrees with " +
690
+ "the copy in their repo, and they will trust neither. If a file looks wrong, SAY SO; don't fix it in " +
691
+ "transit. " +
641
692
  "(For the ICP/context files specifically, sync_icp is the sync — use that one.)",
642
693
  syncPlaybookSchema, syncPlaybookHandler);
643
694
 
@@ -752,7 +803,19 @@ export function createServer() {
752
803
 
753
804
  const mark = (b) => (b ? "✓" : "✗");
754
805
  lines.push("SETUP:");
755
- lines.push(` ${mark(setup.onboarding?.done)} Onboarding${setup.onboarding?.done ? "" : `missing ${(setup.onboarding?.missing ?? []).join(", ") || "details"}`}`);
806
+ // The ICP first, because it IS the gate a workspace without one is not set up, no
807
+ // matter how many integrations are green. If it's mirrored from a file in their repo,
808
+ // say so and say where: that file is the author, and editing anything else is a way of
809
+ // losing their work on the next sync.
810
+ const icp = setup.icp ?? {};
811
+ lines.push(
812
+ ` ${mark(icp.done)} ICP${icp.done
813
+ ? (icp.source === "claude_code" && icp.file_path
814
+ ? ` — mirrored from ${icp.file_path} (their repo is the author; edit the FILE, then sync)`
815
+ : " — authored in Nous")
816
+ : " — MISSING. The workspace is not set up until this exists. Scan their repo before you ask them anything."}`
817
+ );
818
+ lines.push(` ${mark(setup.onboarding?.done)} Profile${setup.onboarding?.done ? "" : ` — missing ${(setup.onboarding?.missing ?? []).join(", ") || "details"}`}`);
756
819
  lines.push(` ${mark(setup.gtm_playbook?.done)} GTM playbook${setup.gtm_playbook?.model ? " (scoring model live)" : ""}${setup.gtm_playbook?.stale_facts ? ` · ${setup.gtm_playbook.stale_facts} stale fact(s)` : ""}`);
757
820
  if (setup.icp_sync) {
758
821
  const sy = setup.icp_sync;
@@ -842,14 +905,14 @@ export function createServer() {
842
905
  );
843
906
 
844
907
  // ===========================================================================
845
- // TOOL: build_scoring_model — POST /v2/workspace/scoring-model
908
+ // TOOL: build_icp_model — POST /v2/workspace/scoring-model
846
909
  // The second half of building the GTM playbook. The agent syncs the GTM context
847
910
  // from the user's files with sync_icp, then calls this to turn it into a weighted
848
911
  // ICP scoring model. After this, accounts get scored for fit and
849
912
  // get_workspace_status shows the playbook as done.
850
913
  // ===========================================================================
851
914
  server.tool(
852
- "build_scoring_model",
915
+ "build_icp_model",
853
916
  "Build (or rebuild) the user's ICP scoring model from their synced GTM context. This is " +
854
917
  "the second half of setting up the GTM playbook: first sync the user's ICP/positioning/pricing " +
855
918
  "files with sync_icp, then call this to translate that context into a weighted set of scoring " +
@@ -858,7 +921,7 @@ export function createServer() {
858
921
  "pass force:true (use that when the context files have changed and the model should be rebuilt). If " +
859
922
  "it reports no GTM context yet, sync the user's context files with sync_icp first, then call this again. " +
860
923
  "STRONGER than this tool: if the user can name a few closed-WON and closed-LOST customer domains, " +
861
- "call record_closed_deals instead (or as well) — it trains the model on real outcomes via " +
924
+ "call train_icp_model instead (or as well) — it trains the model on real outcomes via " +
862
925
  "contrastive lift, which beats a model inferred from a description.",
863
926
  {
864
927
  force: z.boolean().optional()
@@ -882,7 +945,7 @@ export function createServer() {
882
945
  }
883
946
  if (msg.includes("model_exists")) {
884
947
  return { content: [{ type: "text", text:
885
- "A scoring model already exists. Call build_scoring_model again with force:true to rebuild it from the current GTM context." }] };
948
+ "A scoring model already exists. Call build_icp_model again with force:true to rebuild it from the current GTM context." }] };
886
949
  }
887
950
  throw e;
888
951
  }
@@ -890,11 +953,11 @@ export function createServer() {
890
953
  );
891
954
 
892
955
  // ===========================================================================
893
- // TOOL: record_closed_deals — POST /v2/workspace/closed-deals
956
+ // TOOL: train_icp_model — POST /v2/workspace/closed-deals
894
957
  // Build the ICP model from REAL outcomes via contrastive lift (won vs lost).
895
958
  // ===========================================================================
896
959
  server.tool(
897
- "record_closed_deals",
960
+ "train_icp_model",
898
961
  "Build (or sharpen) the ICP scoring model from the user's REAL closed deals. Pass closed-WON " +
899
962
  "customer domains and closed-LOST domains; Nous enriches each, links the contacts you already " +
900
963
  "have there, and runs contrastive lift (what's true of winners but not losers) to discover the " +
@@ -986,7 +1049,7 @@ export function createServer() {
986
1049
  const sig = r.signals ?? [];
987
1050
  if (r.model_status === "created" && sig.length) {
988
1051
  lines.push("", `Built the ICP scoring model — ${sig.length} signal${sig.length === 1 ? "" : "s"}.`);
989
- lines.push("Next: if the user can name a few closed-won + closed-lost domains, call record_closed_deals to sharpen it on real outcomes, then call export_icp_model to write the learned model back into their ICP file.");
1052
+ lines.push("Next: if the user can name a few closed-won + closed-lost domains, call train_icp_model to sharpen it on real outcomes, then call export_icp_model to write the learned model back into their ICP file.");
990
1053
  } else if (r.model_status === "no_icp_memory") {
991
1054
  lines.push("", "Synced, but there wasn't enough ICP content to build a scoring model — make sure the ICP section has real content.");
992
1055
  } else {
@@ -1019,7 +1082,7 @@ export function createServer() {
1019
1082
  "calibration gap) as a ready-to-write markdown block, and write it back into the user's own ICP " +
1020
1083
  "file. This is the payoff of the symbiosis: their file keeps the words, Nous keeps the model, and " +
1021
1084
  "this writes the model under their words. CLAUDE CODE flow: call this after sync_icp or after " +
1022
- "record_closed_deals, then with your file tools open `target_path`, and if the file already has a " +
1085
+ "train_icp_model, then with your file tools open `target_path`, and if the file already has a " +
1023
1086
  "block between '<!-- nous:icp start -->' and '<!-- nous:icp end -->' REPLACE that whole block with " +
1024
1087
  "the returned `block`; if not, append the returned `block` (e.g. replacing a '## [To refine]' " +
1025
1088
  "placeholder). Never edit inside the markers by hand — this tool regenerates them. Everything " +
@@ -1030,11 +1093,11 @@ export function createServer() {
1030
1093
  if (!r.has_model) {
1031
1094
  return { content: [{ type: "text", text:
1032
1095
  "No ICP scoring model yet. Sync the user's ICP file with sync_icp first (or build one with " +
1033
- "build_scoring_model / record_closed_deals), then call this to write it back." }] };
1096
+ "build_icp_model / train_icp_model), then call this to write it back." }] };
1034
1097
  }
1035
1098
  const note = r.has_outcomes
1036
1099
  ? "This model is trained on real closed deals (lift + calibration shown)."
1037
- : "This model is seeded from the ICP only — add closed deals with record_closed_deals to sharpen it.";
1100
+ : "This model is seeded from the ICP only — add closed deals with train_icp_model to sharpen it.";
1038
1101
  return { content: [{ type: "text", text:
1039
1102
  `Write the block below into ${r.target_path} with your file editor — replace any existing block ` +
1040
1103
  `between the nous:icp markers, or append it if there's none (create the file/section if absent). ` +
@@ -1253,14 +1316,14 @@ export function createServer() {
1253
1316
  );
1254
1317
 
1255
1318
  // ===========================================================================
1256
- // TOOL: coverage — POST /v2/dedup (exact) | GET /v2/people/coverage (estimate)
1319
+ // TOOL: get_coverage — POST /v2/dedup (exact) | GET /v2/people/coverage (estimate)
1257
1320
  // "What do I already have?" before spending on a list elsewhere. One tool, two
1258
1321
  // modes: pass identifiers for an EXACT per-lead net-new/re-enrich/reuse check
1259
1322
  // (the pre-spend gate), or a title/keyword for a rough attribute ESTIMATE.
1260
1323
  // (Replaces the former check_leads + lead_coverage tools.)
1261
1324
  // ===========================================================================
1262
1325
  server.tool(
1263
- "coverage",
1326
+ "get_coverage",
1264
1327
  "(Nous Cloud only) Check what you ALREADY have before spending on a list elsewhere (Apollo, Sales Navigator, Clay). " +
1265
1328
  "Two modes:\n" +
1266
1329
  " • EXACT — pass candidate identifiers (emails / linkedin_urls / domains, free in any tool's " +