@opennous/mcp 0.41.0 → 0.43.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 +113 -93
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.41.0",
3
+ "version": "0.43.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
 
@@ -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
@@ -115,19 +115,20 @@ 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
- - You edited an ICP / context / playbook file -> re-sync THIS turn: get_icp (ICP/context) or sync_playbook (a playbook)
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.
126
127
 
127
128
  CRITICAL — edits don't apply until you sync. Editing an ICP / context / playbook
128
129
  file (context/icp.md, positioning.md, references/voice.md, …) does NOT change the
129
130
  ICP score, the exclusions, or what any other agent reads until it is synced into
130
- the graph. After ANY such edit you MUST call get_icp (for ICP/context files) or
131
+ the graph. After ANY such edit you MUST call sync_icp (for ICP/context files) or
131
132
  sync_playbook (for a playbook) in the SAME turn. Never leave an edited file
132
133
  unsynced — an unsynced edit is silently inert.`;
133
134
 
@@ -248,11 +249,27 @@ export function createServer() {
248
249
  "get_account",
249
250
  "Get the full account record for a person or company — the durable FACTS we've learned about them " +
250
251
  "(their atomic memory: budget, authority, pain, stack, plans), every attribute (claim) with its " +
251
- "confidence and freshness, plus the recent activity timeline. Pass an email or entity UUID. " +
252
- "For a task-specific, ranked view, prefer get_context.",
253
- { id: z.string().describe("Email address or entity UUID") },
254
- async ({ id }) => {
255
- 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}`);
256
273
  const lines = [`${rec.type} · ${rec.entity_id}`, ""];
257
274
 
258
275
  if (rec.icp) {
@@ -274,13 +291,37 @@ export function createServer() {
274
291
  }
275
292
  lines.push("");
276
293
  }
277
- const obs = rec.recent_observations ?? [];
278
- 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;
279
319
  lines.push(`TIMELINE (${obs.length}):`);
280
320
  for (const o of obs.slice(0, 30)) {
281
321
  lines.push(` ${whenLabel(o.property, o.observed_at)} ${fmtType(o.property)}`);
282
322
  }
283
323
  }
324
+
284
325
  return { content: [{ type: "text", text: lines.join("\n").trim() }] };
285
326
  }
286
327
  );
@@ -506,9 +547,15 @@ export function createServer() {
506
547
  return { content: [{ type: "text", text: "Nothing needs attention right now." }] };
507
548
  }
508
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.
509
555
  const lines = r.items.map(it => {
510
556
  const when = it.when ? `${fmtWhen(it.when)} — ` : "";
511
- 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}`;
512
559
  });
513
560
  return { content: [{ type: "text", text: `Needs attention (${r.items.length}):\n${lines.join("\n")}` }] };
514
561
  }
@@ -586,53 +633,9 @@ export function createServer() {
586
633
  }
587
634
  );
588
635
 
589
- // ===========================================================================
590
- // TOOL: get_gtm_profile — GET /v2/workspace/facts
591
- // The user's OWN GTM profile: ICP, market, product, pricing, competitors.
592
- // Use this for any question about the user's business — NOT get_account.
593
- // ===========================================================================
594
- const gtmProfileDescription =
595
- "Get the user's OWN GTM profile — their ICP, target market, product, pricing, " +
596
- "competitors, and positioning. These are NOT facts about a person or company; they are " +
597
- "the user's own business profile. Use this for any question about the user's ICP, target " +
598
- "buyer, pricing, market, or differentiators. ALWAYS prefer this over query/get_account " +
599
- "when the question is about the user's business.";
600
- const gtmProfileSchema = {
601
- categories: z.array(z.string()).optional()
602
- .describe("Optional category filter, e.g. ['ICP'] or ['Pricing','Competitors']. Omit for all."),
603
- limit: z.number().min(1).max(500).optional()
604
- .describe("Max facts to return (default 50)"),
605
- };
606
- const gtmProfileHandler = async ({ categories, limit }) => {
607
- const params = {};
608
- if (categories?.length) params.categories = categories.join(",");
609
- if (limit != null) params.limit = limit;
610
- const r = await get("/v2/workspace/facts", params);
611
- if (!r.facts?.length) {
612
- return { content: [{ type: "text", text:
613
- "No GTM profile recorded yet. The user can set it up in the GTM Context tab." }] };
614
- }
615
- const groups = {};
616
- for (const f of r.facts) (groups[f.category] ??= []).push(f);
617
- const lines = [];
618
- for (const [cat, facts] of Object.entries(groups)) {
619
- lines.push(`${cat.toUpperCase()} (${facts.length}):`);
620
- for (const f of facts) {
621
- // Flag AI-drafted facts (confidence < 1) and ones not confirmed in a long
622
- // time, so the agent treats them as provisional and prefers fresh,
623
- // user-confirmed facts when they conflict.
624
- const ageDays = f.recorded_at ? Math.floor((Date.now() - new Date(f.recorded_at).getTime()) / 86400000) : 0;
625
- const tags = [];
626
- if (typeof f.confidence === "number" && f.confidence < 1) tags.push("inferred");
627
- if (ageDays >= 90) tags.push("stale");
628
- const tag = tags.length ? ` (${tags.join(", ")})` : "";
629
- lines.push(` ${f.content}${tag} [${relAge(f.recorded_at)}]`);
630
- }
631
- lines.push("");
632
- }
633
- return { content: [{ type: "text", text: lines.join("\n").trim() }] };
634
- };
635
- server.tool("get_gtm_profile", gtmProfileDescription, gtmProfileSchema, gtmProfileHandler);
636
+ // get_gtm_profile removed: the user's GTM lives in their files, mirrored into
637
+ // the graph as playbooks (get_playbook) plus the learned ICP model. Read
638
+ // get_playbook for the user's own rules, ICP, and positioning.
636
639
 
637
640
  // ===========================================================================
638
641
  // TOOLS: get_playbook / sync_playbook — the POLICY layer (vs. facts).
@@ -681,13 +684,18 @@ export function createServer() {
681
684
  "whenever you edit a policy file in the repo (e.g. references/voice.md, outreach rules), passing the " +
682
685
  "file's new content and its path, so Nous mirrors it and every other agent obeys the same rules. An " +
683
686
  "edited playbook file that isn't synced is silently inert — other agents keep reading the old rules. " +
684
- "(For the ICP/context files specifically, get_icp is the sync use that one.)",
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. " +
692
+ "(For the ICP/context files specifically, sync_icp is the sync — use that one.)",
685
693
  syncPlaybookSchema, syncPlaybookHandler);
686
694
 
687
695
  // The GTM context is no longer written through a dedicated MCP tool. In the file
688
696
  // symbiosis model the user's own files (context/icp.md, positioning.md, …) are
689
697
  // the source of truth: the agent edits those with its own file tools and calls
690
- // `get_icp` to sync them into the graph (and `get_icp_model` to write the learned
698
+ // `sync_icp` to sync them into the graph (and `export_icp_model` to write the learned
691
699
  // model back). The shared POST /v2/workspace/facts route still backs that import.
692
700
 
693
701
  // ===========================================================================
@@ -706,7 +714,7 @@ export function createServer() {
706
714
  "Notes are append-only and dated, so a contact builds a record across meetings — later you can " +
707
715
  "read the last few and see what changed. This is NOT for logging that an interaction happened " +
708
716
  "(use `record` with an interaction.* event for that), and NOT for the user's own GTM profile " +
709
- "(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 " +
717
+ "(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 " +
710
718
  "UI shows the title and date, not the whole body.",
711
719
  {
712
720
  focus: z.string().describe("Who to attach it to — an email, LinkedIn URL, domain, or entity UUID (not a bare name)."),
@@ -795,11 +803,23 @@ export function createServer() {
795
803
 
796
804
  const mark = (b) => (b ? "✓" : "✗");
797
805
  lines.push("SETUP:");
798
- 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"}`}`);
799
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)` : ""}`);
800
820
  if (setup.icp_sync) {
801
821
  const sy = setup.icp_sync;
802
- 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" : ""}`);
822
+ 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" : ""}`);
803
823
  }
804
824
  const ints = setup.integrations?.connected ?? [];
805
825
  lines.push(` ${mark((setup.integrations?.count ?? 0) > 0)} Integrations (${setup.integrations?.count ?? 0})${ints.length ? `: ${ints.map((i) => i.name).join(", ")}` : ""}`);
@@ -851,12 +871,12 @@ export function createServer() {
851
871
  "changing. " +
852
872
  "IMPORTANT for the ICP: before asking the user to describe their ICP from scratch, if you're in " +
853
873
  "Claude Code, look for an ICP they ALREADY wrote — folders like context/, .claude/, gtm/ and files " +
854
- "named icp*, positioning*, pricing*, competitors*. If you find them, read them and call get_icp to " +
874
+ "named icp*, positioning*, pricing*, competitors*. If you find them, read them and call sync_icp to " +
855
875
  "sync them (don't retype the ICP here); if none exists, scaffold a context/ folder (icp.md, " +
856
876
  "positioning.md, pricing.md, market.md, competitors.md, gtm-motion.md) from the conversation + your " +
857
- "site research, then get_icp it — so their ICP lives in their repo. (Not in Claude Code? Capture a " +
877
+ "site research, then sync_icp it — so their ICP lives in their repo. (Not in Claude Code? Capture a " +
858
878
  "first cut in the `icp` field here instead.) " +
859
- "After this, the next step is the context files: call get_icp to sync them into the graph.",
879
+ "After this, the next step is the context files: call sync_icp to sync them into the graph.",
860
880
  {
861
881
  name: z.string().optional().describe("The user's company / workspace name."),
862
882
  website: z.string().optional().describe("The company website (used to seed the GTM context)."),
@@ -880,14 +900,14 @@ export function createServer() {
880
900
  ].filter(Boolean);
881
901
  return { content: [{ type: "text", text:
882
902
  `Workspace profile saved.${set.length ? ` ${set.join(" · ")}.` : ""}\n` +
883
- `Next: call get_workspace_status to see what to set up next (usually syncing the ICP/context files with get_icp).` }] };
903
+ `Next: call get_workspace_status to see what to set up next (usually syncing the ICP/context files with sync_icp).` }] };
884
904
  }
885
905
  );
886
906
 
887
907
  // ===========================================================================
888
908
  // TOOL: build_scoring_model — POST /v2/workspace/scoring-model
889
909
  // The second half of building the GTM playbook. The agent syncs the GTM context
890
- // from the user's files with get_icp, then calls this to turn it into a weighted
910
+ // from the user's files with sync_icp, then calls this to turn it into a weighted
891
911
  // ICP scoring model. After this, accounts get scored for fit and
892
912
  // get_workspace_status shows the playbook as done.
893
913
  // ===========================================================================
@@ -895,11 +915,11 @@ export function createServer() {
895
915
  "build_scoring_model",
896
916
  "Build (or rebuild) the user's ICP scoring model from their synced GTM context. This is " +
897
917
  "the second half of setting up the GTM playbook: first sync the user's ICP/positioning/pricing " +
898
- "files with get_icp, then call this to translate that context into a weighted set of scoring " +
899
- "signals so accounts get scored for fit. (get_icp usually builds the model on first sync, so you " +
918
+ "files with sync_icp, then call this to translate that context into a weighted set of scoring " +
919
+ "signals so accounts get scored for fit. (sync_icp usually builds the model on first sync, so you " +
900
920
  "often won't need this directly.) If a model already exists it is left alone unless you " +
901
921
  "pass force:true (use that when the context files have changed and the model should be rebuilt). If " +
902
- "it reports no GTM context yet, sync the user's context files with get_icp first, then call this again. " +
922
+ "it reports no GTM context yet, sync the user's context files with sync_icp first, then call this again. " +
903
923
  "STRONGER than this tool: if the user can name a few closed-WON and closed-LOST customer domains, " +
904
924
  "call record_closed_deals instead (or as well) — it trains the model on real outcomes via " +
905
925
  "contrastive lift, which beats a model inferred from a description.",
@@ -921,7 +941,7 @@ export function createServer() {
921
941
  const msg = String(e?.message ?? e);
922
942
  if (msg.includes("no_gtm_context")) {
923
943
  return { content: [{ type: "text", text:
924
- "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." }] };
944
+ "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." }] };
925
945
  }
926
946
  if (msg.includes("model_exists")) {
927
947
  return { content: [{ type: "text", text:
@@ -974,16 +994,16 @@ export function createServer() {
974
994
  );
975
995
 
976
996
  // ===========================================================================
977
- // TOOL: get_icp — POST /v2/workspace/icp/import
997
+ // TOOL: sync_icp — POST /v2/workspace/icp/import
978
998
  // The file→Nous half of the ICP symbiosis. In Claude Code the user often
979
999
  // already keeps their ICP/positioning as markdown (context/icp.md, etc.). Don't
980
1000
  // make them re-author it in Nous — READ those files and sync them here. Nous
981
- // mirrors each section and remembers the file path so get_icp_model can write
1001
+ // mirrors each section and remembers the file path so export_icp_model can write
982
1002
  // the learned model back into the same file. Their file stays the source of
983
1003
  // truth for the prose; Nous owns the learned scoring half.
984
1004
  // ===========================================================================
985
1005
  server.tool(
986
- "get_icp",
1006
+ "sync_icp",
987
1007
  "Sync the user's EXISTING ICP/positioning files into Nous, instead of making them re-author their " +
988
1008
  "ICP in a second place. CLAUDE CODE flow: when onboarding (or whenever their ICP files change), look " +
989
1009
  "in the project for an existing GTM setup — folders like context/, .claude/, gtm/, and files named " +
@@ -994,7 +1014,7 @@ export function createServer() {
994
1014
  "Market, messaging.md -> Notes) — one entry per file, do NOT dump several files' content into ICP. " +
995
1015
  "If one file holds several sections under headers, split it by header into multiple entries. " +
996
1016
  "Nous keeps a served copy of the prose and rebuilds the ICP scoring model from it; " +
997
- "the recorded source_path is what get_icp_model writes the learned model back into. " +
1017
+ "the recorded source_path is what export_icp_model writes the learned model back into. " +
998
1018
  "INCLUDE EXCLUSIONS: if the ICP names who they will NOT work with (e.g. 'not cold-calling " +
999
1019
  "agencies', 'no pure branding/messaging shops'), keep that text IN the ICP section — Nous turns " +
1000
1020
  "each stated exclusion into a hard disqualifier that caps those accounts below Not-ICP, even when " +
@@ -1005,9 +1025,9 @@ export function createServer() {
1005
1025
  "file tools), then call this on those files — so their GTM context lives in their repo where they'll " +
1006
1026
  "keep editing it. At minimum create context/icp.md if that's all they'll give you. " +
1007
1027
  "MANDATORY RE-SYNC: whenever you (or the user) edit the ICP/context file — add or change an exclusion, " +
1008
- "reword the ICP, retarget — you MUST call get_icp again in the SAME turn. The edit does NOT change the " +
1028
+ "reword the ICP, retarget — you MUST call sync_icp again in the SAME turn. The edit does NOT change the " +
1009
1029
  "ICP score, the exclusions, or the scoring model until you do; an unsynced file edit is silently inert. " +
1010
- "The ICP section's source_path matters most (it's the write-back target for get_icp_model).",
1030
+ "The ICP section's source_path matters most (it's the write-back target for export_icp_model).",
1011
1031
  {
1012
1032
  sections: z.array(z.object({
1013
1033
  section: z.enum(["ICP", "Market", "Product", "Pricing", "Competitors", "Positioning", "GTM Motion", "Notes"])
@@ -1029,11 +1049,11 @@ export function createServer() {
1029
1049
  const sig = r.signals ?? [];
1030
1050
  if (r.model_status === "created" && sig.length) {
1031
1051
  lines.push("", `Built the ICP scoring model — ${sig.length} signal${sig.length === 1 ? "" : "s"}.`);
1032
- 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.");
1052
+ 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.");
1033
1053
  } else if (r.model_status === "no_icp_memory") {
1034
1054
  lines.push("", "Synced, but there wasn't enough ICP content to build a scoring model — make sure the ICP section has real content.");
1035
1055
  } else {
1036
- lines.push("", "Context synced. Call get_icp_model when you want to write the learned model back into their ICP file.");
1056
+ lines.push("", "Context synced. Call export_icp_model when you want to write the learned model back into their ICP file.");
1037
1057
  }
1038
1058
  return { content: [{ type: "text", text: lines.join("\n").trim() }] };
1039
1059
  } catch (e) {
@@ -1049,7 +1069,7 @@ export function createServer() {
1049
1069
  );
1050
1070
 
1051
1071
  // ===========================================================================
1052
- // TOOL: get_icp_model — GET /v2/workspace/icp/model
1072
+ // TOOL: export_icp_model — GET /v2/workspace/icp/model
1053
1073
  // The Nous→file half of the ICP symbiosis. Nous learns which signals actually
1054
1074
  // predict a win (lift + calibration) from real outcomes; this returns that
1055
1075
  // learned model as a ready-to-write fenced block, which the agent writes back
@@ -1057,11 +1077,11 @@ export function createServer() {
1057
1077
  // block so the format is controlled centrally — the agent just persists it.
1058
1078
  // ===========================================================================
1059
1079
  server.tool(
1060
- "get_icp_model",
1080
+ "export_icp_model",
1061
1081
  "Get the LEARNED ICP scoring model (which signals predict a win, their weight, lift, and the " +
1062
1082
  "calibration gap) as a ready-to-write markdown block, and write it back into the user's own ICP " +
1063
1083
  "file. This is the payoff of the symbiosis: their file keeps the words, Nous keeps the model, and " +
1064
- "this writes the model under their words. CLAUDE CODE flow: call this after get_icp or after " +
1084
+ "this writes the model under their words. CLAUDE CODE flow: call this after sync_icp or after " +
1065
1085
  "record_closed_deals, then with your file tools open `target_path`, and if the file already has a " +
1066
1086
  "block between '<!-- nous:icp start -->' and '<!-- nous:icp end -->' REPLACE that whole block with " +
1067
1087
  "the returned `block`; if not, append the returned `block` (e.g. replacing a '## [To refine]' " +
@@ -1072,7 +1092,7 @@ export function createServer() {
1072
1092
  const r = await get("/v2/workspace/icp/model");
1073
1093
  if (!r.has_model) {
1074
1094
  return { content: [{ type: "text", text:
1075
- "No ICP scoring model yet. Sync the user's ICP file with get_icp first (or build one with " +
1095
+ "No ICP scoring model yet. Sync the user's ICP file with sync_icp first (or build one with " +
1076
1096
  "build_scoring_model / record_closed_deals), then call this to write it back." }] };
1077
1097
  }
1078
1098
  const note = r.has_outcomes