@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.
- package/package.json +1 -1
- package/src/server.js +109 -7
package/package.json
CHANGED
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 ·
|
|
22
|
-
*
|
|
23
|
-
* sync_crm_now · scrape_engagers ·
|
|
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.
|
|
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
|
-
`
|
|
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
|