@opennous/mcp 0.48.0 → 0.50.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 +109 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennous/mcp",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "description": "Nous — the Context Graph for AI Agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/server.js CHANGED
@@ -13,21 +13,22 @@
13
13
  * Tools, by group (the authoritative live catalog is `node scripts/list-tools.mjs`):
14
14
  * READ get_context · get_account · verify · query · attention · get_action_items ·
15
15
  * pipeline · pipeline_intelligence · campaign_performance · score ·
16
- * search_notes · get_foundation · get_insights
16
+ * search_notes · list_notes · get_foundation · get_insights
17
17
  * WRITE record · record_signal · save_note · propose_vault_file · merge_contacts ·
18
18
  * sync_foundation
19
19
  * ACT send_linkedin_message
20
- * CORRECT retract_observation · delete_note
21
- * RUN get_workspace_status · set_workspace_profile · build_icp_model · train_icp_model ·
22
- * sync_icp · export_icp_model · connect_integration · configure_crm_sync ·
23
- * sync_crm_now · scrape_engagers · get_routing_preferences
20
+ * CORRECT retract_observation · delete_note · unmerge_contacts
21
+ * RUN get_workspace_status · whoami · list_integrations · set_workspace_profile ·
22
+ * build_icp_model · train_icp_model · sync_icp · export_icp_model ·
23
+ * connect_integration · configure_crm_sync · sync_crm_now · scrape_engagers ·
24
+ * get_routing_preferences
24
25
  */
25
26
 
26
27
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
28
  import { z } from "zod";
28
29
  import { get, post, del } from "./client.js";
29
30
 
30
- export const SERVER_VERSION = "0.48.0";
31
+ export const SERVER_VERSION = "0.50.0";
31
32
 
32
33
  // ─── helpers ──────────────────────────────────────────────────────────────────
33
34
 
@@ -406,12 +407,54 @@ export function createServer() {
406
407
  (r.relationships_repointed || r.relationships_removed)
407
408
  ? ` relationships: ${r.relationships_repointed} re-pointed, ${r.relationships_removed} pruned` : null,
408
409
  moved ? ` re-pointed: ${moved}` : null,
409
- `The duplicate is now a reversible tombstone (merged into the survivor).`,
410
+ `Reversible: if this was wrong, unmerge_contacts with drop_id "${r.drop_id}" puts it all back.`,
410
411
  ].filter(Boolean);
411
412
  return { content: [{ type: "text", text: lines.join("\n") }] };
412
413
  }
413
414
  );
414
415
 
416
+ server.tool(
417
+ "unmerge_contacts",
418
+ "REVERSE a merge — split a wrongly-merged duplicate back out into its own account. Identify the " +
419
+ "merge to undo either by `drop_id` (the tombstone's id, exactly as merge_contacts reported it) or by " +
420
+ "`keep` (an identifier for the survivor — undoes the MOST RECENT merge into it). Every re-pointed " +
421
+ "identifier, claim, observation, relationship and the record itself go back where they were. " +
422
+ "Forward-only: it can only reverse merges made after reversible-merge tracking existed — an older " +
423
+ "merge returns a clear 'not reversible'. Use this when two DIFFERENT people were merged by mistake; " +
424
+ "it is not for editing a correctly-merged account.",
425
+ {
426
+ drop_id: z.string().optional().describe("The merged-away entity's id, from the merge_contacts result. Provide this OR keep."),
427
+ keep: z.string().optional().describe("The survivor (email, LinkedIn URL, entity UUID, or name) — undoes the most recent un-reversed merge into it."),
428
+ },
429
+ async ({ drop_id, keep }) => {
430
+ if (!drop_id && !keep) {
431
+ return { content: [{ type: "text", text: "Give me the drop_id from the merge result, or the `keep` survivor whose last merge to undo." }] };
432
+ }
433
+ try {
434
+ const r = await post("/v2/accounts/unmerge", { drop_id, keep });
435
+ if (r.status === "ambiguous") {
436
+ const opts = (r.candidates ?? []).map(c => ` • ${c.name ?? "(unnamed)"} [${c.entity_id}]`).join("\n");
437
+ return { content: [{ type: "text", text: `"${keep}" matches several people. Re-call unmerge_contacts with one of these entity ids as keep:\n${opts}` }] };
438
+ }
439
+ const lines = [
440
+ `Un-merged — ${r.drop_id} is its own account again.`,
441
+ ` identifiers restored: ${r.identifiers}, claims: ${r.claims}, observations: ${r.observations}, relationships: ${r.relationships}`,
442
+ r.contact_restored ? ` the contact record was recreated.` : null,
443
+ ].filter(Boolean);
444
+ return { content: [{ type: "text", text: lines.join("\n") }] };
445
+ } catch (e) {
446
+ const msg = /not_reversible/.test(e.message)
447
+ ? "That merge can't be reversed — it predates reversible-merge tracking, or it was already un-merged."
448
+ : /no_reversible_merge/.test(e.message)
449
+ ? "No un-reversed merge on that survivor to undo."
450
+ : /entity_not_found/.test(e.message)
451
+ ? "Couldn't find that survivor — check the keep identifier."
452
+ : `Couldn't un-merge: ${e.message}`;
453
+ return { content: [{ type: "text", text: msg }] };
454
+ }
455
+ }
456
+ );
457
+
415
458
  // ===========================================================================
416
459
  // TOOL: record — POST /v2/observations
417
460
  // The single write verb. You observe — Nous derives the updated facts.
@@ -1652,6 +1695,65 @@ export function createServer() {
1652
1695
  }
1653
1696
  );
1654
1697
 
1698
+ // ===========================================================================
1699
+ // TOOLS: list + introspect — enumerate what's on the workspace, and who you are.
1700
+ // GET /v2/notes · /v2/workspace/integrations · /v2/workspace/members.
1701
+ // ===========================================================================
1702
+ server.tool(
1703
+ "list_notes",
1704
+ "List saved notes newest-first (chronological), optionally scoped to one person/company with " +
1705
+ "`focus`. This is the LIST companion to search_notes (which is semantic): use it for \"show me the " +
1706
+ "notes on X\", or to enumerate notes and get their ids — e.g. to pick one to delete_note. Returns " +
1707
+ "id, type, title, date and a snippet for each.",
1708
+ {
1709
+ focus: z.string().optional().describe("Restrict to one person/company (email, LinkedIn URL, entity UUID, or domain). Omit for the whole workspace."),
1710
+ limit: z.number().int().min(1).max(100).optional().describe("Max notes to return (default 20)."),
1711
+ },
1712
+ async ({ focus, limit }) => {
1713
+ const r = await get("/v2/notes", { ...(focus ? { focus } : {}), ...(limit ? { limit } : {}) });
1714
+ if (r.status === "ambiguous") {
1715
+ const opts = (r.candidates ?? []).map(c => ` • ${c.name ?? "(unnamed)"} [${c.entity_id}]`).join("\n");
1716
+ return { content: [{ type: "text", text: `"${focus}" matches several — re-call list_notes with one of these entity ids as focus:\n${opts}` }] };
1717
+ }
1718
+ if (!r.notes?.length) return { content: [{ type: "text", text: focus ? "No notes on that account." : "No notes saved yet." }] };
1719
+ const lines = r.notes.map(n => ` • ${n.title || n.type}${n.date ? ` (${n.date})` : ""} [${n.id}]\n ${n.snippet}`).join("\n");
1720
+ return { content: [{ type: "text", text: `${r.notes.length} note(s)${r.has_more ? " (more available — raise limit)" : ""}:\n${lines}` }] };
1721
+ }
1722
+ );
1723
+
1724
+ server.tool(
1725
+ "list_integrations",
1726
+ "List the integrations connected to this workspace (Gmail, HubSpot, Apollo, Instantly, LinkedIn, …) " +
1727
+ "— what's wired in and whether it's verified. Use before telling the user to connect something, or " +
1728
+ "to answer \"what's connected here?\".",
1729
+ {},
1730
+ async () => {
1731
+ const r = await get("/v2/workspace/integrations");
1732
+ if (!r.integrations?.length) return { content: [{ type: "text", text: "No integrations connected yet." }] };
1733
+ const lines = r.integrations.map(i => ` • ${i.display_name}${i.category ? ` (${i.category})` : ""}${i.verified ? "" : " — not verified"}`).join("\n");
1734
+ return { content: [{ type: "text", text: `Connected integrations:\n${lines}` }] };
1735
+ }
1736
+ );
1737
+
1738
+ server.tool(
1739
+ "whoami",
1740
+ "Report who this API key acts AS and who else is on the workspace — the agent's own identity in " +
1741
+ "Nous. Returns your scope (a MEMBER key sees only that member's private content plus the shared " +
1742
+ "graph; an ADMIN key sees all raw content) and the team roster with names and roles. Use it to " +
1743
+ "understand whose view you have and to reference teammates. Emails show only for an admin key.",
1744
+ {},
1745
+ async () => {
1746
+ const r = await get("/v2/workspace/members");
1747
+ const scope = r.you?.scope === "admin"
1748
+ ? "an ADMIN key — you see all raw content on this workspace"
1749
+ : "a MEMBER key — you see only your own private content plus the shared graph";
1750
+ const roster = (r.members || [])
1751
+ .map(m => ` • ${m.name || "(unnamed)"} — ${m.role}${m.you ? " (you)" : ""}${m.email ? ` · ${m.email}` : ""}`)
1752
+ .join("\n");
1753
+ return { content: [{ type: "text", text: `You are ${scope}.\n\nWorkspace members (${r.count ?? 0}):\n${roster || " (none)"}` }] };
1754
+ }
1755
+ );
1756
+
1655
1757
  // ===========================================================================
1656
1758
  // TOOL: get_routing_preferences
1657
1759
  // The routing preferences that make THIS agent default to Nous for GTM. The