@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.
- package/package.json +1 -1
- package/src/server.js +52 -84
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
|
|
|
@@ -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,
|
|
119
|
-
- Our ICP/positioning lives in our own files ->
|
|
120
|
-
-
|
|
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.
|
|
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
|
-
//
|
|
584
|
-
//
|
|
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.
|
|
674
|
-
"in the repo (e.g.
|
|
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
|
-
// `
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
890
|
-
"signals so accounts get scored for fit. (
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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.
|
|
998
|
-
"
|
|
999
|
-
"
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|