@opennous/mcp 0.19.0 → 0.21.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 +108 -1
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -28,13 +28,15 @@
|
|
|
28
28
|
* configure_crm_sync — set CRM sync rules (auto-sync, create policy, hygiene cadence)
|
|
29
29
|
* set_trigger — create an outbound event trigger (webhook); list_triggers reads them
|
|
30
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
|
|
32
|
+
* check_leads — pre-spend coverage check: which candidates you already own / should re-enrich
|
|
31
33
|
*/
|
|
32
34
|
|
|
33
35
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
34
36
|
import { z } from "zod";
|
|
35
37
|
import { get, post } from "./client.js";
|
|
36
38
|
|
|
37
|
-
export const SERVER_VERSION = "0.
|
|
39
|
+
export const SERVER_VERSION = "0.21.0";
|
|
38
40
|
|
|
39
41
|
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
40
42
|
|
|
@@ -760,5 +762,110 @@ export function createServer() {
|
|
|
760
762
|
}
|
|
761
763
|
);
|
|
762
764
|
|
|
765
|
+
// ===========================================================================
|
|
766
|
+
// TOOL: lead_list_operations — GET /api/lead-lists[/:id/operations]
|
|
767
|
+
// The operations trail for a lead list: imports, enrichment runs, pushes to
|
|
768
|
+
// campaigns, and replies — filterable by category and time window. This is how
|
|
769
|
+
// you answer "what happened on this list?" and attribute campaign performance
|
|
770
|
+
// back to where the leads came from (the list's source). Call with no
|
|
771
|
+
// lead_list_id to discover the lists and their ids first.
|
|
772
|
+
// ===========================================================================
|
|
773
|
+
server.tool(
|
|
774
|
+
"lead_list_operations",
|
|
775
|
+
"Inspect the operations trail of a lead list — imports, enrichment runs, pushes to campaigns, " +
|
|
776
|
+
"and classified replies — to report on what happened and attribute outcomes to a list's source. " +
|
|
777
|
+
"Call with NO lead_list_id to list the workspace's lead lists (id, name, count, source), then " +
|
|
778
|
+
"call again with an id. Filter with `event` (import | enrich | export | reply) and `days`. " +
|
|
779
|
+
"Each operation is a run-level summary (one row per import/enrich/push), not per-lead noise.",
|
|
780
|
+
{
|
|
781
|
+
lead_list_id: z.string().optional().describe("The lead list's UUID. Omit to list the available lead lists first."),
|
|
782
|
+
event: z.enum(["import", "enrich", "export", "reply"]).optional().describe("Filter to one category of operation."),
|
|
783
|
+
days: z.number().optional().describe("Look back this many days (default 30). Pass a large number for all-time."),
|
|
784
|
+
limit: z.number().optional().describe("Max operations to return (default 100, cap 200)."),
|
|
785
|
+
},
|
|
786
|
+
async ({ lead_list_id, event, days, limit }) => {
|
|
787
|
+
// Discovery mode — no list id yet. Return the lists so the agent can pick.
|
|
788
|
+
if (!lead_list_id) {
|
|
789
|
+
const r = await get("/api/lead-lists");
|
|
790
|
+
const lists = r.lead_lists || [];
|
|
791
|
+
const lines = lists.length
|
|
792
|
+
? [`LEAD LISTS (${lists.length}):`,
|
|
793
|
+
...lists.map(l => ` ${l.id} ${l.name} · ${l.lead_count ?? 0} leads · source: ${l.source || "—"}`),
|
|
794
|
+
"", "Call lead_list_operations again with one of these ids (and an optional event filter)."]
|
|
795
|
+
: ["No lead lists yet."];
|
|
796
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const r = await get(`/api/lead-lists/${encodeURIComponent(lead_list_id)}/operations`, { event, days, limit });
|
|
800
|
+
const ops = r.operations || [];
|
|
801
|
+
const lines = [];
|
|
802
|
+
const summary = Object.entries(r.by_category || {}).map(([k, v]) => `${k} ${v}`).join(" · ");
|
|
803
|
+
lines.push(`OPERATIONS${event ? ` · ${event}` : ""} (${ops.length})${summary ? ` — ${summary}` : ""}`);
|
|
804
|
+
if (!ops.length) {
|
|
805
|
+
lines.push("", "No operations in this window.");
|
|
806
|
+
} else {
|
|
807
|
+
for (const o of ops) {
|
|
808
|
+
const cat = o.metadata?.category || o.event_type;
|
|
809
|
+
lines.push(` ${relAge(o.occurred_at).padEnd(8)} ${String(cat).padEnd(8)} ${o.summary}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return { content: [{ type: "text", text: lines.join("\n").trim() }] };
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
// ===========================================================================
|
|
817
|
+
// TOOL: check_leads — POST /v2/dedup
|
|
818
|
+
// The pre-spend coverage check. Run this BEFORE building/buying a lead list in
|
|
819
|
+
// Apollo, Sales Navigator, DeepSearch, Clay, etc. Paste the candidate emails /
|
|
820
|
+
// LinkedIn URLs / company domains (all visible for free in the tool's preview)
|
|
821
|
+
// and find out what you already own — so you never re-buy a record you have,
|
|
822
|
+
// and you re-enrich stale ones instead of paying to acquire them again.
|
|
823
|
+
// ===========================================================================
|
|
824
|
+
server.tool(
|
|
825
|
+
"check_leads",
|
|
826
|
+
"Pre-spend coverage check — call this BEFORE building or buying a lead list elsewhere (Apollo, " +
|
|
827
|
+
"Sales Navigator, DeepSearch, Clay). Paste the candidate emails, LinkedIn URLs, or company " +
|
|
828
|
+
"domains (free in any tool's preview) and learn what you already have, so you don't pay twice. " +
|
|
829
|
+
"Returns counts: net_new (acquire + enrich these), needs_enrichment (you already OWN these but " +
|
|
830
|
+
"they're stale / not enriched in 90 days — re-enrich instead of re-buying), reusable (you have a " +
|
|
831
|
+
"fresh verified email — reuse, spend nothing), plus engaged/recent/bounced/unsubscribed/known to " +
|
|
832
|
+
"skip. Each result carries entity_id, email_status, enriched_at, and stale so you can act per-lead.",
|
|
833
|
+
{
|
|
834
|
+
emails: z.array(z.string()).optional().describe("Candidate email addresses (up to 50,000)."),
|
|
835
|
+
linkedin_urls: z.array(z.string()).optional().describe("Candidate LinkedIn profile URLs (up to 50,000)."),
|
|
836
|
+
domains: z.array(z.string()).optional().describe("Company domains — 'do I already have anyone here?' (up to 50,000)."),
|
|
837
|
+
},
|
|
838
|
+
async ({ emails, linkedin_urls, domains }) => {
|
|
839
|
+
const body = {};
|
|
840
|
+
if (emails?.length) body.emails = emails;
|
|
841
|
+
if (linkedin_urls?.length) body.linkedin_urls = linkedin_urls;
|
|
842
|
+
if (domains?.length) body.domains = domains;
|
|
843
|
+
if (!Object.keys(body).length) {
|
|
844
|
+
return { content: [{ type: "text", text: "Pass at least one of: emails, linkedin_urls, domains." }] };
|
|
845
|
+
}
|
|
846
|
+
const r = await post("/v2/dedup", body);
|
|
847
|
+
const s = r.summary || {};
|
|
848
|
+
const lines = [
|
|
849
|
+
`COVERAGE (${s.total ?? 0} checked)`,
|
|
850
|
+
` net_new ${s.net_new ?? 0} → acquire + enrich`,
|
|
851
|
+
` needs_enrichment ${s.needs_enrichment ?? 0} → you OWN these but stale (>90d) → re-enrich, don't re-buy`,
|
|
852
|
+
` reusable ${s.reusable ?? 0} → fresh verified email on file → reuse, spend nothing`,
|
|
853
|
+
` engaged ${s.engaged ?? 0} → in an active conversation, don't cold-send`,
|
|
854
|
+
` recent ${s.recent ?? 0} → contacted <30d, defer`,
|
|
855
|
+
` known ${s.known ?? 0} → company already in the workspace`,
|
|
856
|
+
` bounced/unsub ${(s.bounced ?? 0) + (s.unsubscribed ?? 0) + (s.suppressed ?? 0)} → skip`,
|
|
857
|
+
];
|
|
858
|
+
// Surface a few stale entities the caller should re-enrich (with their last date).
|
|
859
|
+
const stale = (r.results || []).filter(x => x.entity_id && x.stale).slice(0, 15);
|
|
860
|
+
if (stale.length) {
|
|
861
|
+
lines.push("", "RE-ENRICH (sample):");
|
|
862
|
+
for (const x of stale) {
|
|
863
|
+
lines.push(` ${x.value} [${x.enriched_at ? `last enriched ${relAge(x.enriched_at)}` : "never enriched"}] ${x.entity_id}`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
867
|
+
}
|
|
868
|
+
);
|
|
869
|
+
|
|
763
870
|
return server;
|
|
764
871
|
}
|