@agifyai/leadify-mcp 3.3.0 → 3.4.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/dist/server.js +1 -1
- package/dist/tools/lead_groups.js +20 -5
- package/dist/tools/personas.js +107 -17
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -16,7 +16,7 @@ import { registerPipelineTools } from "./tools/pipeline.js";
|
|
|
16
16
|
export function createServer() {
|
|
17
17
|
const server = new McpServer({
|
|
18
18
|
name: "leadify",
|
|
19
|
-
version: "3.
|
|
19
|
+
version: "3.4.0",
|
|
20
20
|
});
|
|
21
21
|
registerAuthTools(server);
|
|
22
22
|
registerOrganizationTools(server);
|
|
@@ -160,24 +160,39 @@ export function registerLeadGroupTools(server) {
|
|
|
160
160
|
"Returns null persona if none is assigned. Use this to check what targeting " +
|
|
161
161
|
"rules apply to a group before generating messages. " +
|
|
162
162
|
"By default, `persona` is SLIM ({id, name, organizationId}) — about ~150 chars " +
|
|
163
|
-
"instead of
|
|
164
|
-
"Pass `view=
|
|
165
|
-
"
|
|
163
|
+
"instead of 63k — just enough to resolve the org / link the persona to a lead group. " +
|
|
164
|
+
"Pass `view=outreach` to get the outreach-specific projection (only when you actually " +
|
|
165
|
+
"need the outreach trio — prefer the dedicated get_outreach_templates tool for that). " +
|
|
166
|
+
"SILO: outreach data is its own concern. It does not leak into the default persona payload.", {
|
|
166
167
|
lead_group_id: z.string().describe("Lead group ID."),
|
|
167
168
|
view: z
|
|
168
|
-
.enum(["slim", "outreach"
|
|
169
|
+
.enum(["slim", "outreach"])
|
|
169
170
|
.optional()
|
|
170
171
|
.default("slim")
|
|
171
172
|
.describe("Persona projection. " +
|
|
172
173
|
"'slim' (default) = {id, name, organizationId} — fast, context-friendly. " +
|
|
173
174
|
"'outreach' = projection used by the outreach agent (messaging + targeting). " +
|
|
174
|
-
"'full'
|
|
175
|
+
"The legacy 'full' view is gone (SILO enforcement) — use get_persona + " +
|
|
176
|
+
"get_outreach_templates for a complete picture, scoped per concern."),
|
|
175
177
|
}, async ({ lead_group_id, view }) => {
|
|
176
178
|
try {
|
|
177
179
|
const params = new URLSearchParams();
|
|
178
180
|
if (view)
|
|
179
181
|
params.set("view", view);
|
|
180
182
|
const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/persona`, params);
|
|
183
|
+
// SILO defense in depth: even if the API returns the full payload for
|
|
184
|
+
// `view=slim` (the view param is best-effort on the backend), strip the
|
|
185
|
+
// outreach trio locally so it can never leak through this tool.
|
|
186
|
+
// Outreach data belongs to the dedicated outreach tools.
|
|
187
|
+
if (view !== "outreach" && data && typeof data === "object") {
|
|
188
|
+
const wrapper = data;
|
|
189
|
+
const persona = wrapper.persona ?? null;
|
|
190
|
+
if (persona && typeof persona === "object") {
|
|
191
|
+
delete persona.outreachTemplates;
|
|
192
|
+
delete persona.outreachSignalRouting;
|
|
193
|
+
delete persona.outreachFields;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
181
196
|
return toolResult(data);
|
|
182
197
|
}
|
|
183
198
|
catch (error) {
|
package/dist/tools/personas.js
CHANGED
|
@@ -445,6 +445,31 @@ export function registerPersonaTools(server) {
|
|
|
445
445
|
.describe("Override SDR identity at persona level ({name, title})."),
|
|
446
446
|
}, async (params) => {
|
|
447
447
|
try {
|
|
448
|
+
// SILO guard: the upsert escape hatch must NOT be used for outreach
|
|
449
|
+
// data. Outreach is its own concern with its own granular tools
|
|
450
|
+
// (update_persona_outreach_template, update_persona_outreach_signal_routing).
|
|
451
|
+
// Reject early with a clear pointer so the caller knows what to use.
|
|
452
|
+
if (params.outreach_signal_routing !== undefined ||
|
|
453
|
+
params.outreach_templates !== undefined ||
|
|
454
|
+
params.outreach_fields !== undefined) {
|
|
455
|
+
return toolError("SILO: upsert_persona does NOT accept outreach fields. " +
|
|
456
|
+
"Use the dedicated outreach tools instead: " +
|
|
457
|
+
"update_persona_outreach_template (add/replace/remove a template), " +
|
|
458
|
+
"update_persona_outreach_signal_routing (patch the signal routing block). " +
|
|
459
|
+
"For a read of the trio, use get_outreach_templates({ lead_group_id }).", {
|
|
460
|
+
rejectedParams: [
|
|
461
|
+
...(params.outreach_signal_routing !== undefined
|
|
462
|
+
? ["outreach_signal_routing"]
|
|
463
|
+
: []),
|
|
464
|
+
...(params.outreach_templates !== undefined
|
|
465
|
+
? ["outreach_templates"]
|
|
466
|
+
: []),
|
|
467
|
+
...(params.outreach_fields !== undefined
|
|
468
|
+
? ["outreach_fields"]
|
|
469
|
+
: []),
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
}
|
|
448
473
|
const body = { name: params.name };
|
|
449
474
|
if (params.id !== undefined)
|
|
450
475
|
body.id = params.id;
|
|
@@ -503,21 +528,12 @@ export function registerPersonaTools(server) {
|
|
|
503
528
|
rules: (params.signal_tier_rules.rules ?? []).map(mapSignalTierRule),
|
|
504
529
|
};
|
|
505
530
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
osr.defaultSignal = params.outreach_signal_routing.default_signal;
|
|
513
|
-
if (params.outreach_signal_routing.signals !== undefined)
|
|
514
|
-
osr.signals = params.outreach_signal_routing.signals;
|
|
515
|
-
body.outreachSignalRouting = osr;
|
|
516
|
-
}
|
|
517
|
-
if (params.outreach_templates !== undefined)
|
|
518
|
-
body.outreachTemplates = params.outreach_templates.map(mapOutreachTemplate);
|
|
519
|
-
if (params.outreach_fields !== undefined)
|
|
520
|
-
body.outreachFields = params.outreach_fields;
|
|
531
|
+
// SILO: outreach_* params are accepted on the schema (so users discover
|
|
532
|
+
// they exist here) but ALWAYS rejected by the guard at the top of this
|
|
533
|
+
// handler. Outreach data flows through update_persona_outreach_template
|
|
534
|
+
// and update_persona_outreach_signal_routing only. The unreachable
|
|
535
|
+
// mapping code below was removed — TypeScript narrows the params to
|
|
536
|
+
// `never` after the guard, so the old branches would never run anyway.
|
|
521
537
|
if (params.tone_instructions !== undefined)
|
|
522
538
|
body.toneInstructions = params.tone_instructions;
|
|
523
539
|
if (params.context !== undefined)
|
|
@@ -535,17 +551,91 @@ export function registerPersonaTools(server) {
|
|
|
535
551
|
}
|
|
536
552
|
});
|
|
537
553
|
// ── get_persona ────────────────────────────────────────────────────────
|
|
538
|
-
server.tool("get_persona", "Retrieve a single persona by ID, including all lead groups it's assigned to."
|
|
554
|
+
server.tool("get_persona", "Retrieve a single persona by ID, including all lead groups it's assigned to. " +
|
|
555
|
+
"SILO: the outreach trio (outreachTemplates, outreachSignalRouting, outreachFields) " +
|
|
556
|
+
"is STRIPPED by default to keep the response focused on the persona's targeting & " +
|
|
557
|
+
"qualification concerns (~37k chars instead of ~63k). Pass include_outreach=true to " +
|
|
558
|
+
"get the full payload — but for outreach-only reads prefer get_outreach_templates " +
|
|
559
|
+
"(a much smaller, dedicated read).", {
|
|
539
560
|
id: z.string().describe("Persona ID."),
|
|
540
|
-
|
|
561
|
+
include_outreach: z
|
|
562
|
+
.boolean()
|
|
563
|
+
.optional()
|
|
564
|
+
.default(false)
|
|
565
|
+
.describe("If true, keep the outreach trio in the response (outreachTemplates, " +
|
|
566
|
+
"outreachSignalRouting, outreachFields). Default false (SILO: outreach lives " +
|
|
567
|
+
"in its own tool)."),
|
|
568
|
+
}, async ({ id, include_outreach }) => {
|
|
541
569
|
try {
|
|
542
570
|
const data = await getClient().get(`/api/persona/${encodeURIComponent(id)}`);
|
|
571
|
+
// SILO defense in depth: strip the outreach trio locally unless the
|
|
572
|
+
// caller explicitly opted in. Outreach data belongs to the dedicated
|
|
573
|
+
// outreach tools — never mix it with the persona read by default.
|
|
574
|
+
if (!include_outreach && data && typeof data === "object") {
|
|
575
|
+
const wrapper = data;
|
|
576
|
+
if (wrapper && typeof wrapper === "object") {
|
|
577
|
+
delete wrapper.outreachTemplates;
|
|
578
|
+
delete wrapper.outreachSignalRouting;
|
|
579
|
+
delete wrapper.outreachFields;
|
|
580
|
+
}
|
|
581
|
+
// The persona can also be wrapped under {persona: {...}} depending
|
|
582
|
+
// on the API path. Strip there too.
|
|
583
|
+
const inner = wrapper.persona;
|
|
584
|
+
if (inner && typeof inner === "object") {
|
|
585
|
+
delete inner.outreachTemplates;
|
|
586
|
+
delete inner.outreachSignalRouting;
|
|
587
|
+
delete inner.outreachFields;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
543
590
|
return toolResult(data);
|
|
544
591
|
}
|
|
545
592
|
catch (error) {
|
|
546
593
|
return handlePersonaToolError(error);
|
|
547
594
|
}
|
|
548
595
|
});
|
|
596
|
+
// ── get_outreach_templates ─────────────────────────────────────────────
|
|
597
|
+
server.tool("get_outreach_templates", "Read the persona's outreach trio ONLY (outreachSignalRouting, outreachTemplates, " +
|
|
598
|
+
"outreachFields). SILO: this is the dedicated read for outreach data — never call " +
|
|
599
|
+
"get_persona with include_outreach=true for routine reads. Returns a small payload " +
|
|
600
|
+
"regardless of persona size, so it's safe to call mid-session without saturating " +
|
|
601
|
+
"the LLM context. " +
|
|
602
|
+
"Resolves the persona from the lead group (lead_group_id → personaId → persona), " +
|
|
603
|
+
"then projects the trio. If the lead group has no persona, returns an error.", {
|
|
604
|
+
lead_group_id: z
|
|
605
|
+
.string()
|
|
606
|
+
.describe("Lead group ID. The persona is resolved server-side from the group's personaId."),
|
|
607
|
+
}, async ({ lead_group_id }) => {
|
|
608
|
+
try {
|
|
609
|
+
// Step 1: resolve the persona assigned to this lead group (slim view).
|
|
610
|
+
// We only need the persona's id from here — we re-fetch the full
|
|
611
|
+
// persona below to grab the outreach trio.
|
|
612
|
+
const groupPersona = (await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/persona`));
|
|
613
|
+
const personaId = groupPersona?.persona?.id;
|
|
614
|
+
if (!personaId) {
|
|
615
|
+
return toolError(`No persona is assigned to lead group "${lead_group_id}". ` +
|
|
616
|
+
`Assign one via update_lead_group({ persona_id }) before reading outreach templates.`);
|
|
617
|
+
}
|
|
618
|
+
// Step 2: fetch the full persona so we can project the trio.
|
|
619
|
+
const full = (await getClient().get(`/api/persona/${encodeURIComponent(personaId)}`));
|
|
620
|
+
// The API may return either a bare persona or {persona: {...}}.
|
|
621
|
+
const persona = full.persona ?? full;
|
|
622
|
+
const trio = {
|
|
623
|
+
outreachSignalRouting: persona.outreachSignalRouting ?? null,
|
|
624
|
+
outreachTemplates: persona.outreachTemplates ?? [],
|
|
625
|
+
outreachFields: persona.outreachFields ?? [],
|
|
626
|
+
};
|
|
627
|
+
return toolResult({
|
|
628
|
+
personaId,
|
|
629
|
+
...trio,
|
|
630
|
+
_note: "SILO projection. To edit, use update_persona_outreach_template or " +
|
|
631
|
+
"update_persona_outreach_signal_routing. Do NOT use upsert_persona for outreach " +
|
|
632
|
+
"fields (SILO rejection).",
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
return handlePersonaToolError(error);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
549
639
|
// ── list_personas ──────────────────────────────────────────────────────
|
|
550
640
|
server.tool("list_personas", "List all personas accessible to the caller, optionally scoped to a specific organization " +
|
|
551
641
|
"and/or including global personas. Use this to discover persona IDs before calling " +
|