@opennous/mcp 0.38.0 → 0.40.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 +12 -3
  2. package/src/server.js +82 -55
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Nous — the Context Graph for AI Agents.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -13,7 +13,9 @@
13
13
  "nous-mcp": "src/index.js"
14
14
  },
15
15
  "main": "./src/index.js",
16
- "files": ["src"],
16
+ "files": [
17
+ "src"
18
+ ],
17
19
  "scripts": {
18
20
  "dev": "node --watch src/index.js",
19
21
  "start": "node src/index.js",
@@ -21,7 +23,14 @@
21
23
  "start:http": "node src/http.js",
22
24
  "typecheck": "echo 'no ts in mcp — skipping'"
23
25
  },
24
- "keywords": ["mcp", "nous", "memory", "crm", "gtm", "agents"],
26
+ "keywords": [
27
+ "mcp",
28
+ "nous",
29
+ "memory",
30
+ "crm",
31
+ "gtm",
32
+ "agents"
33
+ ],
25
34
  "dependencies": {
26
35
  "@modelcontextprotocol/sdk": "^1.27.1",
27
36
  "zod": "^3.23.8"
package/src/server.js CHANGED
@@ -19,7 +19,6 @@
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_gtm_profile — the user's GTM profile (ICP, market, pricing, product, competitors)
22
- * update_gtm_profile — write back a change to a GTM context section (evolve, keep history)
23
22
  * save_note — attach a note/document (meeting brief, transcript, prep) to a contact
24
23
  * search_notes — semantic search over saved notes & documents
25
24
  * get_workspace_status — what's set up in this workspace + a ranked next_steps list (call first)
@@ -44,7 +43,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
43
  import { z } from "zod";
45
44
  import { get, post } from "./client.js";
46
45
 
47
- export const SERVER_VERSION = "0.38.0";
46
+ export const SERVER_VERSION = "0.39.0";
48
47
 
49
48
  // ─── helpers ──────────────────────────────────────────────────────────────────
50
49
 
@@ -117,8 +116,8 @@ Nous first even when the user never says "Nous":
117
116
  - Your action items / what you owe an account -> get_action_items
118
117
  - A fact looks stale before you act on it -> verify
119
118
  - Our ICP, market, pricing, positioning -> get_gtm_profile
120
- - Our ICP lives in a file (sync it / learn it) -> get_icp / get_icp_model
121
- - Our own GTM shifted -> update_gtm_profile
119
+ - Our ICP/positioning lives in our own files -> get_icp (file graph) / get_icp_model (graph → file)
120
+ - Our own GTM shifted (repriced, repositioned) -> edit the context file, then re-run get_icp
122
121
  - A brief / note / transcript on a contact -> save_note / search_notes
123
122
  - What's set up here and what to do next -> get_workspace_status
124
123
 
@@ -629,40 +628,58 @@ export function createServer() {
629
628
  server.tool("get_gtm_profile", gtmProfileDescription, gtmProfileSchema, gtmProfileHandler);
630
629
 
631
630
  // ===========================================================================
632
- // TOOL: update_gtm_profilePOST /v2/workspace/facts
633
- // Write-back: the agent records a durable change to the user's OWN GTM profile
634
- // and EVOLVES the matching belief (supersede + keep history) instead of piling
635
- // up contradictions. This is the loop that keeps the context current as the
636
- // company learns — pair it with get_gtm_profile.
631
+ // TOOLS: get_playbook / sync_playbookthe POLICY layer (vs. facts).
632
+ // Playbooks are versioned rule-docs that GOVERN agent behavior: voice, outreach,
633
+ // icp, positioning. Read the relevant one BEFORE acting; push file edits back so
634
+ // every agent obeys the same rules. GET/POST /v2/playbooks.
637
635
  // ===========================================================================
638
- server.tool(
639
- "update_gtm_profile",
640
- "Keep a SECTION of the user's OWN GTM context current. Each section is a living file: ICP, " +
641
- "Market, Product, Pricing, Competitors, Positioning (these feed the ICP scoring model), plus " +
642
- "'GTM Motion' (how they sell motion, RevOps, process) and 'Notes' (a running log for anything " +
643
- "else durable about their GTM that doesn't fit the others). Use this whenever the user states or " +
644
- "you learn a lasting change to how THEY go to market — repriced, moved upmarket, sharpened " +
645
- "positioning, changed their motion, won a new segment, or a useful note about how they operate. " +
646
- "This is NOT for facts about a prospect or account (use `record` for those). " +
647
- "Rules: keep content short and current — a sentence or two, not an essay. In the default 'replace' " +
648
- "mode the section EVOLVES (the old version is kept as history, never silently contradicted), so " +
649
- "just write the section's current state. Use 'append' mode to log a Notes entry without replacing. " +
650
- "Nous is the source of truth for the GTM context — write back here instead of keeping a local file.",
651
- {
652
- section: z.enum(["ICP", "Market", "Product", "Pricing", "Competitors", "Positioning", "GTM Motion", "Notes"])
653
- .describe("Which section of the GTM context this updates."),
654
- content: z.string().describe("The section's current content short and current, not an essay."),
655
- mode: z.enum(["replace", "append"]).optional()
656
- .describe("'replace' (default) evolves the section and keeps the prior version as history. 'append' logs a new entry without replacing — the default for Notes."),
657
- supersedes: z.string().optional()
658
- .describe("Optional id of a specific existing fact to replace (overrides section matching)."),
659
- },
660
- async ({ section, content, mode, supersedes }) => {
661
- const r = await post("/v2/workspace/facts", { section, content, mode, supersedes });
662
- const verb = r.mode === "append" ? "Logged to" : r.superseded ? "Updated" : "Recorded";
663
- return { content: [{ type: "text", text: `${verb} ${section}: ${content}` }] };
664
- },
665
- );
636
+ const getPlaybookSchema = {
637
+ kind: z.enum(["voice", "outreach", "icp", "positioning"]).optional()
638
+ .describe("Which policy to read. Omit to list all four."),
639
+ };
640
+ const getPlaybookHandler = async ({ kind }) => {
641
+ const r = await get("/v2/playbooks", kind ? { kind } : undefined);
642
+ const pbs = r.playbooks || [];
643
+ if (!pbs.length) return { content: [{ type: "text", text:
644
+ "No playbooks set up yet. The user can set them up on the Playbooks page or in their context files." }] };
645
+ if (kind) {
646
+ const pb = pbs[0];
647
+ const src = pb.source === "claude_code" ? `mirrors ${pb.file_path}` : "stored in Nous";
648
+ return { content: [{ type: "text", text:
649
+ `# ${pb.title} — ${pb.kind} playbook (v${pb.version}, ${src})\n\n${pb.body_md}` }] };
650
+ }
651
+ const lines = pbs.map(p => ` ${p.kind.padEnd(12)} ${p.title} (${p.source === "claude_code" ? p.file_path : "stored in Nous"})`);
652
+ return { content: [{ type: "text", text:
653
+ "The user's playbooks (read one with get_playbook(kind)):\n" + lines.join("\n") }] };
654
+ };
655
+ server.tool("get_playbook",
656
+ "Read a PLAYBOOK — the user's policy/rules for a kind of action: voice, outreach, icp, or positioning. " +
657
+ "These are RULES TO OBEY, not facts. Read the relevant playbook BEFORE you act: before writing outreach " +
658
+ "read 'voice' and 'outreach'; before scoring or qualifying read 'icp'; for messaging read 'positioning'. " +
659
+ "Omit kind to list all four.",
660
+ getPlaybookSchema, getPlaybookHandler);
661
+
662
+ const syncPlaybookSchema = {
663
+ kind: z.enum(["voice", "outreach", "icp", "positioning"]).describe("Which playbook to update."),
664
+ body_md: z.string().describe("The full markdown content of the playbook. Follow the Nous document house style so every playbook reads like a clean text file: a '# Title' line, a '> ' one-paragraph lede, an optional plain 'Key: value' block, a '---' divider, then '## Title-case' sections with plain '- ' bullets. Keep it markdown, no decorative formatting."),
665
+ file_path: z.string().optional().describe("The repo file this mirrors, e.g. 'context/icp/icp.md'. Pass it when syncing a Claude Code file so the source is recorded as the file."),
666
+ };
667
+ const syncPlaybookHandler = async ({ kind, body_md, file_path }) => {
668
+ const r = await post(`/v2/playbooks/${kind}`, { body_md, file_path });
669
+ return { content: [{ type: "text", text:
670
+ `Synced the ${r.playbook?.kind || kind} playbook into Nous (v${r.playbook?.version}). Other agents now read the same rules.` }] };
671
+ };
672
+ server.tool("sync_playbook",
673
+ "Push a playbook's content into Nous so the graph stays current. Call this AFTER you edit a policy file " +
674
+ "in the repo (e.g. context/icp/icp.md, references/voice.md), passing the file's new content and its path, " +
675
+ "so Nous mirrors it and every other agent obeys the same rules.",
676
+ syncPlaybookSchema, syncPlaybookHandler);
677
+
678
+ // The GTM context is no longer written through a dedicated MCP tool. In the file
679
+ // symbiosis model the user's own files (context/icp.md, positioning.md, …) are
680
+ // the source of truth: the agent edits those with its own file tools and calls
681
+ // `get_icp` to sync them into the graph (and `get_icp_model` to write the learned
682
+ // model back). The shared POST /v2/workspace/facts route still backs that import.
666
683
 
667
684
  // ===========================================================================
668
685
  // TOOL: save_note — POST /v2/notes
@@ -680,7 +697,7 @@ export function createServer() {
680
697
  "Notes are append-only and dated, so a contact builds a record across meetings — later you can " +
681
698
  "read the last few and see what changed. This is NOT for logging that an interaction happened " +
682
699
  "(use `record` with an interaction.* event for that), and NOT for the user's own GTM profile " +
683
- "(use `update_gtm_profile`). Put the full text in `content` — it's kept for agents to read; the " +
700
+ "(that lives in their context files — sync it with `get_icp`). Put the full text in `content` — it's kept for agents to read; the " +
684
701
  "UI shows the title and date, not the whole body.",
685
702
  {
686
703
  focus: z.string().describe("Who to attach it to — an email, LinkedIn URL, domain, or entity UUID (not a bare name)."),
@@ -826,9 +843,11 @@ export function createServer() {
826
843
  "IMPORTANT for the ICP: before asking the user to describe their ICP from scratch, if you're in " +
827
844
  "Claude Code, look for an ICP they ALREADY wrote — folders like context/, .claude/, gtm/ and files " +
828
845
  "named icp*, positioning*, pricing*, competitors*. If you find them, read them and call get_icp to " +
829
- "sync them (don't retype the ICP here); if none exists, offer to create context/icp.md from the " +
830
- "conversation, then get_icp it so their ICP lives in their repo. " +
831
- "After this, the next step is usually the GTM playbook (update_gtm_profile) or get_icp.",
846
+ "sync them (don't retype the ICP here); if none exists, scaffold a context/ folder (icp.md, " +
847
+ "positioning.md, pricing.md, market.md, competitors.md, gtm-motion.md) from the conversation + your " +
848
+ "site research, then get_icp it so their ICP lives in their repo. (Not in Claude Code? Capture a " +
849
+ "first cut in the `icp` field here instead.) " +
850
+ "After this, the next step is the context files: call get_icp to sync them into the graph.",
832
851
  {
833
852
  name: z.string().optional().describe("The user's company / workspace name."),
834
853
  website: z.string().optional().describe("The company website (used to seed the GTM context)."),
@@ -852,25 +871,26 @@ export function createServer() {
852
871
  ].filter(Boolean);
853
872
  return { content: [{ type: "text", text:
854
873
  `Workspace profile saved.${set.length ? ` ${set.join(" · ")}.` : ""}\n` +
855
- `Next: call get_workspace_status to see what to set up next (usually the GTM playbook).` }] };
874
+ `Next: call get_workspace_status to see what to set up next (usually syncing the ICP/context files with get_icp).` }] };
856
875
  }
857
876
  );
858
877
 
859
878
  // ===========================================================================
860
879
  // TOOL: build_scoring_model — POST /v2/workspace/scoring-model
861
- // The second half of building the GTM playbook. The agent records the GTM
862
- // context with update_gtm_profile, then calls this to turn it into a weighted
880
+ // The second half of building the GTM playbook. The agent syncs the GTM context
881
+ // from the user's files with get_icp, then calls this to turn it into a weighted
863
882
  // ICP scoring model. After this, accounts get scored for fit and
864
883
  // get_workspace_status shows the playbook as done.
865
884
  // ===========================================================================
866
885
  server.tool(
867
886
  "build_scoring_model",
868
- "Build (or rebuild) the user's ICP scoring model from the GTM context they've recorded. This is " +
869
- "the second half of setting up the GTM playbook: first record the ICP and how they sell with " +
870
- "update_gtm_profile, then call this to translate that context into a weighted set of scoring " +
871
- "signals so accounts get scored for fit. If a model already exists it is left alone unless you " +
872
- "pass force:true (use that when the GTM context has changed and the model should be rebuilt). If " +
873
- "it reports no GTM context yet, record some with update_gtm_profile first, then call this again. " +
887
+ "Build (or rebuild) the user's ICP scoring model from their synced GTM context. This is " +
888
+ "the second half of setting up the GTM playbook: first sync the user's ICP/positioning/pricing " +
889
+ "files with get_icp, then call this to translate that context into a weighted set of scoring " +
890
+ "signals so accounts get scored for fit. (get_icp usually builds the model on first sync, so you " +
891
+ "often won't need this directly.) If a model already exists it is left alone unless you " +
892
+ "pass force:true (use that when the context files have changed and the model should be rebuilt). If " +
893
+ "it reports no GTM context yet, sync the user's context files with get_icp first, then call this again. " +
874
894
  "STRONGER than this tool: if the user can name a few closed-WON and closed-LOST customer domains, " +
875
895
  "call record_closed_deals instead (or as well) — it trains the model on real outcomes via " +
876
896
  "contrastive lift, which beats a model inferred from a description.",
@@ -892,7 +912,7 @@ export function createServer() {
892
912
  const msg = String(e?.message ?? e);
893
913
  if (msg.includes("no_gtm_context")) {
894
914
  return { content: [{ type: "text", text:
895
- "No GTM context recorded yet. Record the ICP and how they sell with update_gtm_profile first, then build the model." }] };
915
+ "No GTM context yet. Sync the user's ICP/context files with get_icp first (or scaffold context/icp.md, then get_icp), then build the model." }] };
896
916
  }
897
917
  if (msg.includes("model_exists")) {
898
918
  return { content: [{ type: "text", text:
@@ -966,10 +986,17 @@ export function createServer() {
966
986
  "If one file holds several sections under headers, split it by header into multiple entries. " +
967
987
  "Nous keeps a served copy of the prose and rebuilds the ICP scoring model from it; " +
968
988
  "the recorded source_path is what get_icp_model writes the learned model back into. " +
969
- "IF NO ICP FILE EXISTS: don't invent one in Nous. Offer to create `context/icp.md` from what the user " +
970
- "tells you (write it with your file tools), then call this on that file so their ICP lives in their " +
971
- "repo where they'll keep editing it. Re-run this after the user edits an ICP file to re-sync. The ICP " +
972
- "section's source_path matters most (it's the write-back target).",
989
+ "INCLUDE EXCLUSIONS: if the ICP names who they will NOT work with (e.g. 'not cold-calling " +
990
+ "agencies', 'no pure branding/messaging shops'), keep that text IN the ICP section Nous turns " +
991
+ "each stated exclusion into a hard disqualifier that caps those accounts below Not-ICP, even when " +
992
+ "they also match the firmographics. So a 'Not a fit' list in icp.md actively lowers their score. " +
993
+ "IF NO ICP FILES EXIST: don't invent context in Nous. Offer to SCAFFOLD a context/ folder in their " +
994
+ "repo — context/icp.md, positioning.md, pricing.md, market.md, competitors.md, gtm-motion.md — " +
995
+ "filled from what the user tells you plus your own research of their website (write them with your " +
996
+ "file tools), then call this on those files — so their GTM context lives in their repo where they'll " +
997
+ "keep editing it. At minimum create context/icp.md if that's all they'll give you. Re-run this after " +
998
+ "the user edits any context file to re-sync. The ICP section's source_path matters most (it's the " +
999
+ "write-back target for get_icp_model).",
973
1000
  {
974
1001
  sections: z.array(z.object({
975
1002
  section: z.enum(["ICP", "Market", "Product", "Pricing", "Competitors", "Positioning", "GTM Motion", "Notes"])