@opennous/mcp 0.30.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "description": "Nous — the Context Graph for AI Agents.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
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
@@ -145,7 +145,8 @@ export function createServer() {
145
145
  "get_context",
146
146
  "Get engineered context for a specific task about a person or company. Pass their email (or " +
147
147
  "entity id) and the intent. Returns a focused, ranked context block: the facts that matter for " +
148
- "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 " +
149
150
  "stakeholders, open predictions, and the account's ICP fit score (0-100 + why). Call this before " +
150
151
  "drafting outreach, preparing for a meeting, " +
151
152
  "or making any decision about a person. A fact's freshness tells you whether to trust it: 'fresh' " +
@@ -175,8 +176,14 @@ export function createServer() {
175
176
  lines.push(`ICP FIT: ${ctx.icp.score}/100 — ${label}${ctx.icp.reason ? ` (${ctx.icp.reason})` : ""}`);
176
177
  lines.push("");
177
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
+ }
178
185
  if (ctx.claims?.length) {
179
- lines.push(`FACTS (${ctx.meta?.claims_returned ?? ctx.claims.length}):`);
186
+ lines.push(`ATTRIBUTES (${ctx.meta?.claims_returned ?? ctx.claims.length}):`);
180
187
  for (const c of ctx.claims) {
181
188
  lines.push(` ${c.property}: ${fmtVal(c.value)} [${pct(c.confidence)} · ${c.freshness}]`);
182
189
  }
@@ -229,7 +236,8 @@ export function createServer() {
229
236
  // ===========================================================================
230
237
  server.tool(
231
238
  "get_account",
232
- "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 " +
233
241
  "confidence and freshness, plus the recent activity timeline. Pass an email or entity UUID. " +
234
242
  "For a task-specific, ranked view, prefer get_context.",
235
243
  { id: z.string().describe("Email address or entity UUID") },
@@ -242,9 +250,15 @@ export function createServer() {
242
250
  lines.push(`ICP FIT: ${rec.icp.score}/100 — ${label}${rec.icp.reason ? ` (${rec.icp.reason})` : ""}`);
243
251
  lines.push("");
244
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
+ }
245
259
  const claims = Object.values(rec.claims ?? {});
246
260
  if (claims.length) {
247
- lines.push(`FACTS (${claims.length}):`);
261
+ lines.push(`ATTRIBUTES (${claims.length}):`);
248
262
  for (const c of claims) {
249
263
  lines.push(` ${c.property}: ${fmtVal(c.value)} [${pct(c.confidence)} · ${c.freshness}]`);
250
264
  }
@@ -355,7 +369,11 @@ export function createServer() {
355
369
  " 4. Scheduled meetings/calls are events with property 'interaction.meeting_scheduled' and a " +
356
370
  "future-dated `when`. For 'what's booked today/this week', set property:'interaction.meeting_scheduled' " +
357
371
  "with from/to bounding the day or week (since_days only looks backward and can't reach them), and " +
358
- "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.)",
359
377
  {
360
378
  scope: z.object({
361
379
  kind: z.enum(["event", "state"]).optional(),
@@ -367,6 +385,7 @@ export function createServer() {
367
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"),
368
386
  order: z.enum(["asc", "desc"]).optional().describe("observed_at order (default desc, newest first). Use 'asc' for an upcoming-meeting schedule (soonest first)"),
369
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."),
370
389
  }).describe("Corpus filter"),
371
390
  without: z.object({
372
391
  kind: z.enum(["event", "state"]).optional(),
@@ -386,7 +405,7 @@ export function createServer() {
386
405
  const r = await post("/v2/query", body);
387
406
  const head = `${r.matched} match${r.matched !== 1 ? "es" : ""}` +
388
407
  (r.sampled ? ` (showing ${r.returned})` : "") +
389
- (r.return === "entities" ? " · grouped by entity" : "");
408
+ (r.corpus === "facts" ? " · facts" : r.return === "entities" ? " · grouped by entity" : "");
390
409
  const roll = Object.entries(r.rollups?.by_type ?? {})
391
410
  .map(([t, n]) => `${n}× ${fmtType(t)}`).join(" · ");
392
411
  const lines = [head, roll].filter(Boolean);
@@ -395,7 +414,10 @@ export function createServer() {
395
414
  }
396
415
  lines.push("");
397
416
  for (const it of r.items ?? []) {
398
- 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") {
399
421
  lines.push(` ${it.entity_name ?? it.entity_id} ` +
400
422
  `(${it.matches} match${it.matches !== 1 ? "es" : ""}, last ${whenLabel(it.most_recent_type, it.most_recent_at)})` +
401
423
  (it.most_recent_value != null ? ` → ${fmtVal(it.most_recent_value)}` : "") +