@opennous/mcp 0.37.0 → 0.39.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 +87 -56
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.37.0",
3
+ "version": "0.39.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.37.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."),
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)."),
@@ -771,6 +788,10 @@ export function createServer() {
771
788
  lines.push("SETUP:");
772
789
  lines.push(` ${mark(setup.onboarding?.done)} Onboarding${setup.onboarding?.done ? "" : ` — missing ${(setup.onboarding?.missing ?? []).join(", ") || "details"}`}`);
773
790
  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
+ if (setup.icp_sync) {
792
+ 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 get_icp_model to refresh the file" : ""}`);
794
+ }
774
795
  const ints = setup.integrations?.connected ?? [];
775
796
  lines.push(` ${mark((setup.integrations?.count ?? 0) > 0)} Integrations (${setup.integrations?.count ?? 0})${ints.length ? `: ${ints.map((i) => i.name).join(", ")}` : ""}`);
776
797
  const crm = setup.crm_sync ?? {};
@@ -822,9 +843,11 @@ export function createServer() {
822
843
  "IMPORTANT for the ICP: before asking the user to describe their ICP from scratch, if you're in " +
823
844
  "Claude Code, look for an ICP they ALREADY wrote — folders like context/, .claude/, gtm/ and files " +
824
845
  "named icp*, positioning*, pricing*, competitors*. If you find them, read them and call get_icp to " +
825
- "sync them (don't retype the ICP here); if none exists, offer to create context/icp.md from the " +
826
- "conversation, then get_icp it so their ICP lives in their repo. " +
827
- "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.",
828
851
  {
829
852
  name: z.string().optional().describe("The user's company / workspace name."),
830
853
  website: z.string().optional().describe("The company website (used to seed the GTM context)."),
@@ -848,25 +871,26 @@ export function createServer() {
848
871
  ].filter(Boolean);
849
872
  return { content: [{ type: "text", text:
850
873
  `Workspace profile saved.${set.length ? ` ${set.join(" · ")}.` : ""}\n` +
851
- `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).` }] };
852
875
  }
853
876
  );
854
877
 
855
878
  // ===========================================================================
856
879
  // TOOL: build_scoring_model — POST /v2/workspace/scoring-model
857
- // The second half of building the GTM playbook. The agent records the GTM
858
- // 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
859
882
  // ICP scoring model. After this, accounts get scored for fit and
860
883
  // get_workspace_status shows the playbook as done.
861
884
  // ===========================================================================
862
885
  server.tool(
863
886
  "build_scoring_model",
864
- "Build (or rebuild) the user's ICP scoring model from the GTM context they've recorded. This is " +
865
- "the second half of setting up the GTM playbook: first record the ICP and how they sell with " +
866
- "update_gtm_profile, then call this to translate that context into a weighted set of scoring " +
867
- "signals so accounts get scored for fit. If a model already exists it is left alone unless you " +
868
- "pass force:true (use that when the GTM context has changed and the model should be rebuilt). If " +
869
- "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. " +
870
894
  "STRONGER than this tool: if the user can name a few closed-WON and closed-LOST customer domains, " +
871
895
  "call record_closed_deals instead (or as well) — it trains the model on real outcomes via " +
872
896
  "contrastive lift, which beats a model inferred from a description.",
@@ -888,7 +912,7 @@ export function createServer() {
888
912
  const msg = String(e?.message ?? e);
889
913
  if (msg.includes("no_gtm_context")) {
890
914
  return { content: [{ type: "text", text:
891
- "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." }] };
892
916
  }
893
917
  if (msg.includes("model_exists")) {
894
918
  return { content: [{ type: "text", text:
@@ -956,12 +980,19 @@ export function createServer() {
956
980
  "in the project for an existing GTM setup — folders like context/, .claude/, gtm/, and files named " +
957
981
  "icp*, positioning*, pricing*, competitors*, messaging*, market*. READ the ones you find with your " +
958
982
  "own file tools, then call this with each file's content mapped to a section, AND its path in " +
959
- "`source_path`. Nous keeps a served copy of the prose and rebuilds the ICP scoring model from it; " +
983
+ "`source_path`. MAP GRANULARLY: map each FILE to the single section it best fits (icp.md -> ICP, " +
984
+ "positioning.md -> Positioning, pricing.md -> Pricing, competitors.md -> Competitors, market.md -> " +
985
+ "Market, messaging.md -> Notes) — one entry per file, do NOT dump several files' content into ICP. " +
986
+ "If one file holds several sections under headers, split it by header into multiple entries. " +
987
+ "Nous keeps a served copy of the prose and rebuilds the ICP scoring model from it; " +
960
988
  "the recorded source_path is what get_icp_model writes the learned model back into. " +
961
- "IF NO ICP FILE EXISTS: don't invent one in Nous. Offer to create `context/icp.md` from what the user " +
962
- "tells you (write it with your file tools), then call this on that file so their ICP lives in their " +
963
- "repo where they'll keep editing it. Re-run this after the user edits an ICP file to re-sync. The ICP " +
964
- "section's source_path matters most (it's the write-back target).",
989
+ "IF NO ICP FILES EXIST: don't invent context in Nous. Offer to SCAFFOLD a context/ folder in their " +
990
+ "repo context/icp.md, positioning.md, pricing.md, market.md, competitors.md, gtm-motion.md — " +
991
+ "filled from what the user tells you plus your own research of their website (write them with your " +
992
+ "file tools), then call this on those files — so their GTM context lives in their repo where they'll " +
993
+ "keep editing it. At minimum create context/icp.md if that's all they'll give you. Re-run this after " +
994
+ "the user edits any context file to re-sync. The ICP section's source_path matters most (it's the " +
995
+ "write-back target for get_icp_model).",
965
996
  {
966
997
  sections: z.array(z.object({
967
998
  section: z.enum(["ICP", "Market", "Product", "Pricing", "Competitors", "Positioning", "GTM Motion", "Notes"])