@opennous/mcp 0.49.0 → 0.51.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 +103 -6
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
|
-
* ACT send_linkedin_message
|
|
19
|
+
* ACT draft_email · send_linkedin_message
|
|
20
20
|
* CORRECT retract_observation · delete_note · unmerge_contacts
|
|
21
|
-
* RUN get_workspace_status ·
|
|
22
|
-
*
|
|
23
|
-
* sync_crm_now · scrape_engagers ·
|
|
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.51.0";
|
|
31
32
|
|
|
32
33
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
33
34
|
|
|
@@ -1638,6 +1639,43 @@ export function createServer() {
|
|
|
1638
1639
|
}
|
|
1639
1640
|
);
|
|
1640
1641
|
|
|
1642
|
+
server.tool(
|
|
1643
|
+
"draft_email",
|
|
1644
|
+
"DRAFT a follow-up email for the operator to review and SEND themselves — you draft, they send. " +
|
|
1645
|
+
"This does NOT send: it puts a card in front of the operator with To / Subject / Body and a Send " +
|
|
1646
|
+
"button; the email goes out from THEIR OWN Gmail, in their name, only when they click send. Use it " +
|
|
1647
|
+
"whenever the ask is to email, follow up by email, send a recap, or reply by email. Identify the " +
|
|
1648
|
+
"recipient with `focus` (their email, LinkedIn URL, entity id, or name); the recipient address is " +
|
|
1649
|
+
"read off the record unless you pass `to`. Write a real `subject` and `body` in the operator's " +
|
|
1650
|
+
"voice — real paragraphs, no placeholders or [brackets] — and give the `rationale`, the one line " +
|
|
1651
|
+
"they judge the draft against. For a LinkedIn-only contact with no email, use send_linkedin_message.",
|
|
1652
|
+
{
|
|
1653
|
+
focus: z.string().describe("The recipient — email, LinkedIn URL, entity UUID, or name."),
|
|
1654
|
+
subject: z.string().describe("The email subject line."),
|
|
1655
|
+
body: z.string().min(1).describe("The email body, in the operator's voice. Real paragraphs, no placeholders."),
|
|
1656
|
+
to: z.string().optional().describe("Override the recipient email address (else the entity's best email on record is used)."),
|
|
1657
|
+
rationale: z.string().optional().describe("One line on what the draft is based on — shown to the operator on the card."),
|
|
1658
|
+
},
|
|
1659
|
+
async ({ focus, subject, body, to, rationale }) => {
|
|
1660
|
+
try {
|
|
1661
|
+
const r = await post("/v2/drafts/email", { focus, subject, body, to, rationale });
|
|
1662
|
+
if (r.status === "ambiguous") {
|
|
1663
|
+
const opts = (r.candidates ?? []).map(c => ` • ${c.name ?? "(unnamed)"} [${c.entity_id}]`).join("\n");
|
|
1664
|
+
return { content: [{ type: "text", text: `"${focus}" matches several people. Re-call draft_email with one of these entity ids as focus:\n${opts}` }] };
|
|
1665
|
+
}
|
|
1666
|
+
return { content: [{ type: "text", text:
|
|
1667
|
+
`Email draft ready for ${r.recipient} <${r.to}> — it's in front of the operator to review and send from their own Gmail. You have not sent it.` }] };
|
|
1668
|
+
} catch (e) {
|
|
1669
|
+
const msg = /no_email/.test(e.message)
|
|
1670
|
+
? "No email on file for that person. Ask the operator for the address (pass it as `to`), or draft a LinkedIn message instead."
|
|
1671
|
+
: /entity_not_found/.test(e.message)
|
|
1672
|
+
? "Couldn't find that recipient on the record — check the focus identifier."
|
|
1673
|
+
: `Couldn't create the draft: ${e.message}`;
|
|
1674
|
+
return { content: [{ type: "text", text: msg }] };
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
);
|
|
1678
|
+
|
|
1641
1679
|
// ===========================================================================
|
|
1642
1680
|
// TOOLS: the CORRECTION layer — unsay something recorded by mistake. `record` and
|
|
1643
1681
|
// `save_note` are how you write; these are how you take it back. Both heal the
|
|
@@ -1694,6 +1732,65 @@ export function createServer() {
|
|
|
1694
1732
|
}
|
|
1695
1733
|
);
|
|
1696
1734
|
|
|
1735
|
+
// ===========================================================================
|
|
1736
|
+
// TOOLS: list + introspect — enumerate what's on the workspace, and who you are.
|
|
1737
|
+
// GET /v2/notes · /v2/workspace/integrations · /v2/workspace/members.
|
|
1738
|
+
// ===========================================================================
|
|
1739
|
+
server.tool(
|
|
1740
|
+
"list_notes",
|
|
1741
|
+
"List saved notes newest-first (chronological), optionally scoped to one person/company with " +
|
|
1742
|
+
"`focus`. This is the LIST companion to search_notes (which is semantic): use it for \"show me the " +
|
|
1743
|
+
"notes on X\", or to enumerate notes and get their ids — e.g. to pick one to delete_note. Returns " +
|
|
1744
|
+
"id, type, title, date and a snippet for each.",
|
|
1745
|
+
{
|
|
1746
|
+
focus: z.string().optional().describe("Restrict to one person/company (email, LinkedIn URL, entity UUID, or domain). Omit for the whole workspace."),
|
|
1747
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max notes to return (default 20)."),
|
|
1748
|
+
},
|
|
1749
|
+
async ({ focus, limit }) => {
|
|
1750
|
+
const r = await get("/v2/notes", { ...(focus ? { focus } : {}), ...(limit ? { limit } : {}) });
|
|
1751
|
+
if (r.status === "ambiguous") {
|
|
1752
|
+
const opts = (r.candidates ?? []).map(c => ` • ${c.name ?? "(unnamed)"} [${c.entity_id}]`).join("\n");
|
|
1753
|
+
return { content: [{ type: "text", text: `"${focus}" matches several — re-call list_notes with one of these entity ids as focus:\n${opts}` }] };
|
|
1754
|
+
}
|
|
1755
|
+
if (!r.notes?.length) return { content: [{ type: "text", text: focus ? "No notes on that account." : "No notes saved yet." }] };
|
|
1756
|
+
const lines = r.notes.map(n => ` • ${n.title || n.type}${n.date ? ` (${n.date})` : ""} [${n.id}]\n ${n.snippet}`).join("\n");
|
|
1757
|
+
return { content: [{ type: "text", text: `${r.notes.length} note(s)${r.has_more ? " (more available — raise limit)" : ""}:\n${lines}` }] };
|
|
1758
|
+
}
|
|
1759
|
+
);
|
|
1760
|
+
|
|
1761
|
+
server.tool(
|
|
1762
|
+
"list_integrations",
|
|
1763
|
+
"List the integrations connected to this workspace (Gmail, HubSpot, Apollo, Instantly, LinkedIn, …) " +
|
|
1764
|
+
"— what's wired in and whether it's verified. Use before telling the user to connect something, or " +
|
|
1765
|
+
"to answer \"what's connected here?\".",
|
|
1766
|
+
{},
|
|
1767
|
+
async () => {
|
|
1768
|
+
const r = await get("/v2/workspace/integrations");
|
|
1769
|
+
if (!r.integrations?.length) return { content: [{ type: "text", text: "No integrations connected yet." }] };
|
|
1770
|
+
const lines = r.integrations.map(i => ` • ${i.display_name}${i.category ? ` (${i.category})` : ""}${i.verified ? "" : " — not verified"}`).join("\n");
|
|
1771
|
+
return { content: [{ type: "text", text: `Connected integrations:\n${lines}` }] };
|
|
1772
|
+
}
|
|
1773
|
+
);
|
|
1774
|
+
|
|
1775
|
+
server.tool(
|
|
1776
|
+
"whoami",
|
|
1777
|
+
"Report who this API key acts AS and who else is on the workspace — the agent's own identity in " +
|
|
1778
|
+
"Nous. Returns your scope (a MEMBER key sees only that member's private content plus the shared " +
|
|
1779
|
+
"graph; an ADMIN key sees all raw content) and the team roster with names and roles. Use it to " +
|
|
1780
|
+
"understand whose view you have and to reference teammates. Emails show only for an admin key.",
|
|
1781
|
+
{},
|
|
1782
|
+
async () => {
|
|
1783
|
+
const r = await get("/v2/workspace/members");
|
|
1784
|
+
const scope = r.you?.scope === "admin"
|
|
1785
|
+
? "an ADMIN key — you see all raw content on this workspace"
|
|
1786
|
+
: "a MEMBER key — you see only your own private content plus the shared graph";
|
|
1787
|
+
const roster = (r.members || [])
|
|
1788
|
+
.map(m => ` • ${m.name || "(unnamed)"} — ${m.role}${m.you ? " (you)" : ""}${m.email ? ` · ${m.email}` : ""}`)
|
|
1789
|
+
.join("\n");
|
|
1790
|
+
return { content: [{ type: "text", text: `You are ${scope}.\n\nWorkspace members (${r.count ?? 0}):\n${roster || " (none)"}` }] };
|
|
1791
|
+
}
|
|
1792
|
+
);
|
|
1793
|
+
|
|
1697
1794
|
// ===========================================================================
|
|
1698
1795
|
// TOOL: get_routing_preferences
|
|
1699
1796
|
// The routing preferences that make THIS agent default to Nous for GTM. The
|