@opennous/mcp 0.62.0 → 0.64.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 +1 -1
  2. package/src/server.js +118 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.62.0",
3
+ "version": "0.64.0",
4
4
  "description": "Nous \u2014 the Context Graph for AI Agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/server.js CHANGED
@@ -10,8 +10,8 @@
10
10
  * sees raw rows — it gets engineered, epistemics-tagged context. It never
11
11
  * "updates" — it records observations; Nous derives.
12
12
  *
13
- * The revenue-plugin surface — 7 primitives, and nothing else (legacy tools pruned):
14
- * READ get_context · get_account · query (search folds in via facts:true) · score
13
+ * The revenue-plugin surface — the primitives below, and nothing else (legacy tools pruned):
14
+ * READ get_context · get_account · query (search folds in via facts:true) · score · deals
15
15
  * WRITE record · record_insight (you observe; the engine derives; no overwrites)
16
16
  * IDENTITY whoami (who the key acts as · scope admin|member · GTM role[s])
17
17
  */
@@ -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.62.0";
32
+ export const SERVER_VERSION = "0.64.0";
33
33
 
34
34
  // ─── helpers ──────────────────────────────────────────────────────────────────
35
35
 
@@ -129,12 +129,12 @@ export function createServer() {
129
129
  // ICP: set_icp writes the hypothesis; record_closed_deals trains it on real
130
130
  // won/lost outcomes (contrastive lift). Identity: whoami (who the key acts as,
131
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.
132
+ // save_note is out. Deals: deal health + how likely each deal is to close, and why. The
133
+ // `tool()` guard keeps the surface locked to these ten — a stray non-primitive registration
134
+ // is dropped, not exposed. See docs/revenue-plugin/README.md §2–3.
135
135
  const PLUGIN_TOOLS = new Set([
136
136
  "record", "record_insight", "get_context", "get_account", "query", "score", "whoami", "set_icp",
137
- "record_closed_deals",
137
+ "record_closed_deals", "deals",
138
138
  ]);
139
139
  const _tool = server.tool.bind(server);
140
140
  const tool = (name, ...rest) => {
@@ -300,6 +300,17 @@ export function createServer() {
300
300
  lines.push(`ICP FIT: ${rec.icp.score}/100 — ${label}${rec.icp.reason ? ` (${rec.icp.reason})` : ""}`);
301
301
  lines.push("");
302
302
  }
303
+ if (rec.scorecard) {
304
+ // The latest call scorecard the app computed (Coaching v2). The after-call skill writes the
305
+ // coaching review out of THIS — it does not re-score. Each dimension carries its exact moment.
306
+ const sc = rec.scorecard;
307
+ lines.push(`LAST CALL SCORE: ${sc.overall}/100 · ${sc.next_step_secured ? "next step secured" : "no next step secured"}${sc.occurred_at ? ` (${relAge(sc.occurred_at)})` : ""}`);
308
+ for (const d of (sc.scores ?? [])) {
309
+ lines.push(` [${d.score}] ${d.label}${d.note ? ` — ${d.note}` : ""}`);
310
+ if (d.moment) lines.push(` "${d.moment}"`);
311
+ }
312
+ lines.push("");
313
+ }
303
314
  if (rec.facts?.length) {
304
315
  // Atomic memory — the durable, decision-relevant facts learned about them.
305
316
  lines.push(`FACTS (${rec.facts.length} — durable memory about them):`);
@@ -386,6 +397,106 @@ export function createServer() {
386
397
 
387
398
 
388
399
 
400
+ // ===========================================================================
401
+ // TOOL: deals — GET /v2/deals · GET /v2/deals/:id
402
+ // Deal health next to how likely a deal is to close, and why. Per deal — the
403
+ // revenue roll-up is not this tool.
404
+ // ===========================================================================
405
+ tool(
406
+ "deals",
407
+ "DEAL HEALTH and HOW LIKELY A DEAL IS TO CLOSE, with the reasons. Two ways to call it:\n" +
408
+ " • no `account` → the open deals most likely to close within `within_days` (default 30), ranked. Each carries " +
409
+ "its chance to close in that window, its overall chance to win, its deal health (score, band, what to watch) and " +
410
+ "the facts that moved its odds. Use for 'which deals are most likely to close in the next 30 days', 'what will " +
411
+ "close this month', 'what's closest to closing'.\n" +
412
+ " • `account` (email, domain, LinkedIn URL, entity UUID, or name) → that one deal: health with all four signals " +
413
+ "and the engagement read, its chance to close overall, within `within_days` and within 90 days, the stage-only " +
414
+ "baseline, and every fact pushing the odds up or down. Use for 'how likely is Acme to close', 'what's the deal " +
415
+ "health on Acme', 'will Acme close this month'.\n" +
416
+ "The odds are the stage's base rate moved by what the graph recorded: open objections, competitors, stall, " +
417
+ "silence, threading, ICP fit, intent. 'unknown' within N days means this stage has no closed deals to judge " +
418
+ "timing on yet: say so, never call it 0%. When a NOTE says the weights are defaults, pass that on.",
419
+ {
420
+ account: z.string().optional().describe("One account to judge: email, domain, LinkedIn URL, entity UUID, or name. Omit to rank the open deals."),
421
+ within_days: z.number().optional().describe("The window for 'likely to close within N days'. Default 30."),
422
+ limit: z.number().optional().describe("How many ranked deals to return (max 25, default 10). Ignored with `account`."),
423
+ },
424
+ async ({ account, within_days, limit }) => {
425
+ const pctTxt = (v) => (v == null ? "unknown" : `${v}%`);
426
+ const q = within_days ? { within_days } : {};
427
+ const lines = [];
428
+
429
+ if (account) {
430
+ const r = await get(`/v2/deals/${encodeURIComponent(account)}`, q);
431
+ if (r.status === "ambiguous") {
432
+ const opts = (r.candidates ?? []).map(c =>
433
+ ` • ${c.name ?? "(unnamed)"}${c.detail ? ` — ${c.detail}` : ""} [${c.entity_id}]`).join("\n");
434
+ return { content: [{ type: "text", text:
435
+ `"${account}" matches several people. Call deals again with one of these entity ids:\n${opts}` }] };
436
+ }
437
+ const name = r.name ?? r.entity_id;
438
+ if (r.status === "open") {
439
+ const windows = Object.entries(r.close_within_pct ?? {})
440
+ .map(([d, v]) => `${pctTxt(v)} within ${d}d`).join(" · ");
441
+ lines.push(`${name} · ${r.entity_id} · stage ${r.stage}, ${r.days_in_stage}d in stage` +
442
+ (r.typical_days_in_stage != null ? ` (typical ${r.typical_days_in_stage}d)` : ""));
443
+ lines.push(`CLOSE: ${pctTxt(r.win_pct)} to win · ${windows} (stage alone: ${pctTxt(r.stage_only_win_pct)})`);
444
+ if (r.timing && r.timing !== "judged") lines.push(`TIMING: ${r.timing}`);
445
+ if (r.why?.length) {
446
+ lines.push("WHY:");
447
+ for (const y of r.why) lines.push(` ${y}`);
448
+ }
449
+ } else {
450
+ lines.push(`${name} · ${r.entity_id}${r.stage ? ` · ${r.stage}` : ""}`);
451
+ lines.push(r.note);
452
+ }
453
+ if (r.icp) {
454
+ const i = r.icp;
455
+ if (!i.scored) lines.push(`ICP: ${i.note}`);
456
+ else {
457
+ lines.push(`ICP: ${i.score ?? "—"}/100${i.tier ? ` ${i.tier}` : ""}${i.via_person?.name ? ` (scored on ${i.via_person.name})` : ""}`);
458
+ if (i.fired?.length) for (const f of i.fired) lines.push(` +${f.weight} ${f.label}`);
459
+ if (i.note) lines.push(` ${i.note}`);
460
+ else if (i.missing_inputs?.length) lines.push(` not on the record: ${i.missing_inputs.join(", ")}`);
461
+ }
462
+ }
463
+ if (r.health) {
464
+ lines.push(`HEALTH: ${r.health.score ?? "—"}/100 ${r.health.band ?? ""}`.trim());
465
+ if (r.health.evidence?.note) lines.push(` ${r.health.evidence.note}`);
466
+ for (const s of r.health.signals ?? []) lines.push(` ${s.signal}: ${s.level} — ${s.reason}`);
467
+ const e = r.health.engagement ?? {};
468
+ const eng = [
469
+ e.last_response && `last response ${e.last_response}`,
470
+ e.engagement_trend && `engagement ${e.engagement_trend}`,
471
+ e.multi_threading && `${e.multi_threading}`,
472
+ e.response_time && e.response_time !== "—" && `buyer replies in ${e.response_time}`,
473
+ e.meetings && e.meetings,
474
+ ].filter(Boolean);
475
+ if (eng.length) lines.push(` ${eng.join(" · ")}`);
476
+ }
477
+ if (r.via_person) lines.push(`(resolved from a person at this account: ${r.via_person})`);
478
+ for (const n of r.model?.notes ?? []) lines.push(`NOTE: ${n}`);
479
+ return { content: [{ type: "text", text: lines.join("\n") }] };
480
+ }
481
+
482
+ const r = await get("/v2/deals", { ...q, ...(limit ? { limit } : {}) });
483
+ const deals = r.deals ?? [];
484
+ lines.push(`Open deals ranked by ${r.ranked_by}. Showing ${deals.length} of ${r.open_deals} open; ` +
485
+ `timing known for ${r.timing_known}.`);
486
+ deals.forEach((d, i) => {
487
+ lines.push(`${i + 1}. ${d.name ?? "(unnamed)"} [${d.entity_id}] — ${d.stage}, ${d.days_in_stage}d · ` +
488
+ `${pctTxt(d.close_within_pct?.[r.within_days])} within ${r.within_days}d · ${pctTxt(d.win_pct)} to win · ` +
489
+ `health ${d.health?.score ?? "—"}${d.health?.band ? ` ${d.health.band}` : ""}`);
490
+ if (d.timing && d.timing !== "judged") lines.push(` timing: ${d.timing}`);
491
+ if (d.why?.length) lines.push(` why: ${d.why.join("; ")}`);
492
+ if (d.health?.watch?.length) lines.push(` watch: ${d.health.watch.join("; ")}`);
493
+ });
494
+ if (!deals.length) lines.push("No open deals: nothing has a reconstructed stage between first contact and a close.");
495
+ for (const n of r.model?.notes ?? []) lines.push(`NOTE: ${n}`);
496
+ return { content: [{ type: "text", text: lines.join("\n") }] };
497
+ }
498
+ );
499
+
389
500
  // ===========================================================================
390
501
  // TOOL: record — POST /v2/observations
391
502
  // The single write verb. You observe — Nous derives the updated facts.