@opennous/mcp 0.60.0 → 0.62.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 +4 -3
  2. package/src/server.js +83 -20
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.60.0",
4
- "description": "Nous the Context Graph for AI Agents.",
3
+ "version": "0.62.0",
4
+ "description": "Nous \u2014 the Context Graph for AI Agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -21,7 +21,8 @@
21
21
  "start": "node src/index.js",
22
22
  "dev:http": "node --watch src/http.js",
23
23
  "start:http": "node src/http.js",
24
- "typecheck": "echo 'no ts in mcp skipping'"
24
+ "typecheck": "echo 'no ts in mcp \u2014 skipping'",
25
+ "test": "node --test tests/*.test.mjs"
25
26
  },
26
27
  "keywords": [
27
28
  "mcp",
package/src/server.js CHANGED
@@ -29,7 +29,7 @@ const APP_URL = () => (process.env.NOUS_APP_URL || "https://app.opennous.cloud")
29
29
  const OAUTH_CONNECT = { gmail: "gmail", gmail_oauth: "gmail", google: "gmail", "google-mail": "gmail", googlemail: "gmail", linkedin: "linkedin" };
30
30
  const connectLink = (slug) => `${APP_URL()}/settings?section=integrations&connect=${slug}`;
31
31
 
32
- export const SERVER_VERSION = "0.60.0";
32
+ export const SERVER_VERSION = "0.62.0";
33
33
 
34
34
  // ─── helpers ──────────────────────────────────────────────────────────────────
35
35
 
@@ -91,6 +91,24 @@ const pct = (c) => `${Math.round((c ?? 0) * 100)}%`;
91
91
 
92
92
  // ─── factory ──────────────────────────────────────────────────────────────────
93
93
 
94
+ // A closed deal as the agent may supply it. A bare domain is the original shape and still
95
+ // works; the object form carries the money and the date, which the forecast needs. Widened
96
+ // rather than replaced so nothing that already calls this breaks.
97
+ //
98
+ // Module scope, not inside createServer(). It was declared below its own first use, and a
99
+ // `const` in the temporal dead zone throws on every request — the hosted server crashed on
100
+ // each call with "Cannot access 'CLOSED_DEAL' before initialization". Out here it is also
101
+ // built once instead of per request, since createServer() runs for every incoming call.
102
+ const CLOSED_DEAL = z.union([
103
+ z.string(),
104
+ z.object({
105
+ domain: z.string().describe("Company domain, e.g. 'acme.com' — no scheme."),
106
+ amount: z.number().optional().describe("Deal value in major units (48000, not 4800000)."),
107
+ currency: z.string().optional().describe("ISO code or symbol. Defaults to the workspace currency."),
108
+ closed_at: z.string().optional().describe("When it actually closed, ISO date. Drives every cycle-time measurement, so pass the real date rather than today's."),
109
+ }),
110
+ ]);
111
+
94
112
  export function createServer() {
95
113
  const server = new McpServer({
96
114
  name: "nous",
@@ -105,15 +123,18 @@ export function createServer() {
105
123
  ],
106
124
  });
107
125
 
108
- // Nous is fully a revenue plugin: this server exposes EXACTLY the 7 primitives and
126
+ // Nous is fully a revenue plugin: this server exposes EXACTLY these primitives and
109
127
  // nothing else (the ~30 legacy tools were pruned). Writes: record · record_insight.
110
128
  // Reads: get_context · get_account · query (search folds in via facts:true) · score.
111
- // Identity: whoami (who the key acts as, scope, GTM role[s]) — the governance
112
- // substrate. record_signal folds into record; save_note is out. The `tool()` guard
113
- // keeps the surface locked to these eight a stray non-primitive registration is
114
- // dropped, not exposed. See docs/revenue-plugin/README.md §2–3.
129
+ // ICP: set_icp writes the hypothesis; record_closed_deals trains it on real
130
+ // won/lost outcomes (contrastive lift). Identity: whoami (who the key acts as,
131
+ // scope, GTM role[s]) the governance substrate. record_signal folds into record;
132
+ // save_note is out. The `tool()` guard keeps the surface locked to these nine — a
133
+ // stray non-primitive registration is dropped, not exposed. See
134
+ // docs/revenue-plugin/README.md §2–3.
115
135
  const PLUGIN_TOOLS = new Set([
116
136
  "record", "record_insight", "get_context", "get_account", "query", "score", "whoami", "set_icp",
137
+ "record_closed_deals",
117
138
  ]);
118
139
  const _tool = server.tool.bind(server);
119
140
  const tool = (name, ...rest) => {
@@ -387,8 +408,12 @@ export function createServer() {
387
408
  "Insights about YOUR OWN business (product/positioning/market/buyer) go to record_insight, not here. " +
388
409
  "CLOSING THE LOOP — when you RECOMMEND an action, write the recommendation down before it happens: " +
389
410
  "{kind:'state',property:'decision.proposed',value:{decision_id:'<your id>',proposal:'send a follow-up " +
390
- "naming the security objection',rationale:'the security review is the real blocker at this stage', " +
411
+ "naming the security objection',proposal_body:'<the exact draft>',rationale:'the security review " +
412
+ "is the real blocker at this stage', " +
391
413
  "evidence_ids:['<claim/note id you reasoned from>'],action_type:'email_send',category:'outreach'}}. " +
414
+ "`proposal_body` is the draft itself — hashed and discarded, never stored, and the only way to tell " +
415
+ "later whether the human sent it as written or rewrote it first. Omit it and the decision reports as " +
416
+ "sent-verbatim whatever happened. " +
392
417
  "Keep `proposal` (WHAT you are doing) apart from `rationale` (WHY you think it works), and list the " +
393
418
  "facts you actually reasoned from in `evidence_ids` — that is what lets Nous learn which KINDS of " +
394
419
  "evidence are worth acting on, not just which actions worked. When the human answers, " +
@@ -484,6 +509,57 @@ export function createServer() {
484
509
  }
485
510
  );
486
511
 
512
+ // ===========================================================================
513
+ // TOOL: record_closed_deals — POST /v2/workspace/closed-deals
514
+ // Train the ICP on REAL outcomes. set_icp writes the hypothesis; this upgrades
515
+ // it to an outcome-graded model: feed the won and lost domains (the CRM/Stripe
516
+ // closed cohorts), the engine runs contrastive lift — what separates wins from
517
+ // losses — links known contacts, resolves their open predictions with the real
518
+ // outcome, and re-scores every open account. The onboarding + win-loss payoff.
519
+ // ===========================================================================
520
+ tool(
521
+ "record_closed_deals",
522
+ "Train this workspace's ICP on REAL closed deals. Pass the won and lost deals (each a bare " +
523
+ "domain, or an object {domain, amount, currency, closed_at}). The engine enriches each domain, " +
524
+ "links the contacts you already have there, resolves their open ICP predictions with the real " +
525
+ "outcome, runs contrastive lift (the signals that separate wins from losses), and re-scores every " +
526
+ "open account against the upgraded model. Use it in onboarding once the backfill has landed the " +
527
+ "closed cohorts (pull them with query scope.property:'stage' — accounts at 'closed_won' and " +
528
+ "'closed_lost'), and whenever new deals close. Needs at least one deal; both sides (some won AND " +
529
+ "some lost) make the lift trustworthy — one side alone is directional only. The ICP is the one " +
530
+ "company model, so this is an admin/founder action.",
531
+ {
532
+ won: z.array(CLOSED_DEAL).optional().describe("Closed-WON deals — bare domains or {domain, amount, currency, closed_at}. Pull from accounts at stage 'closed_won'."),
533
+ lost: z.array(CLOSED_DEAL).optional().describe("Closed-LOST deals — same shape. Pull from accounts at stage 'closed_lost'."),
534
+ },
535
+ async ({ won = [], lost = [] }) => {
536
+ const r = await post("/v2/workspace/closed-deals", { won, lost });
537
+ if (r.need_more_deals || r.error === "need_more_deals") {
538
+ return { content: [{ type: "text", text:
539
+ "Need at least one closed deal (a won or lost domain) to train the ICP. Pull the closed cohorts with query scope.property:'stage', then pass their domains." }] };
540
+ }
541
+ const lines = [
542
+ `ICP trained on real outcomes — ${r.won || 0} won · ${r.lost || 0} lost` +
543
+ (r.enriched ? ` · ${r.enriched} companies enriched` : "") +
544
+ (r.surfaced ? ` · ${r.surfaced} predictions resolved` : "") + ".",
545
+ ];
546
+ if ((r.discovered || []).length) {
547
+ lines.push("", "Signals the model learned (contrastive lift, won vs lost):");
548
+ for (const d of r.discovered) {
549
+ const dir = (d.weight ?? 0) > 0 ? "+" : "";
550
+ lines.push(` • ${d.label} [${dir}${d.weight}]${d.note ? ` — ${d.note}` : ""}`);
551
+ }
552
+ }
553
+ if ((r.linked || []).length) {
554
+ const names = r.linked.map(l => l.name).filter(Boolean);
555
+ lines.push("", `Linked ${r.linked.length} known contact${r.linked.length !== 1 ? "s" : ""} to these deals` +
556
+ (names.length ? `: ${names.slice(0, 8).join(", ")}${names.length > 8 ? "…" : ""}` : "") + ".");
557
+ }
558
+ lines.push("", "Open accounts have been re-scored. score / get_context now reflect the outcome-graded ICP.");
559
+ return { content: [{ type: "text", text: lines.join("\n") }] };
560
+ }
561
+ );
562
+
487
563
  // ===========================================================================
488
564
  // TOOL: query — POST /v2/query
489
565
  // Retrieve a corpus of activity across many people. You do the analysis.
@@ -691,19 +767,6 @@ export function createServer() {
691
767
 
692
768
 
693
769
 
694
- // ===========================================================================
695
- // A closed deal as the agent may supply it. A bare domain is the original shape and still
696
- // works; the object form carries the money and the date, which the forecast needs. Widened
697
- // rather than replaced so nothing that already calls this breaks.
698
- const CLOSED_DEAL = z.union([
699
- z.string(),
700
- z.object({
701
- domain: z.string().describe("Company domain, e.g. 'acme.com' — no scheme."),
702
- amount: z.number().optional().describe("Deal value in major units (48000, not 4800000)."),
703
- currency: z.string().optional().describe("ISO code or symbol. Defaults to the workspace currency."),
704
- closed_at: z.string().optional().describe("When it actually closed, ISO date. Drives every cycle-time measurement, so pass the real date rather than today's."),
705
- }),
706
- ]);
707
770
 
708
771
 
709
772