@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.
- package/package.json +1 -1
- package/src/server.js +113 -93
package/package.json
CHANGED
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
|
-
*
|
|
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
|
-
*
|
|
29
|
-
*
|
|
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.
|
|
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
|
|
92
|
+
const ROUTING_PREFERENCES = `# Nous is this workspace's GTM context graph
|
|
93
93
|
|
|
94
|
-
Nous is our
|
|
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,
|
|
119
|
-
- Our ICP/positioning lives in our own files ->
|
|
120
|
-
- You edited an ICP / context / playbook file -> re-sync THIS turn:
|
|
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
|
|
125
|
-
|
|
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
|
|
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
|
|
252
|
-
"
|
|
253
|
-
{
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
591
|
-
//
|
|
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
|
-
"
|
|
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
|
-
// `
|
|
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 `
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
899
|
-
"signals so accounts get scored for fit. (
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|