@opennous/mcp 0.63.0 → 0.65.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 +3 -3
- package/src/server.js +113 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennous/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Nous
|
|
3
|
+
"version": "0.65.0",
|
|
4
|
+
"description": "Nous — the Context Graph for AI Agents.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -21,7 +21,7 @@
|
|
|
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
|
|
24
|
+
"typecheck": "echo 'no ts in mcp — skipping'",
|
|
25
25
|
"test": "node --test tests/*.test.mjs"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
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 —
|
|
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.
|
|
32
|
+
export const SERVER_VERSION = "0.65.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.
|
|
133
|
-
// stray non-primitive registration
|
|
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) => {
|
|
@@ -397,6 +397,106 @@ export function createServer() {
|
|
|
397
397
|
|
|
398
398
|
|
|
399
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
|
+
|
|
400
500
|
// ===========================================================================
|
|
401
501
|
// TOOL: record — POST /v2/observations
|
|
402
502
|
// The single write verb. You observe — Nous derives the updated facts.
|
|
@@ -429,7 +529,12 @@ export function createServer() {
|
|
|
429
529
|
"facts you actually reasoned from in `evidence_ids` — that is what lets Nous learn which KINDS of " +
|
|
430
530
|
"evidence are worth acting on, not just which actions worked. When the human answers, " +
|
|
431
531
|
"record the verdict: property 'decision.accepted' | 'decision.edited' | 'decision.rejected' with the same " +
|
|
432
|
-
"decision_id (pass drafted_body + sent_body on accepted and the edit is measured for you).
|
|
532
|
+
"decision_id (pass drafted_body + sent_body on accepted and the edit is measured for you). On the verdict, " +
|
|
533
|
+
"say what the correction MEANT — you are the only one who heard it: {interpretation:'cut the pricing " +
|
|
534
|
+
"paragraph', correction_kind:'rule'|'structure'|'claim'|'constraint'|'none', correction_scope:'once'|" +
|
|
535
|
+
"'person'|'workspace'|'product'}. 'none' and 'once' are correct most of the time; a typo or a detail only " +
|
|
536
|
+
"they knew teaches nothing, and recording it crowds out the real signal. Ask 'just this one, or always?' " +
|
|
537
|
+
"only when you genuinely cannot tell. Then pass that " +
|
|
433
538
|
"same decision_id on the interaction observation when the action fires. That is what turns advice into " +
|
|
434
539
|
"learnable data — without it a send is an orphan nobody can grade.",
|
|
435
540
|
{
|