@opennous/mcp 0.18.0 → 0.20.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 +183 -1
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -24,13 +24,18 @@
|
|
|
24
24
|
* get_workspace_status — what's set up in this workspace + a ranked next_steps list (call first)
|
|
25
25
|
* set_workspace_profile— agent-driven onboarding: set the workspace's name, site, type, ICP
|
|
26
26
|
* build_scoring_model — build/rebuild the ICP scoring model from the recorded GTM context
|
|
27
|
+
* connect_integration — connect a key-based integration (Apollo, Prospeo, HubSpot, …)
|
|
28
|
+
* configure_crm_sync — set CRM sync rules (auto-sync, create policy, hygiene cadence)
|
|
29
|
+
* set_trigger — create an outbound event trigger (webhook); list_triggers reads them
|
|
30
|
+
* list_triggers — list the workspace's event triggers + available events
|
|
31
|
+
* lead_list_operations — the operations trail of a lead list (imports/enrich/push/replies), filterable
|
|
27
32
|
*/
|
|
28
33
|
|
|
29
34
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
30
35
|
import { z } from "zod";
|
|
31
36
|
import { get, post } from "./client.js";
|
|
32
37
|
|
|
33
|
-
export const SERVER_VERSION = "0.
|
|
38
|
+
export const SERVER_VERSION = "0.20.0";
|
|
34
39
|
|
|
35
40
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
36
41
|
|
|
@@ -630,5 +635,182 @@ export function createServer() {
|
|
|
630
635
|
}
|
|
631
636
|
);
|
|
632
637
|
|
|
638
|
+
// ===========================================================================
|
|
639
|
+
// TOOL: connect_integration — POST /v2/workspace/integrations
|
|
640
|
+
// The agent connects a KEY-BASED integration for the user (no clicking through
|
|
641
|
+
// the Integrations page). OAuth providers still need a browser, so this is
|
|
642
|
+
// limited to providers that authenticate with an API key/token.
|
|
643
|
+
// ===========================================================================
|
|
644
|
+
server.tool(
|
|
645
|
+
"connect_integration",
|
|
646
|
+
"Connect a key-based integration for the user — an enrichment, CRM, or sequencer provider that " +
|
|
647
|
+
"authenticates with an API key or token (e.g. Apollo, Prospeo, Instantly, HubSpot private-app " +
|
|
648
|
+
"token, Pipedrive, Attio, Smartlead, HeyReach). Ask the user for the provider's API key, then " +
|
|
649
|
+
"call this; it verifies the credentials before saving. Providers that use a browser sign-in " +
|
|
650
|
+
"(OAuth, e.g. Gmail) can't be connected this way — for those, point the user to the Integrations " +
|
|
651
|
+
"page. After connecting an enrichment provider, the account record starts filling in.",
|
|
652
|
+
{
|
|
653
|
+
provider: z.string().describe("Provider name, lowercase — e.g. 'apollo', 'prospeo', 'instantly', 'hubspot', 'pipedrive', 'attio'."),
|
|
654
|
+
credentials: z.record(z.string()).describe("The provider's credentials as key/value, e.g. { api_key: '...' } or { access_token: '...' }."),
|
|
655
|
+
name: z.string().optional().describe("Optional label for the connection."),
|
|
656
|
+
},
|
|
657
|
+
async ({ provider, credentials, name }) => {
|
|
658
|
+
try {
|
|
659
|
+
const r = await post("/v2/workspace/integrations", { provider, credentials, name });
|
|
660
|
+
return { content: [{ type: "text", text: `Connected ${r.connection?.provider ?? provider}.${r.message ? ` ${r.message}` : ""}` }] };
|
|
661
|
+
} catch (e) {
|
|
662
|
+
const msg = String(e?.message ?? e);
|
|
663
|
+
if (msg.includes("oauth_provider")) {
|
|
664
|
+
return { content: [{ type: "text", text: `${provider} uses a browser sign-in, so it can't be connected with a key. Tell the user to connect it on the Integrations page.` }] };
|
|
665
|
+
}
|
|
666
|
+
if (msg.includes("invalid_credentials")) {
|
|
667
|
+
return { content: [{ type: "text", text: `Those credentials didn't verify for ${provider}. Ask the user to double-check the key and try again.` }] };
|
|
668
|
+
}
|
|
669
|
+
if (msg.includes("unknown_provider")) {
|
|
670
|
+
return { content: [{ type: "text", text: `No provider named "${provider}". Ask the user which tool they mean.` }] };
|
|
671
|
+
}
|
|
672
|
+
throw e;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
// ===========================================================================
|
|
678
|
+
// TOOL: configure_crm_sync — POST /v2/workspace/crm-sync
|
|
679
|
+
// The agent sets the CRM sync rules — the same options as the CRM Sync page.
|
|
680
|
+
// The CRM must already be connected (OAuth connect stays a human step).
|
|
681
|
+
// ===========================================================================
|
|
682
|
+
server.tool(
|
|
683
|
+
"configure_crm_sync",
|
|
684
|
+
"Configure how Nous keeps a connected CRM in sync — the same settings as the CRM Sync page. The " +
|
|
685
|
+
"CRM must already be connected (HubSpot/Pipedrive/Attio). Set any of: auto-sync (daily pull), " +
|
|
686
|
+
"push of touchpoints, the create policy (when a new record is auto-created and the ICP-fit " +
|
|
687
|
+
"threshold), and the hygiene cadence. Only send the fields you want to change. If it reports the " +
|
|
688
|
+
"CRM isn't connected, tell the user to connect it on the Integrations page first.",
|
|
689
|
+
{
|
|
690
|
+
provider: z.enum(["hubspot", "pipedrive", "attio"]).describe("Which connected CRM to configure."),
|
|
691
|
+
autoSync: z.boolean().optional().describe("Pull contacts/companies/deals daily."),
|
|
692
|
+
pushActivities: z.boolean().optional().describe("Push touchpoints (meetings, replies, proposals) back to the CRM."),
|
|
693
|
+
createInCrm: z.boolean().optional().describe("Auto-create new records in the CRM when they earn it."),
|
|
694
|
+
createTrigger: z.enum(["any_reply_or_meeting", "positive_reply_or_meeting", "meeting_only", "interested_stage"]).optional()
|
|
695
|
+
.describe("What earns a new record."),
|
|
696
|
+
createRequireIcpFit: z.boolean().optional().describe("Require an ICP-fit score before creating a record."),
|
|
697
|
+
createIcpThreshold: z.number().optional().describe("Minimum ICP-fit score to create (0-100)."),
|
|
698
|
+
hygieneEnabled: z.boolean().optional().describe("Run scheduled hygiene reconciliation."),
|
|
699
|
+
hygieneCadence: z.enum(["weekly", "monthly"]).optional().describe("How often hygiene runs."),
|
|
700
|
+
},
|
|
701
|
+
async (args) => {
|
|
702
|
+
try {
|
|
703
|
+
const r = await post("/v2/workspace/crm-sync", args);
|
|
704
|
+
const c = r.config ?? {};
|
|
705
|
+
return { content: [{ type: "text", text:
|
|
706
|
+
`CRM sync configured for ${args.provider}. auto-sync ${c.auto_sync ? "on" : "off"}, ` +
|
|
707
|
+
`create ${c.create_in_crm ? `on (${c.create_trigger}${c.create_require_icp_fit ? `, ICP ≥ ${c.create_icp_threshold}` : ""})` : "off"}, ` +
|
|
708
|
+
`hygiene ${c.hygiene_enabled ? c.hygiene_cadence : "off"}.` }] };
|
|
709
|
+
} catch (e) {
|
|
710
|
+
const msg = String(e?.message ?? e);
|
|
711
|
+
if (msg.includes("crm_not_connected")) {
|
|
712
|
+
return { content: [{ type: "text", text: `${args.provider} isn't connected yet. Tell the user to connect it on the Integrations page, then configure sync.` }] };
|
|
713
|
+
}
|
|
714
|
+
throw e;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
// ===========================================================================
|
|
720
|
+
// TOOL: set_trigger / list_triggers — /v2/workspace/triggers
|
|
721
|
+
// Outbound event triggers (webhooks) — wire the user's stack to fire when the
|
|
722
|
+
// record changes.
|
|
723
|
+
// ===========================================================================
|
|
724
|
+
server.tool(
|
|
725
|
+
"set_trigger",
|
|
726
|
+
"Create an outbound event trigger (a webhook) so an external tool is notified when something " +
|
|
727
|
+
"happens in the workspace — e.g. a new contact, a reply, a meeting booked. Pass the destination " +
|
|
728
|
+
"URL and which events to fire on. Call list_triggers first to see the available event names.",
|
|
729
|
+
{
|
|
730
|
+
url: z.string().describe("The destination URL the event is POSTed to."),
|
|
731
|
+
events: z.array(z.string()).describe("Event names to fire on (see list_triggers for the catalog)."),
|
|
732
|
+
name: z.string().optional().describe("Optional label for the trigger."),
|
|
733
|
+
},
|
|
734
|
+
async ({ url, events, name }) => {
|
|
735
|
+
try {
|
|
736
|
+
const r = await post("/v2/workspace/triggers", { url, events, name });
|
|
737
|
+
return { content: [{ type: "text", text: `Trigger created for ${events.join(", ")} → ${url}.` }] };
|
|
738
|
+
} catch (e) {
|
|
739
|
+
const msg = String(e?.message ?? e);
|
|
740
|
+
return { content: [{ type: "text", text: `Couldn't create the trigger: ${msg}. Call list_triggers to see valid event names.` }] };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
server.tool(
|
|
745
|
+
"list_triggers",
|
|
746
|
+
"List the workspace's outbound event triggers (webhooks) and the catalog of available event names.",
|
|
747
|
+
{},
|
|
748
|
+
async () => {
|
|
749
|
+
const r = await get("/v2/workspace/triggers");
|
|
750
|
+
const lines = [];
|
|
751
|
+
if (r.triggers?.length) {
|
|
752
|
+
lines.push(`TRIGGERS (${r.triggers.length}):`);
|
|
753
|
+
for (const t of r.triggers) lines.push(` ${t.name || "(unnamed)"} → ${t.url} [${(t.events || []).join(", ")}]`);
|
|
754
|
+
} else {
|
|
755
|
+
lines.push("No triggers set up yet.");
|
|
756
|
+
}
|
|
757
|
+
if (r.available_events?.length) {
|
|
758
|
+
lines.push("", `AVAILABLE EVENTS: ${r.available_events.join(", ")}`);
|
|
759
|
+
}
|
|
760
|
+
return { content: [{ type: "text", text: lines.join("\n").trim() }] };
|
|
761
|
+
}
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
// ===========================================================================
|
|
765
|
+
// TOOL: lead_list_operations — GET /api/lead-lists[/:id/operations]
|
|
766
|
+
// The operations trail for a lead list: imports, enrichment runs, pushes to
|
|
767
|
+
// campaigns, and replies — filterable by category and time window. This is how
|
|
768
|
+
// you answer "what happened on this list?" and attribute campaign performance
|
|
769
|
+
// back to where the leads came from (the list's source). Call with no
|
|
770
|
+
// lead_list_id to discover the lists and their ids first.
|
|
771
|
+
// ===========================================================================
|
|
772
|
+
server.tool(
|
|
773
|
+
"lead_list_operations",
|
|
774
|
+
"Inspect the operations trail of a lead list — imports, enrichment runs, pushes to campaigns, " +
|
|
775
|
+
"and classified replies — to report on what happened and attribute outcomes to a list's source. " +
|
|
776
|
+
"Call with NO lead_list_id to list the workspace's lead lists (id, name, count, source), then " +
|
|
777
|
+
"call again with an id. Filter with `event` (import | enrich | export | reply) and `days`. " +
|
|
778
|
+
"Each operation is a run-level summary (one row per import/enrich/push), not per-lead noise.",
|
|
779
|
+
{
|
|
780
|
+
lead_list_id: z.string().optional().describe("The lead list's UUID. Omit to list the available lead lists first."),
|
|
781
|
+
event: z.enum(["import", "enrich", "export", "reply"]).optional().describe("Filter to one category of operation."),
|
|
782
|
+
days: z.number().optional().describe("Look back this many days (default 30). Pass a large number for all-time."),
|
|
783
|
+
limit: z.number().optional().describe("Max operations to return (default 100, cap 200)."),
|
|
784
|
+
},
|
|
785
|
+
async ({ lead_list_id, event, days, limit }) => {
|
|
786
|
+
// Discovery mode — no list id yet. Return the lists so the agent can pick.
|
|
787
|
+
if (!lead_list_id) {
|
|
788
|
+
const r = await get("/api/lead-lists");
|
|
789
|
+
const lists = r.lead_lists || [];
|
|
790
|
+
const lines = lists.length
|
|
791
|
+
? [`LEAD LISTS (${lists.length}):`,
|
|
792
|
+
...lists.map(l => ` ${l.id} ${l.name} · ${l.lead_count ?? 0} leads · source: ${l.source || "—"}`),
|
|
793
|
+
"", "Call lead_list_operations again with one of these ids (and an optional event filter)."]
|
|
794
|
+
: ["No lead lists yet."];
|
|
795
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const r = await get(`/api/lead-lists/${encodeURIComponent(lead_list_id)}/operations`, { event, days, limit });
|
|
799
|
+
const ops = r.operations || [];
|
|
800
|
+
const lines = [];
|
|
801
|
+
const summary = Object.entries(r.by_category || {}).map(([k, v]) => `${k} ${v}`).join(" · ");
|
|
802
|
+
lines.push(`OPERATIONS${event ? ` · ${event}` : ""} (${ops.length})${summary ? ` — ${summary}` : ""}`);
|
|
803
|
+
if (!ops.length) {
|
|
804
|
+
lines.push("", "No operations in this window.");
|
|
805
|
+
} else {
|
|
806
|
+
for (const o of ops) {
|
|
807
|
+
const cat = o.metadata?.category || o.event_type;
|
|
808
|
+
lines.push(` ${relAge(o.occurred_at).padEnd(8)} ${String(cat).padEnd(8)} ${o.summary}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return { content: [{ type: "text", text: lines.join("\n").trim() }] };
|
|
812
|
+
}
|
|
813
|
+
);
|
|
814
|
+
|
|
633
815
|
return server;
|
|
634
816
|
}
|