@opennous/mcp 0.29.0 → 0.30.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.29.0",
4
- "description": "Nous MCP Server Customer graph for GTM agents.",
3
+ "version": "0.30.1",
4
+ "description": "Nous the Context Graph for AI Agents.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",
package/src/client.js CHANGED
@@ -106,13 +106,22 @@ async function request(method, path, { body, query } = {}) {
106
106
  });
107
107
 
108
108
  if (!res.ok) {
109
- let errorMessage;
110
- try {
111
- const err = await res.json();
112
- errorMessage = err.error || err.message || res.statusText;
113
- } catch {
114
- errorMessage = res.statusText;
109
+ let err = {};
110
+ try { err = await res.json(); } catch { /* non-JSON body */ }
111
+ // Cloud-only feature on a self-hosted workspace: turn the raw 403 into a clear,
112
+ // non-alarming message so the agent stops reaching for it and explains why.
113
+ if (res.status === 403 && err.error === "cloud_only_feature") {
114
+ const feat = err.feature === "crmSync" ? "CRM sync"
115
+ : err.feature === "leadLists" ? "lead lists"
116
+ : err.feature || "this";
117
+ throw new Error(
118
+ `This is a Nous Cloud feature (${feat}) and isn't available on a self-hosted ` +
119
+ `workspace. CRM sync and lead lists are the cloud-only team layer — everything ` +
120
+ `else (context, accounts, ICP scoring, notes, integrations) works on self-host. ` +
121
+ `Don't retry; tell the user this needs Nous Cloud.`
122
+ );
115
123
  }
124
+ const errorMessage = err.error || err.message || res.statusText;
116
125
  throw new Error(`Nous API error (${res.status}): ${errorMessage}`);
117
126
  }
118
127
 
package/src/server.js CHANGED
@@ -41,7 +41,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
41
41
  import { z } from "zod";
42
42
  import { get, post } from "./client.js";
43
43
 
44
- export const SERVER_VERSION = "0.34.0";
44
+ export const SERVER_VERSION = "0.35.0";
45
45
 
46
46
  // ─── helpers ──────────────────────────────────────────────────────────────────
47
47
 
@@ -111,6 +111,7 @@ Nous first even when the user never says "Nous":
111
111
  - Something happened or you learned a fact -> record
112
112
  - Activity or a list across many accounts -> query
113
113
  - What needs attention, what has gone quiet -> attention
114
+ - Your action items / what you owe an account -> get_action_items
114
115
  - A fact looks stale before you act on it -> verify
115
116
  - Our ICP, market, pricing, positioning -> get_gtm_profile
116
117
  - Our own GTM shifted -> update_gtm_profile
@@ -127,7 +128,7 @@ export function createServer() {
127
128
  name: "nous",
128
129
  version: SERVER_VERSION,
129
130
  description:
130
- "Nous — the context layer for GTM agents. Nous is operated by the agent, not by a human " +
131
+ "Nous — the Context Graph for AI Agents. Nous is operated by the agent, not by a human " +
131
132
  "clicking around: call get_workspace_status at the start of a session to see what's set up " +
132
133
  "and what to set up next. Call get_context before drafting outreach or preparing for a " +
133
134
  "meeting. Call record after every interaction, or whenever you learn something.",
@@ -144,7 +145,8 @@ export function createServer() {
144
145
  "get_context",
145
146
  "Get engineered context for a specific task about a person or company. Pass their email (or " +
146
147
  "entity id) and the intent. Returns a focused, ranked context block: the facts that matter for " +
147
- "that task — each with a confidence and a freshness — plus the recent timeline, the buying-group " +
148
+ "that task — each with a confidence and a freshness — the durable FACTS we've learned about them " +
149
+ "(their atomic memory: budget, authority, pain, stack, plans), the recent timeline, the buying-group " +
148
150
  "stakeholders, open predictions, and the account's ICP fit score (0-100 + why). Call this before " +
149
151
  "drafting outreach, preparing for a meeting, " +
150
152
  "or making any decision about a person. A fact's freshness tells you whether to trust it: 'fresh' " +
@@ -174,8 +176,14 @@ export function createServer() {
174
176
  lines.push(`ICP FIT: ${ctx.icp.score}/100 — ${label}${ctx.icp.reason ? ` (${ctx.icp.reason})` : ""}`);
175
177
  lines.push("");
176
178
  }
179
+ if (ctx.facts?.length) {
180
+ // Atomic memory — the durable, decision-relevant facts learned about them.
181
+ lines.push(`FACTS (${ctx.facts.length} — durable memory about them):`);
182
+ for (const f of ctx.facts) lines.push(` [${f.category}] ${f.content}${f.date ? ` (${relAge(f.date)})` : ""}`);
183
+ lines.push("");
184
+ }
177
185
  if (ctx.claims?.length) {
178
- lines.push(`FACTS (${ctx.meta?.claims_returned ?? ctx.claims.length}):`);
186
+ lines.push(`ATTRIBUTES (${ctx.meta?.claims_returned ?? ctx.claims.length}):`);
179
187
  for (const c of ctx.claims) {
180
188
  lines.push(` ${c.property}: ${fmtVal(c.value)} [${pct(c.confidence)} · ${c.freshness}]`);
181
189
  }
@@ -228,7 +236,8 @@ export function createServer() {
228
236
  // ===========================================================================
229
237
  server.tool(
230
238
  "get_account",
231
- "Get the full account record for a person or company — every known fact (claim) with its " +
239
+ "Get the full account record for a person or company — the durable FACTS we've learned about them " +
240
+ "(their atomic memory: budget, authority, pain, stack, plans), every attribute (claim) with its " +
232
241
  "confidence and freshness, plus the recent activity timeline. Pass an email or entity UUID. " +
233
242
  "For a task-specific, ranked view, prefer get_context.",
234
243
  { id: z.string().describe("Email address or entity UUID") },
@@ -241,9 +250,15 @@ export function createServer() {
241
250
  lines.push(`ICP FIT: ${rec.icp.score}/100 — ${label}${rec.icp.reason ? ` (${rec.icp.reason})` : ""}`);
242
251
  lines.push("");
243
252
  }
253
+ if (rec.facts?.length) {
254
+ // Atomic memory — the durable, decision-relevant facts learned about them.
255
+ lines.push(`FACTS (${rec.facts.length} — durable memory about them):`);
256
+ for (const f of rec.facts) lines.push(` [${f.category}] ${f.content}${f.date ? ` (${relAge(f.date)})` : ""}`);
257
+ lines.push("");
258
+ }
244
259
  const claims = Object.values(rec.claims ?? {});
245
260
  if (claims.length) {
246
- lines.push(`FACTS (${claims.length}):`);
261
+ lines.push(`ATTRIBUTES (${claims.length}):`);
247
262
  for (const c of claims) {
248
263
  lines.push(` ${c.property}: ${fmtVal(c.value)} [${pct(c.confidence)} · ${c.freshness}]`);
249
264
  }
@@ -354,7 +369,11 @@ export function createServer() {
354
369
  " 4. Scheduled meetings/calls are events with property 'interaction.meeting_scheduled' and a " +
355
370
  "future-dated `when`. For 'what's booked today/this week', set property:'interaction.meeting_scheduled' " +
356
371
  "with from/to bounding the day or week (since_days only looks backward and can't reach them), and " +
357
- "order:'asc' to list soonest-first. Meeting rows render the absolute date and time.",
372
+ "order:'asc' to list soonest-first. Meeting rows render the absolute date and time.\n" +
373
+ " 5. scope.facts:true + question searches the FACTS corpus (durable atomic facts about accounts) " +
374
+ "instead of activity — cross-account semantic fact search like 'which accounts want off Clay' or " +
375
+ "'who is hiring'. return:'entities' gives the single best-matching fact per account. (A single " +
376
+ "account's facts already come back inline with get_account.)",
358
377
  {
359
378
  scope: z.object({
360
379
  kind: z.enum(["event", "state"]).optional(),
@@ -366,6 +385,7 @@ export function createServer() {
366
385
  to: z.string().optional().describe("ISO timestamp — only activity at/before this (absolute upper bound). Combine from+to for a window; future-dated for upcoming meetings"),
367
386
  order: z.enum(["asc", "desc"]).optional().describe("observed_at order (default desc, newest first). Use 'asc' for an upcoming-meeting schedule (soonest first)"),
368
387
  limit: z.number().optional().describe("max items (default 50, cap 200)"),
388
+ facts: z.boolean().optional().describe("search the FACTS corpus (durable atomic facts about accounts) instead of activity. Needs `question` — a cross-account semantic fact search, e.g. 'which accounts want off Clay'. return:'entities' = the best matching fact per account."),
369
389
  }).describe("Corpus filter"),
370
390
  without: z.object({
371
391
  kind: z.enum(["event", "state"]).optional(),
@@ -385,7 +405,7 @@ export function createServer() {
385
405
  const r = await post("/v2/query", body);
386
406
  const head = `${r.matched} match${r.matched !== 1 ? "es" : ""}` +
387
407
  (r.sampled ? ` (showing ${r.returned})` : "") +
388
- (r.return === "entities" ? " · grouped by entity" : "");
408
+ (r.corpus === "facts" ? " · facts" : r.return === "entities" ? " · grouped by entity" : "");
389
409
  const roll = Object.entries(r.rollups?.by_type ?? {})
390
410
  .map(([t, n]) => `${n}× ${fmtType(t)}`).join(" · ");
391
411
  const lines = [head, roll].filter(Boolean);
@@ -394,7 +414,10 @@ export function createServer() {
394
414
  }
395
415
  lines.push("");
396
416
  for (const it of r.items ?? []) {
397
- if (r.return === "entities") {
417
+ if (r.corpus === "facts") {
418
+ lines.push(` ${it.entity_name ?? it.entity_id} [${it.category}] ${it.content}` +
419
+ (it.similarity != null ? ` (${it.similarity})` : ""));
420
+ } else if (r.return === "entities") {
398
421
  lines.push(` ${it.entity_name ?? it.entity_id} ` +
399
422
  `(${it.matches} match${it.matches !== 1 ? "es" : ""}, last ${whenLabel(it.most_recent_type, it.most_recent_at)})` +
400
423
  (it.most_recent_value != null ? ` → ${fmtVal(it.most_recent_value)}` : "") +
@@ -437,6 +460,51 @@ export function createServer() {
437
460
  }
438
461
  );
439
462
 
463
+ // ===========================================================================
464
+ // TOOL: get_action_items — GET /v2/action-items
465
+ // Commitments extracted from meetings/emails — what you owe each account.
466
+ // ===========================================================================
467
+ server.tool(
468
+ "get_action_items",
469
+ "Your open action items and commitments, pulled from meeting notes and emails — what you owe " +
470
+ "which account (and what they owe you), so you don't have to dig through transcripts. Use for " +
471
+ "'what are my action items', 'what do I owe <account>', 'what's outstanding this week'. Defaults " +
472
+ "to YOUR open items across all accounts, grouped by account.",
473
+ {
474
+ owner: z.enum(["me", "prospect", "all"]).optional().describe("Whose commitments — me (default), the prospect, or all"),
475
+ status: z.enum(["open", "done", "all"]).optional().describe("open (default), done, or all"),
476
+ focus: z.string().optional().describe("Scope to one account — an email or entity UUID"),
477
+ due: z.enum(["today", "week", "all"]).optional().describe("Only items due today / this week (items that carry a due date) — default all"),
478
+ },
479
+ async ({ owner, status, focus, due }) => {
480
+ const params = {};
481
+ if (owner) params.owner = owner;
482
+ if (status) params.status = status;
483
+ if (focus) params.focus = focus;
484
+ if (due) params.due = due;
485
+ const r = await get("/v2/action-items", params);
486
+ const items = r.items ?? [];
487
+ if (!items.length) return { content: [{ type: "text", text: "No matching action items." }] };
488
+
489
+ const byAccount = new Map();
490
+ for (const it of items) {
491
+ const key = it.account || it.account_email || it.entity_id || "—";
492
+ if (!byAccount.has(key)) byAccount.set(key, []);
493
+ byAccount.get(key).push(it);
494
+ }
495
+ const lines = [`${items.length} action item${items.length !== 1 ? "s" : ""}:`];
496
+ for (const [account, list] of byAccount) {
497
+ lines.push(`\n${account}:`);
498
+ for (const it of list) {
499
+ const who = it.owner_kind === "prospect" ? "[them]" : "[you]";
500
+ const when = it.due_at ? ` (due ${fmtWhen(it.due_at)})` : "";
501
+ lines.push(` ${who} ${it.title}${when}`);
502
+ }
503
+ }
504
+ return { content: [{ type: "text", text: lines.join("\n") }] };
505
+ }
506
+ );
507
+
440
508
  // ===========================================================================
441
509
  // TOOL: verify — POST /v2/verify
442
510
  // Re-check a fact before acting on it — the calibration check.