@opennous/mcp 0.41.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 +39 -82
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,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
|
|
|
@@ -586,53 +587,9 @@ export function createServer() {
|
|
|
586
587
|
}
|
|
587
588
|
);
|
|
588
589
|
|
|
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);
|
|
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.
|
|
636
593
|
|
|
637
594
|
// ===========================================================================
|
|
638
595
|
// TOOLS: get_playbook / sync_playbook — the POLICY layer (vs. facts).
|
|
@@ -681,13 +638,13 @@ export function createServer() {
|
|
|
681
638
|
"whenever you edit a policy file in the repo (e.g. references/voice.md, outreach rules), passing the " +
|
|
682
639
|
"file's new content and its path, so Nous mirrors it and every other agent obeys the same rules. An " +
|
|
683
640
|
"edited playbook file that isn't synced is silently inert — other agents keep reading the old rules. " +
|
|
684
|
-
"(For the ICP/context files specifically,
|
|
641
|
+
"(For the ICP/context files specifically, sync_icp is the sync — use that one.)",
|
|
685
642
|
syncPlaybookSchema, syncPlaybookHandler);
|
|
686
643
|
|
|
687
644
|
// The GTM context is no longer written through a dedicated MCP tool. In the file
|
|
688
645
|
// symbiosis model the user's own files (context/icp.md, positioning.md, …) are
|
|
689
646
|
// the source of truth: the agent edits those with its own file tools and calls
|
|
690
|
-
// `
|
|
647
|
+
// `sync_icp` to sync them into the graph (and `export_icp_model` to write the learned
|
|
691
648
|
// model back). The shared POST /v2/workspace/facts route still backs that import.
|
|
692
649
|
|
|
693
650
|
// ===========================================================================
|
|
@@ -706,7 +663,7 @@ export function createServer() {
|
|
|
706
663
|
"Notes are append-only and dated, so a contact builds a record across meetings — later you can " +
|
|
707
664
|
"read the last few and see what changed. This is NOT for logging that an interaction happened " +
|
|
708
665
|
"(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 `
|
|
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 " +
|
|
710
667
|
"UI shows the title and date, not the whole body.",
|
|
711
668
|
{
|
|
712
669
|
focus: z.string().describe("Who to attach it to — an email, LinkedIn URL, domain, or entity UUID (not a bare name)."),
|
|
@@ -799,7 +756,7 @@ export function createServer() {
|
|
|
799
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)` : ""}`);
|
|
800
757
|
if (setup.icp_sync) {
|
|
801
758
|
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
|
|
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" : ""}`);
|
|
803
760
|
}
|
|
804
761
|
const ints = setup.integrations?.connected ?? [];
|
|
805
762
|
lines.push(` ${mark((setup.integrations?.count ?? 0) > 0)} Integrations (${setup.integrations?.count ?? 0})${ints.length ? `: ${ints.map((i) => i.name).join(", ")}` : ""}`);
|
|
@@ -851,12 +808,12 @@ export function createServer() {
|
|
|
851
808
|
"changing. " +
|
|
852
809
|
"IMPORTANT for the ICP: before asking the user to describe their ICP from scratch, if you're in " +
|
|
853
810
|
"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
|
|
811
|
+
"named icp*, positioning*, pricing*, competitors*. If you find them, read them and call sync_icp to " +
|
|
855
812
|
"sync them (don't retype the ICP here); if none exists, scaffold a context/ folder (icp.md, " +
|
|
856
813
|
"positioning.md, pricing.md, market.md, competitors.md, gtm-motion.md) from the conversation + your " +
|
|
857
|
-
"site research, then
|
|
814
|
+
"site research, then sync_icp it — so their ICP lives in their repo. (Not in Claude Code? Capture a " +
|
|
858
815
|
"first cut in the `icp` field here instead.) " +
|
|
859
|
-
"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.",
|
|
860
817
|
{
|
|
861
818
|
name: z.string().optional().describe("The user's company / workspace name."),
|
|
862
819
|
website: z.string().optional().describe("The company website (used to seed the GTM context)."),
|
|
@@ -880,14 +837,14 @@ export function createServer() {
|
|
|
880
837
|
].filter(Boolean);
|
|
881
838
|
return { content: [{ type: "text", text:
|
|
882
839
|
`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
|
|
840
|
+
`Next: call get_workspace_status to see what to set up next (usually syncing the ICP/context files with sync_icp).` }] };
|
|
884
841
|
}
|
|
885
842
|
);
|
|
886
843
|
|
|
887
844
|
// ===========================================================================
|
|
888
845
|
// TOOL: build_scoring_model — POST /v2/workspace/scoring-model
|
|
889
846
|
// The second half of building the GTM playbook. The agent syncs the GTM context
|
|
890
|
-
// from the user's files with
|
|
847
|
+
// from the user's files with sync_icp, then calls this to turn it into a weighted
|
|
891
848
|
// ICP scoring model. After this, accounts get scored for fit and
|
|
892
849
|
// get_workspace_status shows the playbook as done.
|
|
893
850
|
// ===========================================================================
|
|
@@ -895,11 +852,11 @@ export function createServer() {
|
|
|
895
852
|
"build_scoring_model",
|
|
896
853
|
"Build (or rebuild) the user's ICP scoring model from their synced GTM context. This is " +
|
|
897
854
|
"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. (
|
|
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 " +
|
|
900
857
|
"often won't need this directly.) If a model already exists it is left alone unless you " +
|
|
901
858
|
"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
|
|
859
|
+
"it reports no GTM context yet, sync the user's context files with sync_icp first, then call this again. " +
|
|
903
860
|
"STRONGER than this tool: if the user can name a few closed-WON and closed-LOST customer domains, " +
|
|
904
861
|
"call record_closed_deals instead (or as well) — it trains the model on real outcomes via " +
|
|
905
862
|
"contrastive lift, which beats a model inferred from a description.",
|
|
@@ -921,7 +878,7 @@ export function createServer() {
|
|
|
921
878
|
const msg = String(e?.message ?? e);
|
|
922
879
|
if (msg.includes("no_gtm_context")) {
|
|
923
880
|
return { content: [{ type: "text", text:
|
|
924
|
-
"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." }] };
|
|
925
882
|
}
|
|
926
883
|
if (msg.includes("model_exists")) {
|
|
927
884
|
return { content: [{ type: "text", text:
|
|
@@ -974,16 +931,16 @@ export function createServer() {
|
|
|
974
931
|
);
|
|
975
932
|
|
|
976
933
|
// ===========================================================================
|
|
977
|
-
// TOOL:
|
|
934
|
+
// TOOL: sync_icp — POST /v2/workspace/icp/import
|
|
978
935
|
// The file→Nous half of the ICP symbiosis. In Claude Code the user often
|
|
979
936
|
// already keeps their ICP/positioning as markdown (context/icp.md, etc.). Don't
|
|
980
937
|
// 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
|
|
938
|
+
// mirrors each section and remembers the file path so export_icp_model can write
|
|
982
939
|
// the learned model back into the same file. Their file stays the source of
|
|
983
940
|
// truth for the prose; Nous owns the learned scoring half.
|
|
984
941
|
// ===========================================================================
|
|
985
942
|
server.tool(
|
|
986
|
-
"
|
|
943
|
+
"sync_icp",
|
|
987
944
|
"Sync the user's EXISTING ICP/positioning files into Nous, instead of making them re-author their " +
|
|
988
945
|
"ICP in a second place. CLAUDE CODE flow: when onboarding (or whenever their ICP files change), look " +
|
|
989
946
|
"in the project for an existing GTM setup — folders like context/, .claude/, gtm/, and files named " +
|
|
@@ -994,7 +951,7 @@ export function createServer() {
|
|
|
994
951
|
"Market, messaging.md -> Notes) — one entry per file, do NOT dump several files' content into ICP. " +
|
|
995
952
|
"If one file holds several sections under headers, split it by header into multiple entries. " +
|
|
996
953
|
"Nous keeps a served copy of the prose and rebuilds the ICP scoring model from it; " +
|
|
997
|
-
"the recorded source_path is what
|
|
954
|
+
"the recorded source_path is what export_icp_model writes the learned model back into. " +
|
|
998
955
|
"INCLUDE EXCLUSIONS: if the ICP names who they will NOT work with (e.g. 'not cold-calling " +
|
|
999
956
|
"agencies', 'no pure branding/messaging shops'), keep that text IN the ICP section — Nous turns " +
|
|
1000
957
|
"each stated exclusion into a hard disqualifier that caps those accounts below Not-ICP, even when " +
|
|
@@ -1005,9 +962,9 @@ export function createServer() {
|
|
|
1005
962
|
"file tools), then call this on those files — so their GTM context lives in their repo where they'll " +
|
|
1006
963
|
"keep editing it. At minimum create context/icp.md if that's all they'll give you. " +
|
|
1007
964
|
"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
|
|
965
|
+
"reword the ICP, retarget — you MUST call sync_icp again in the SAME turn. The edit does NOT change the " +
|
|
1009
966
|
"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
|
|
967
|
+
"The ICP section's source_path matters most (it's the write-back target for export_icp_model).",
|
|
1011
968
|
{
|
|
1012
969
|
sections: z.array(z.object({
|
|
1013
970
|
section: z.enum(["ICP", "Market", "Product", "Pricing", "Competitors", "Positioning", "GTM Motion", "Notes"])
|
|
@@ -1029,11 +986,11 @@ export function createServer() {
|
|
|
1029
986
|
const sig = r.signals ?? [];
|
|
1030
987
|
if (r.model_status === "created" && sig.length) {
|
|
1031
988
|
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
|
|
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.");
|
|
1033
990
|
} else if (r.model_status === "no_icp_memory") {
|
|
1034
991
|
lines.push("", "Synced, but there wasn't enough ICP content to build a scoring model — make sure the ICP section has real content.");
|
|
1035
992
|
} else {
|
|
1036
|
-
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.");
|
|
1037
994
|
}
|
|
1038
995
|
return { content: [{ type: "text", text: lines.join("\n").trim() }] };
|
|
1039
996
|
} catch (e) {
|
|
@@ -1049,7 +1006,7 @@ export function createServer() {
|
|
|
1049
1006
|
);
|
|
1050
1007
|
|
|
1051
1008
|
// ===========================================================================
|
|
1052
|
-
// TOOL:
|
|
1009
|
+
// TOOL: export_icp_model — GET /v2/workspace/icp/model
|
|
1053
1010
|
// The Nous→file half of the ICP symbiosis. Nous learns which signals actually
|
|
1054
1011
|
// predict a win (lift + calibration) from real outcomes; this returns that
|
|
1055
1012
|
// learned model as a ready-to-write fenced block, which the agent writes back
|
|
@@ -1057,11 +1014,11 @@ export function createServer() {
|
|
|
1057
1014
|
// block so the format is controlled centrally — the agent just persists it.
|
|
1058
1015
|
// ===========================================================================
|
|
1059
1016
|
server.tool(
|
|
1060
|
-
"
|
|
1017
|
+
"export_icp_model",
|
|
1061
1018
|
"Get the LEARNED ICP scoring model (which signals predict a win, their weight, lift, and the " +
|
|
1062
1019
|
"calibration gap) as a ready-to-write markdown block, and write it back into the user's own ICP " +
|
|
1063
1020
|
"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
|
|
1021
|
+
"this writes the model under their words. CLAUDE CODE flow: call this after sync_icp or after " +
|
|
1065
1022
|
"record_closed_deals, then with your file tools open `target_path`, and if the file already has a " +
|
|
1066
1023
|
"block between '<!-- nous:icp start -->' and '<!-- nous:icp end -->' REPLACE that whole block with " +
|
|
1067
1024
|
"the returned `block`; if not, append the returned `block` (e.g. replacing a '## [To refine]' " +
|
|
@@ -1072,7 +1029,7 @@ export function createServer() {
|
|
|
1072
1029
|
const r = await get("/v2/workspace/icp/model");
|
|
1073
1030
|
if (!r.has_model) {
|
|
1074
1031
|
return { content: [{ type: "text", text:
|
|
1075
|
-
"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 " +
|
|
1076
1033
|
"build_scoring_model / record_closed_deals), then call this to write it back." }] };
|
|
1077
1034
|
}
|
|
1078
1035
|
const note = r.has_outcomes
|