@opennous/mcp 0.40.0 → 0.42.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 +52 -84
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.40.0",
3
+ "version": "0.42.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
@@ -18,15 +18,15 @@
18
18
  * query — retrieve + summarise a corpus of activity across many people
19
19
  * attention — what needs your attention (accounts gone quiet, facts decayed)
20
20
  * verify — re-check a fact before acting on it
21
- * get_gtm_profile — the user's GTM profile (ICP, market, pricing, product, competitors)
21
+ * get_playbook read the user's own rules: voice, outreach, icp, positioning
22
22
  * save_note — attach a note/document (meeting brief, transcript, prep) to a contact
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
26
  * build_scoring_model — build/rebuild the ICP scoring model from the recorded GTM context
27
27
  * record_closed_deals — build the ICP model from real closed-won/lost deals (contrastive lift)
28
- * get_icp — sync the user's EXISTING ICP/positioning files into Nous (file → graph)
29
- * get_icp_model — get the learned ICP model as a block to write back into their ICP file (graph → file)
28
+ * sync_icp — sync the user's EXISTING ICP/positioning files into Nous (file → graph)
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, …)
31
31
  * configure_crm_sync — set CRM sync rules (auto-sync, create policy, hygiene cadence)
32
32
  * sync_crm_now — run an immediate incremental/full CRM pull (don't wait for the daily cron)
@@ -43,7 +43,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
43
43
  import { z } from "zod";
44
44
  import { get, post } from "./client.js";
45
45
 
46
- export const SERVER_VERSION = "0.39.0";
46
+ export const SERVER_VERSION = "0.42.0";
47
47
 
48
48
  // ─── helpers ──────────────────────────────────────────────────────────────────
49
49
 
@@ -115,14 +115,22 @@ Nous first even when the user never says "Nous":
115
115
  - What needs attention, what has gone quiet -> attention
116
116
  - Your action items / what you owe an account -> get_action_items
117
117
  - A fact looks stale before you act on it -> verify
118
- - Our ICP, market, pricing, positioning -> get_gtm_profile
119
- - Our ICP/positioning lives in our own files -> get_icp (file → graph) / get_icp_model (graph → file)
120
- - Our own GTM shifted (repriced, repositioned) -> edit the context file, then re-run get_icp
118
+ - Our own rules: ICP, positioning, voice, outreach -> get_playbook
119
+ - Our ICP/positioning lives in our own files -> sync_icp (file → graph) / export_icp_model (graph → file)
120
+ - You edited an ICP / context / playbook file -> re-sync THIS turn: sync_icp (ICP/context) or sync_playbook (a playbook)
121
121
  - A brief / note / transcript on a contact -> save_note / search_notes
122
122
  - What's set up here and what to do next -> get_workspace_status
123
123
 
124
- Read get_gtm_profile at the start of GTM work and write back what changed at the
125
- end. After every interaction you help with, call record so the record stays current.`;
124
+ Read get_playbook at the start of GTM work for the user's own rules (voice,
125
+ outreach, icp, positioning). After every interaction you help with, call record so
126
+ the record stays current.
127
+
128
+ CRITICAL — edits don't apply until you sync. Editing an ICP / context / playbook
129
+ file (context/icp.md, positioning.md, references/voice.md, …) does NOT change the
130
+ ICP score, the exclusions, or what any other agent reads until it is synced into
131
+ the graph. After ANY such edit you MUST call sync_icp (for ICP/context files) or
132
+ sync_playbook (for a playbook) in the SAME turn. Never leave an edited file
133
+ unsynced — an unsynced edit is silently inert.`;
126
134
 
127
135
  // ─── factory ──────────────────────────────────────────────────────────────────
128
136
 
@@ -579,53 +587,9 @@ export function createServer() {
579
587
  }
580
588
  );
581
589
 
582
- // ===========================================================================
583
- // TOOL: get_gtm_profile — GET /v2/workspace/facts
584
- // The user's OWN GTM profile: ICP, market, product, pricing, competitors.
585
- // Use this for any question about the user's business — NOT get_account.
586
- // ===========================================================================
587
- const gtmProfileDescription =
588
- "Get the user's OWN GTM profile — their ICP, target market, product, pricing, " +
589
- "competitors, and positioning. These are NOT facts about a person or company; they are " +
590
- "the user's own business profile. Use this for any question about the user's ICP, target " +
591
- "buyer, pricing, market, or differentiators. ALWAYS prefer this over query/get_account " +
592
- "when the question is about the user's business.";
593
- const gtmProfileSchema = {
594
- categories: z.array(z.string()).optional()
595
- .describe("Optional category filter, e.g. ['ICP'] or ['Pricing','Competitors']. Omit for all."),
596
- limit: z.number().min(1).max(500).optional()
597
- .describe("Max facts to return (default 50)"),
598
- };
599
- const gtmProfileHandler = async ({ categories, limit }) => {
600
- const params = {};
601
- if (categories?.length) params.categories = categories.join(",");
602
- if (limit != null) params.limit = limit;
603
- const r = await get("/v2/workspace/facts", params);
604
- if (!r.facts?.length) {
605
- return { content: [{ type: "text", text:
606
- "No GTM profile recorded yet. The user can set it up in the GTM Context tab." }] };
607
- }
608
- const groups = {};
609
- for (const f of r.facts) (groups[f.category] ??= []).push(f);
610
- const lines = [];
611
- for (const [cat, facts] of Object.entries(groups)) {
612
- lines.push(`${cat.toUpperCase()} (${facts.length}):`);
613
- for (const f of facts) {
614
- // Flag AI-drafted facts (confidence < 1) and ones not confirmed in a long
615
- // time, so the agent treats them as provisional and prefers fresh,
616
- // user-confirmed facts when they conflict.
617
- const ageDays = f.recorded_at ? Math.floor((Date.now() - new Date(f.recorded_at).getTime()) / 86400000) : 0;
618
- const tags = [];
619
- if (typeof f.confidence === "number" && f.confidence < 1) tags.push("inferred");
620
- if (ageDays >= 90) tags.push("stale");
621
- const tag = tags.length ? ` (${tags.join(", ")})` : "";
622
- lines.push(` ${f.content}${tag} [${relAge(f.recorded_at)}]`);
623
- }
624
- lines.push("");
625
- }
626
- return { content: [{ type: "text", text: lines.join("\n").trim() }] };
627
- };
628
- server.tool("get_gtm_profile", gtmProfileDescription, gtmProfileSchema, gtmProfileHandler);
590
+ // get_gtm_profile removed: the user's GTM lives in their files, mirrored into
591
+ // the graph as playbooks (get_playbook) plus the learned ICP model. Read
592
+ // get_playbook for the user's own rules, ICP, and positioning.
629
593
 
630
594
  // ===========================================================================
631
595
  // TOOLS: get_playbook / sync_playbook — the POLICY layer (vs. facts).
@@ -670,15 +634,17 @@ export function createServer() {
670
634
  `Synced the ${r.playbook?.kind || kind} playbook into Nous (v${r.playbook?.version}). Other agents now read the same rules.` }] };
671
635
  };
672
636
  server.tool("sync_playbook",
673
- "Push a playbook's content into Nous so the graph stays current. Call this AFTER you edit a policy file " +
674
- "in the repo (e.g. context/icp/icp.md, references/voice.md), passing the file's new content and its path, " +
675
- "so Nous mirrors it and every other agent obeys the same rules.",
637
+ "Push a playbook's content into Nous so the graph stays current. You MUST call this in the SAME turn " +
638
+ "whenever you edit a policy file in the repo (e.g. references/voice.md, outreach rules), passing the " +
639
+ "file's new content and its path, so Nous mirrors it and every other agent obeys the same rules. An " +
640
+ "edited playbook file that isn't synced is silently inert — other agents keep reading the old rules. " +
641
+ "(For the ICP/context files specifically, sync_icp is the sync — use that one.)",
676
642
  syncPlaybookSchema, syncPlaybookHandler);
677
643
 
678
644
  // The GTM context is no longer written through a dedicated MCP tool. In the file
679
645
  // symbiosis model the user's own files (context/icp.md, positioning.md, …) are
680
646
  // the source of truth: the agent edits those with its own file tools and calls
681
- // `get_icp` to sync them into the graph (and `get_icp_model` to write the learned
647
+ // `sync_icp` to sync them into the graph (and `export_icp_model` to write the learned
682
648
  // model back). The shared POST /v2/workspace/facts route still backs that import.
683
649
 
684
650
  // ===========================================================================
@@ -697,7 +663,7 @@ export function createServer() {
697
663
  "Notes are append-only and dated, so a contact builds a record across meetings — later you can " +
698
664
  "read the last few and see what changed. This is NOT for logging that an interaction happened " +
699
665
  "(use `record` with an interaction.* event for that), and NOT for the user's own GTM profile " +
700
- "(that lives in their context files — sync it with `get_icp`). Put the full text in `content` — it's kept for agents to read; the " +
666
+ "(that lives in their context files — sync it with `sync_icp`). Put the full text in `content` — it's kept for agents to read; the " +
701
667
  "UI shows the title and date, not the whole body.",
702
668
  {
703
669
  focus: z.string().describe("Who to attach it to — an email, LinkedIn URL, domain, or entity UUID (not a bare name)."),
@@ -790,7 +756,7 @@ export function createServer() {
790
756
  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)` : ""}`);
791
757
  if (setup.icp_sync) {
792
758
  const sy = setup.icp_sync;
793
- lines.push(` ⟳ ICP synced from ${sy.synced_from} (${relAge(sy.synced_at)})${sy.model_changed ? " · model has CHANGED since — run get_icp_model to refresh the file" : ""}`);
759
+ lines.push(` ⟳ ICP synced from ${sy.synced_from} (${relAge(sy.synced_at)})${sy.model_changed ? " · model has CHANGED since — run export_icp_model to refresh the file" : ""}`);
794
760
  }
795
761
  const ints = setup.integrations?.connected ?? [];
796
762
  lines.push(` ${mark((setup.integrations?.count ?? 0) > 0)} Integrations (${setup.integrations?.count ?? 0})${ints.length ? `: ${ints.map((i) => i.name).join(", ")}` : ""}`);
@@ -842,12 +808,12 @@ export function createServer() {
842
808
  "changing. " +
843
809
  "IMPORTANT for the ICP: before asking the user to describe their ICP from scratch, if you're in " +
844
810
  "Claude Code, look for an ICP they ALREADY wrote — folders like context/, .claude/, gtm/ and files " +
845
- "named icp*, positioning*, pricing*, competitors*. If you find them, read them and call get_icp to " +
811
+ "named icp*, positioning*, pricing*, competitors*. If you find them, read them and call sync_icp to " +
846
812
  "sync them (don't retype the ICP here); if none exists, scaffold a context/ folder (icp.md, " +
847
813
  "positioning.md, pricing.md, market.md, competitors.md, gtm-motion.md) from the conversation + your " +
848
- "site research, then get_icp it — so their ICP lives in their repo. (Not in Claude Code? Capture a " +
814
+ "site research, then sync_icp it — so their ICP lives in their repo. (Not in Claude Code? Capture a " +
849
815
  "first cut in the `icp` field here instead.) " +
850
- "After this, the next step is the context files: call get_icp to sync them into the graph.",
816
+ "After this, the next step is the context files: call sync_icp to sync them into the graph.",
851
817
  {
852
818
  name: z.string().optional().describe("The user's company / workspace name."),
853
819
  website: z.string().optional().describe("The company website (used to seed the GTM context)."),
@@ -871,14 +837,14 @@ export function createServer() {
871
837
  ].filter(Boolean);
872
838
  return { content: [{ type: "text", text:
873
839
  `Workspace profile saved.${set.length ? ` ${set.join(" · ")}.` : ""}\n` +
874
- `Next: call get_workspace_status to see what to set up next (usually syncing the ICP/context files with get_icp).` }] };
840
+ `Next: call get_workspace_status to see what to set up next (usually syncing the ICP/context files with sync_icp).` }] };
875
841
  }
876
842
  );
877
843
 
878
844
  // ===========================================================================
879
845
  // TOOL: build_scoring_model — POST /v2/workspace/scoring-model
880
846
  // The second half of building the GTM playbook. The agent syncs the GTM context
881
- // from the user's files with get_icp, then calls this to turn it into a weighted
847
+ // from the user's files with sync_icp, then calls this to turn it into a weighted
882
848
  // ICP scoring model. After this, accounts get scored for fit and
883
849
  // get_workspace_status shows the playbook as done.
884
850
  // ===========================================================================
@@ -886,11 +852,11 @@ export function createServer() {
886
852
  "build_scoring_model",
887
853
  "Build (or rebuild) the user's ICP scoring model from their synced GTM context. This is " +
888
854
  "the second half of setting up the GTM playbook: first sync the user's ICP/positioning/pricing " +
889
- "files with get_icp, then call this to translate that context into a weighted set of scoring " +
890
- "signals so accounts get scored for fit. (get_icp usually builds the model on first sync, so you " +
855
+ "files with sync_icp, then call this to translate that context into a weighted set of scoring " +
856
+ "signals so accounts get scored for fit. (sync_icp usually builds the model on first sync, so you " +
891
857
  "often won't need this directly.) If a model already exists it is left alone unless you " +
892
858
  "pass force:true (use that when the context files have changed and the model should be rebuilt). If " +
893
- "it reports no GTM context yet, sync the user's context files with get_icp first, then call this again. " +
859
+ "it reports no GTM context yet, sync the user's context files with sync_icp first, then call this again. " +
894
860
  "STRONGER than this tool: if the user can name a few closed-WON and closed-LOST customer domains, " +
895
861
  "call record_closed_deals instead (or as well) — it trains the model on real outcomes via " +
896
862
  "contrastive lift, which beats a model inferred from a description.",
@@ -912,7 +878,7 @@ export function createServer() {
912
878
  const msg = String(e?.message ?? e);
913
879
  if (msg.includes("no_gtm_context")) {
914
880
  return { content: [{ type: "text", text:
915
- "No GTM context yet. Sync the user's ICP/context files with get_icp first (or scaffold context/icp.md, then get_icp), then build the model." }] };
881
+ "No GTM context yet. Sync the user's ICP/context files with sync_icp first (or scaffold context/icp.md, then sync_icp), then build the model." }] };
916
882
  }
917
883
  if (msg.includes("model_exists")) {
918
884
  return { content: [{ type: "text", text:
@@ -965,16 +931,16 @@ export function createServer() {
965
931
  );
966
932
 
967
933
  // ===========================================================================
968
- // TOOL: get_icp — POST /v2/workspace/icp/import
934
+ // TOOL: sync_icp — POST /v2/workspace/icp/import
969
935
  // The file→Nous half of the ICP symbiosis. In Claude Code the user often
970
936
  // already keeps their ICP/positioning as markdown (context/icp.md, etc.). Don't
971
937
  // make them re-author it in Nous — READ those files and sync them here. Nous
972
- // mirrors each section and remembers the file path so get_icp_model can write
938
+ // mirrors each section and remembers the file path so export_icp_model can write
973
939
  // the learned model back into the same file. Their file stays the source of
974
940
  // truth for the prose; Nous owns the learned scoring half.
975
941
  // ===========================================================================
976
942
  server.tool(
977
- "get_icp",
943
+ "sync_icp",
978
944
  "Sync the user's EXISTING ICP/positioning files into Nous, instead of making them re-author their " +
979
945
  "ICP in a second place. CLAUDE CODE flow: when onboarding (or whenever their ICP files change), look " +
980
946
  "in the project for an existing GTM setup — folders like context/, .claude/, gtm/, and files named " +
@@ -985,7 +951,7 @@ export function createServer() {
985
951
  "Market, messaging.md -> Notes) — one entry per file, do NOT dump several files' content into ICP. " +
986
952
  "If one file holds several sections under headers, split it by header into multiple entries. " +
987
953
  "Nous keeps a served copy of the prose and rebuilds the ICP scoring model from it; " +
988
- "the recorded source_path is what get_icp_model writes the learned model back into. " +
954
+ "the recorded source_path is what export_icp_model writes the learned model back into. " +
989
955
  "INCLUDE EXCLUSIONS: if the ICP names who they will NOT work with (e.g. 'not cold-calling " +
990
956
  "agencies', 'no pure branding/messaging shops'), keep that text IN the ICP section — Nous turns " +
991
957
  "each stated exclusion into a hard disqualifier that caps those accounts below Not-ICP, even when " +
@@ -994,9 +960,11 @@ export function createServer() {
994
960
  "repo — context/icp.md, positioning.md, pricing.md, market.md, competitors.md, gtm-motion.md — " +
995
961
  "filled from what the user tells you plus your own research of their website (write them with your " +
996
962
  "file tools), then call this on those files — so their GTM context lives in their repo where they'll " +
997
- "keep editing it. At minimum create context/icp.md if that's all they'll give you. Re-run this after " +
998
- "the user edits any context file to re-sync. The ICP section's source_path matters most (it's the " +
999
- "write-back target for get_icp_model).",
963
+ "keep editing it. At minimum create context/icp.md if that's all they'll give you. " +
964
+ "MANDATORY RE-SYNC: whenever you (or the user) edit the ICP/context file add or change an exclusion, " +
965
+ "reword the ICP, retarget — you MUST call sync_icp again in the SAME turn. The edit does NOT change the " +
966
+ "ICP score, the exclusions, or the scoring model until you do; an unsynced file edit is silently inert. " +
967
+ "The ICP section's source_path matters most (it's the write-back target for export_icp_model).",
1000
968
  {
1001
969
  sections: z.array(z.object({
1002
970
  section: z.enum(["ICP", "Market", "Product", "Pricing", "Competitors", "Positioning", "GTM Motion", "Notes"])
@@ -1018,11 +986,11 @@ export function createServer() {
1018
986
  const sig = r.signals ?? [];
1019
987
  if (r.model_status === "created" && sig.length) {
1020
988
  lines.push("", `Built the ICP scoring model — ${sig.length} signal${sig.length === 1 ? "" : "s"}.`);
1021
- 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 get_icp_model to write the learned model back into their ICP file.");
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.");
1022
990
  } else if (r.model_status === "no_icp_memory") {
1023
991
  lines.push("", "Synced, but there wasn't enough ICP content to build a scoring model — make sure the ICP section has real content.");
1024
992
  } else {
1025
- lines.push("", "Context synced. Call get_icp_model when you want to write the learned model back into their ICP file.");
993
+ lines.push("", "Context synced. Call export_icp_model when you want to write the learned model back into their ICP file.");
1026
994
  }
1027
995
  return { content: [{ type: "text", text: lines.join("\n").trim() }] };
1028
996
  } catch (e) {
@@ -1038,7 +1006,7 @@ export function createServer() {
1038
1006
  );
1039
1007
 
1040
1008
  // ===========================================================================
1041
- // TOOL: get_icp_model — GET /v2/workspace/icp/model
1009
+ // TOOL: export_icp_model — GET /v2/workspace/icp/model
1042
1010
  // The Nous→file half of the ICP symbiosis. Nous learns which signals actually
1043
1011
  // predict a win (lift + calibration) from real outcomes; this returns that
1044
1012
  // learned model as a ready-to-write fenced block, which the agent writes back
@@ -1046,11 +1014,11 @@ export function createServer() {
1046
1014
  // block so the format is controlled centrally — the agent just persists it.
1047
1015
  // ===========================================================================
1048
1016
  server.tool(
1049
- "get_icp_model",
1017
+ "export_icp_model",
1050
1018
  "Get the LEARNED ICP scoring model (which signals predict a win, their weight, lift, and the " +
1051
1019
  "calibration gap) as a ready-to-write markdown block, and write it back into the user's own ICP " +
1052
1020
  "file. This is the payoff of the symbiosis: their file keeps the words, Nous keeps the model, and " +
1053
- "this writes the model under their words. CLAUDE CODE flow: call this after get_icp or after " +
1021
+ "this writes the model under their words. CLAUDE CODE flow: call this after sync_icp or after " +
1054
1022
  "record_closed_deals, then with your file tools open `target_path`, and if the file already has a " +
1055
1023
  "block between '<!-- nous:icp start -->' and '<!-- nous:icp end -->' REPLACE that whole block with " +
1056
1024
  "the returned `block`; if not, append the returned `block` (e.g. replacing a '## [To refine]' " +
@@ -1061,7 +1029,7 @@ export function createServer() {
1061
1029
  const r = await get("/v2/workspace/icp/model");
1062
1030
  if (!r.has_model) {
1063
1031
  return { content: [{ type: "text", text:
1064
- "No ICP scoring model yet. Sync the user's ICP file with get_icp first (or build one with " +
1032
+ "No ICP scoring model yet. Sync the user's ICP file with sync_icp first (or build one with " +
1065
1033
  "build_scoring_model / record_closed_deals), then call this to write it back." }] };
1066
1034
  }
1067
1035
  const note = r.has_outcomes