@agifyai/leadify-mcp 4.0.0 → 5.0.1

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.
@@ -277,11 +277,15 @@ export function registerDataRoomTools(server) {
277
277
  });
278
278
  // ── get_outreach_context ──────────────────────────────────────────────
279
279
  server.tool("get_outreach_context", "Get a curated outreach context for a lead group: the outreach-relevant slice " +
280
- "of companyInfo (identity, pitch, constraints, curated products, economics " +
281
- "triggers/baseline, product ICP roles, proof wording) PLUS the full persona. " +
280
+ "of companyInfo only (identity, pitch, constraints, curated products, economics " +
281
+ "triggers/baseline, product ICP roles, proof wording). " +
282
282
  "Takes a lead_group_id (NOT organization_id) — the org is resolved server-side, " +
283
283
  "eliminating the cross-org footgun of get_data_room. " +
284
284
  "Returns a HARD error if no persona is linked to the group. " +
285
+ "NOTE: the `outreach` settings block is NOT returned by this tool — " +
286
+ "outreach configuration (positioning, tone, rules, templates, etc.) is " +
287
+ "managed exclusively via the Fine Tuning tools (append_fine_tuning / " +
288
+ "set_fine_tuning / get_fine_tuning). " +
285
289
  "Use this instead of get_data_room + get_lead_group_persona in the outreach " +
286
290
  "pipeline Phase 0.", {
287
291
  lead_group_id: z.string().describe("ID of the lead group to resolve context for."),
@@ -290,7 +294,9 @@ export function registerDataRoomTools(server) {
290
294
  const params = new URLSearchParams();
291
295
  params.set("lead_group_id", lead_group_id);
292
296
  const data = await getClient().get("/api/outreach-context", params);
293
- return toolResult(data);
297
+ // Strip the `outreach` block — outreach config is managed via Fine Tuning only.
298
+ const { outreach, ...rest } = data;
299
+ return toolResult(rest);
294
300
  }
295
301
  catch (error) {
296
302
  return handleToolError(error);
@@ -67,6 +67,51 @@ const urlValue = z
67
67
  .union([z.literal(""), z.string().url()])
68
68
  .nullable()
69
69
  .optional();
70
+ // ── Schemas for persona-migrated fields (PRD-737) ────────────────────────
71
+ const outreachTemplate = z
72
+ .object({
73
+ id: z.string().optional(),
74
+ signal: z.string().optional(),
75
+ channel: z.string().nullable().optional(),
76
+ fieldName: z.string().optional(),
77
+ signalContext: z.string().optional(),
78
+ purpose: z.string().optional(),
79
+ example: z.string().optional(),
80
+ maxLength: z.number().optional(),
81
+ subject: z.string().optional(),
82
+ body: z.string().optional(),
83
+ })
84
+ .passthrough();
85
+ const outreachSignalRouting = z
86
+ .object({
87
+ signalSourceField: z.string().optional(),
88
+ defaultSignal: z.string().optional(),
89
+ signals: z.array(z.string()).optional(),
90
+ })
91
+ .passthrough()
92
+ .nullable();
93
+ const extraPromptPhase = z
94
+ .object({
95
+ scope: z.enum(["qualify", "outreach"]),
96
+ insertionPoint: z.enum(["prefix", "phase_1_5", "phase_2_extra", "suffix"]),
97
+ content: z.string(),
98
+ })
99
+ .passthrough();
100
+ const senderOverride = z
101
+ .object({ name: z.string().min(1), title: z.string().optional() })
102
+ .nullable()
103
+ .optional();
104
+ const icpStrategy = z
105
+ .object({
106
+ dealType: z.string().optional(),
107
+ dreamOutcome: z.string().optional(),
108
+ mainPains: z.array(z.string()).optional(),
109
+ keyObjections: z.array(z.string()).optional(),
110
+ decisionDrivers: z.array(z.string()).optional(),
111
+ })
112
+ .passthrough()
113
+ .nullable()
114
+ .optional();
70
115
  async function fetchCurrentSettings(leadGroupId) {
71
116
  const data = await getClient().get(`/api/lead-group/${encodeURIComponent(leadGroupId)}/outreach-settings`);
72
117
  return data;
@@ -109,7 +154,10 @@ export function registerOutreachSettingsTools(server) {
109
154
  "provided — if you only want to tweak a single slot or sub-field, prefer " +
110
155
  "update_outreach_positioning / update_outreach_sequence_slot / " +
111
156
  "update_outreach_rules / update_outreach_case_study / update_outreach_urls / " +
112
- "set_outreach_connection_request, which do safe read-modify-write merges.", {
157
+ "set_outreach_connection_request / update_outreach_agent_identity / " +
158
+ "update_outreach_icp_strategy / update_outreach_pain_point / " +
159
+ "update_outreach_lookalike_client / update_outreach_outreach_template / " +
160
+ "update_outreach_signal_routing, which do safe read-modify-write merges.", {
113
161
  lead_group_id: z.string().describe("Lead group ID."),
114
162
  connection_request_enabled: z
115
163
  .boolean()
@@ -133,9 +181,69 @@ export function registerOutreachSettingsTools(server) {
133
181
  .max(5)
134
182
  .optional()
135
183
  .describe("Up to 5 case studies used as proof (title, summary ≤200 chars, idealFor, proofPoints[])."),
136
- }, async ({ lead_group_id, connection_request_enabled, positioning: pos, sequence_design, rules: rls, booking_url, website_url, case_studies, }) => {
184
+ // ── Fields migrated from Persona (PRD-737) ───────────────────────────
185
+ description: z
186
+ .string()
187
+ .optional()
188
+ .describe("Description / context for this lead group's outreach."),
189
+ output_language: z
190
+ .string()
191
+ .optional()
192
+ .describe("Output language code (fr, en, de, ...)."),
193
+ agent_domain: z
194
+ .string()
195
+ .optional()
196
+ .describe("Agent identity string (e.g. 'for medtech companies')."),
197
+ icp_strategy: icpStrategy
198
+ .optional()
199
+ .describe("ICP strategy mental map: {dealType, dreamOutcome, mainPains[], keyObjections[], decisionDrivers[]}."),
200
+ pain_points: z
201
+ .array(z.object({ title: z.string().optional(), description: z.string().optional() }).passthrough())
202
+ .optional()
203
+ .describe("Structured business pain points ({title, description})."),
204
+ lookalike_clients: z
205
+ .array(z
206
+ .object({
207
+ name: z.string(),
208
+ sector: z.string().optional(),
209
+ size: z.string().optional(),
210
+ segment: z.string().optional(),
211
+ comparison_criteria: z.string().optional(),
212
+ })
213
+ .passthrough())
214
+ .optional()
215
+ .describe("Lookalike client references ({name, sector?, size?, segment?, comparison_criteria?})."),
216
+ outreach_templates: z
217
+ .array(outreachTemplate)
218
+ .optional()
219
+ .describe("Outreach message templates by channel/signal. Each: {channel, signal, fieldName, purpose, example, subject, body}."),
220
+ tone_instructions: z
221
+ .string()
222
+ .optional()
223
+ .describe("Free-form markdown voice / tone guidance."),
224
+ messaging_guidelines: z
225
+ .any()
226
+ .optional()
227
+ .describe("Messaging approach: {tone, language, can_mention, must_not_mention, value_propositions}."),
228
+ outreach_signal_routing: outreachSignalRouting
229
+ .optional()
230
+ .describe("Signal routing config: {signalSourceField, defaultSignal, signals[]}. defaultSignal must exist in signals[]."),
231
+ sender_override: senderOverride
232
+ .describe("Override SDR identity: {name, title?}. Pass null to clear."),
233
+ outreach_fields: z
234
+ .any()
235
+ .optional()
236
+ .describe("Variables referenceable in templates: [{key, label, source}]."),
237
+ extra_prompt_phases: z
238
+ .array(extraPromptPhase)
239
+ .optional()
240
+ .describe("Custom prompt snippets: [{scope, insertionPoint, content}]. scope: qualify|outreach. insertionPoint: prefix|phase_1_5|phase_2_extra|suffix."),
241
+ }, async ({ lead_group_id, connection_request_enabled, positioning: pos, sequence_design, rules: rls, booking_url, website_url, case_studies,
242
+ // ── PRD-737 fields ──────────────────────────────────────────────────
243
+ description, output_language, agent_domain, icp_strategy: icp, pain_points, lookalike_clients, outreach_templates, tone_instructions, messaging_guidelines, outreach_signal_routing: signal_routing, sender_override: sdr_override, outreach_fields, extra_prompt_phases, }) => {
137
244
  try {
138
245
  const body = {};
246
+ // Legacy fields
139
247
  if (connection_request_enabled !== undefined)
140
248
  body.connectionRequestEnabled = connection_request_enabled;
141
249
  if (pos !== undefined)
@@ -150,6 +258,33 @@ export function registerOutreachSettingsTools(server) {
150
258
  body.websiteUrl = website_url;
151
259
  if (case_studies !== undefined)
152
260
  body.caseStudies = case_studies;
261
+ // PRD-737 fields
262
+ if (description !== undefined)
263
+ body.description = description;
264
+ if (output_language !== undefined)
265
+ body.outputLanguage = output_language;
266
+ if (agent_domain !== undefined)
267
+ body.agentDomain = agent_domain;
268
+ if (icp !== undefined)
269
+ body.icpStrategy = icp;
270
+ if (pain_points !== undefined)
271
+ body.painPoints = pain_points;
272
+ if (lookalike_clients !== undefined)
273
+ body.lookalikeClients = lookalike_clients;
274
+ if (outreach_templates !== undefined)
275
+ body.outreachTemplates = outreach_templates;
276
+ if (tone_instructions !== undefined)
277
+ body.toneInstructions = tone_instructions;
278
+ if (messaging_guidelines !== undefined)
279
+ body.messagingGuidelines = messaging_guidelines;
280
+ if (signal_routing !== undefined)
281
+ body.outreachSignalRouting = signal_routing;
282
+ if (sdr_override !== undefined)
283
+ body.senderOverride = sdr_override;
284
+ if (outreach_fields !== undefined)
285
+ body.outreachFields = outreach_fields;
286
+ if (extra_prompt_phases !== undefined)
287
+ body.extraPromptPhases = extra_prompt_phases;
153
288
  const data = await putPartial(lead_group_id, body);
154
289
  return toolResult(data);
155
290
  }
@@ -407,4 +542,463 @@ export function registerOutreachSettingsTools(server) {
407
542
  return handleToolError(error);
408
543
  }
409
544
  });
545
+ // ═══════════════════════════════════════════════════════════════════════
546
+ // Granular tools for persona-migrated fields (PRD-737)
547
+ // ═══════════════════════════════════════════════════════════════════════
548
+ // ── update_outreach_agent_identity ─────────────────────────────────────
549
+ server.tool("update_outreach_agent_identity", "Patch the agent identity fields (description, outputLanguage, " +
550
+ "agentDomain). Reads current settings, merges your patch, writes back. " +
551
+ "Pass only the fields you want to change.", {
552
+ lead_group_id: z.string().describe("Lead group ID."),
553
+ description: z
554
+ .string()
555
+ .optional()
556
+ .describe("Description / context for this lead group's outreach."),
557
+ output_language: z
558
+ .string()
559
+ .optional()
560
+ .describe("Output language code (fr, en, de, ...)."),
561
+ agent_domain: z
562
+ .string()
563
+ .optional()
564
+ .describe("Agent identity string (e.g. 'for medtech companies')."),
565
+ }, async ({ lead_group_id, description, output_language, agent_domain }) => {
566
+ try {
567
+ if (description === undefined &&
568
+ output_language === undefined &&
569
+ agent_domain === undefined) {
570
+ return toolError("Provide at least one of: description, output_language, agent_domain.");
571
+ }
572
+ const current = await fetchCurrentSettings(lead_group_id);
573
+ const body = {};
574
+ if (description !== undefined)
575
+ body.description = description;
576
+ if (output_language !== undefined)
577
+ body.outputLanguage = output_language;
578
+ if (agent_domain !== undefined)
579
+ body.agentDomain = agent_domain;
580
+ const data = await putPartial(lead_group_id, body);
581
+ return toolResult(data);
582
+ }
583
+ catch (error) {
584
+ return handleToolError(error);
585
+ }
586
+ });
587
+ // ── update_outreach_icp_strategy ──────────────────────────────────────
588
+ server.tool("update_outreach_icp_strategy", "Patch the ICP strategy mental map for the outreach agent. Reads " +
589
+ "current icpStrategy, shallow-merges the keys you pass, writes back. " +
590
+ "Arrays (mainPains/keyObjections/decisionDrivers) are replaced " +
591
+ "wholesale when passed.", {
592
+ lead_group_id: z.string().describe("Lead group ID."),
593
+ deal_type: z
594
+ .string()
595
+ .optional()
596
+ .describe("Type of deal (e.g. 'midmarket', 'enterprise', 'self-serve')."),
597
+ dream_outcome: z
598
+ .string()
599
+ .optional()
600
+ .describe("The aspirational result the lead wants."),
601
+ main_pains: z
602
+ .array(z.string())
603
+ .optional()
604
+ .describe("Replaces mainPains[] wholesale."),
605
+ key_objections: z
606
+ .array(z.string())
607
+ .optional()
608
+ .describe("Replaces keyObjections[] wholesale."),
609
+ decision_drivers: z
610
+ .array(z.string())
611
+ .optional()
612
+ .describe("Replaces decisionDrivers[] wholesale."),
613
+ }, async ({ lead_group_id, deal_type, dream_outcome, main_pains, key_objections, decision_drivers, }) => {
614
+ try {
615
+ if (deal_type === undefined &&
616
+ dream_outcome === undefined &&
617
+ main_pains === undefined &&
618
+ key_objections === undefined &&
619
+ decision_drivers === undefined) {
620
+ return toolError("Provide at least one field to update.");
621
+ }
622
+ const current = await fetchCurrentSettings(lead_group_id);
623
+ const merged = {
624
+ ...(current.icpStrategy ?? {}),
625
+ ...(deal_type !== undefined && { dealType: deal_type }),
626
+ ...(dream_outcome !== undefined && { dreamOutcome: dream_outcome }),
627
+ ...(main_pains !== undefined && { mainPains: main_pains }),
628
+ ...(key_objections !== undefined && { keyObjections: key_objections }),
629
+ ...(decision_drivers !== undefined && { decisionDrivers: decision_drivers }),
630
+ };
631
+ const data = await putPartial(lead_group_id, { icpStrategy: merged });
632
+ return toolResult(data);
633
+ }
634
+ catch (error) {
635
+ return handleToolError(error);
636
+ }
637
+ });
638
+ // ── update_outreach_pain_point ────────────────────────────────────────
639
+ server.tool("update_outreach_pain_point", "Add, replace, or remove a single pain point without re-sending the " +
640
+ "whole list. Pain points have no stable id — entries are addressed by " +
641
+ "zero-based index. Beware: indices shift after a remove.", {
642
+ lead_group_id: z.string().describe("Lead group ID."),
643
+ action: z
644
+ .enum(["add", "replace", "remove"])
645
+ .describe("add = append; replace = overwrite at index; remove = delete at index."),
646
+ index: z
647
+ .number()
648
+ .int()
649
+ .nonnegative()
650
+ .optional()
651
+ .describe("Zero-based index. Required for 'replace' and 'remove'."),
652
+ pain: z
653
+ .object({
654
+ title: z.string().optional(),
655
+ description: z.string().optional(),
656
+ })
657
+ .optional()
658
+ .describe("Pain point payload {title?, description?}. Required for 'add' and 'replace'."),
659
+ }, async ({ lead_group_id, action, index, pain }) => {
660
+ try {
661
+ if ((action === "add" || action === "replace") && !pain) {
662
+ return toolError(`pain is required for action '${action}'.`);
663
+ }
664
+ if ((action === "replace" || action === "remove") && index === undefined) {
665
+ return toolError(`index is required for action '${action}'.`);
666
+ }
667
+ const current = await fetchCurrentSettings(lead_group_id);
668
+ const list = Array.isArray(current.painPoints)
669
+ ? [...current.painPoints]
670
+ : [];
671
+ if (action === "add") {
672
+ list.push(pain);
673
+ }
674
+ else if (action === "replace") {
675
+ if (index < 0 || index >= list.length) {
676
+ return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
677
+ }
678
+ list[index] = pain;
679
+ }
680
+ else {
681
+ if (index < 0 || index >= list.length) {
682
+ return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
683
+ }
684
+ list.splice(index, 1);
685
+ }
686
+ const data = await putPartial(lead_group_id, { painPoints: list });
687
+ return toolResult(data);
688
+ }
689
+ catch (error) {
690
+ return handleToolError(error);
691
+ }
692
+ });
693
+ // ── update_outreach_lookalike_client ──────────────────────────────────
694
+ server.tool("update_outreach_lookalike_client", "Add, replace, or remove a single lookalike client without re-sending " +
695
+ "the whole list. Lookups are by client name (case-sensitive exact match).", {
696
+ lead_group_id: z.string().describe("Lead group ID."),
697
+ action: z
698
+ .enum(["add", "replace", "remove"])
699
+ .describe("add = append (fails if name exists); replace = overwrite by name; remove = delete by name."),
700
+ name: z
701
+ .string()
702
+ .optional()
703
+ .describe("Lookup key for replace/remove. Required for those actions."),
704
+ client: z
705
+ .object({
706
+ name: z.string(),
707
+ sector: z.string().optional(),
708
+ size: z.string().optional(),
709
+ segment: z.string().optional(),
710
+ comparison_criteria: z.string().optional(),
711
+ })
712
+ .passthrough()
713
+ .optional()
714
+ .describe("Client payload {name, sector?, size?, segment?, comparison_criteria?}. Required for 'add' and 'replace'."),
715
+ }, async ({ lead_group_id, action, name, client }) => {
716
+ try {
717
+ if ((action === "add" || action === "replace") && !client) {
718
+ return toolError(`client is required for action '${action}'.`);
719
+ }
720
+ if (action !== "add" && !name) {
721
+ return toolError(`name is required for action '${action}'.`);
722
+ }
723
+ const current = await fetchCurrentSettings(lead_group_id);
724
+ const list = Array.isArray(current.lookalikeClients)
725
+ ? [...current.lookalikeClients]
726
+ : [];
727
+ if (action === "add") {
728
+ const exists = list.some((c) => c.name === client.name);
729
+ if (exists) {
730
+ return toolError(`Lookalike client '${client.name}' already exists. Use 'replace' to update.`);
731
+ }
732
+ list.push(client);
733
+ }
734
+ else if (action === "replace") {
735
+ const idx = list.findIndex((c) => c.name === name);
736
+ if (idx === -1) {
737
+ return toolError(`Lookalike client '${name}' not found.`);
738
+ }
739
+ list[idx] = client;
740
+ }
741
+ else {
742
+ const idx = list.findIndex((c) => c.name === name);
743
+ if (idx === -1) {
744
+ return toolError(`Lookalike client '${name}' not found.`);
745
+ }
746
+ list.splice(idx, 1);
747
+ }
748
+ const data = await putPartial(lead_group_id, { lookalikeClients: list });
749
+ return toolResult(data);
750
+ }
751
+ catch (error) {
752
+ return handleToolError(error);
753
+ }
754
+ });
755
+ // ── update_outreach_outreach_template ─────────────────────────────────
756
+ server.tool("update_outreach_outreach_template", "Add, replace, or remove a single outreach template without re-sending " +
757
+ "the whole list. Lookups are by template id (preferred) or by " +
758
+ "composite key (channel + signal + fieldName).", {
759
+ lead_group_id: z.string().describe("Lead group ID."),
760
+ action: z
761
+ .enum(["add", "replace", "remove"])
762
+ .describe("add = append; replace = overwrite by lookup; remove = delete by lookup."),
763
+ template_id: z
764
+ .string()
765
+ .optional()
766
+ .describe("Template id for replace/remove. Preferred lookup key."),
767
+ lookup_channel: z
768
+ .string()
769
+ .optional()
770
+ .describe("Fallback lookup: channel (combined with lookup_signal + lookup_field_name)."),
771
+ lookup_signal: z
772
+ .string()
773
+ .optional()
774
+ .describe("Fallback lookup: signal."),
775
+ lookup_field_name: z
776
+ .string()
777
+ .optional()
778
+ .describe("Fallback lookup: field_name."),
779
+ template: outreachTemplate
780
+ .optional()
781
+ .describe("Template payload. Required for 'add' and 'replace'. " +
782
+ "{channel, signal, fieldName, purpose, example, subject, body}."),
783
+ }, async ({ lead_group_id, action, template_id, lookup_channel, lookup_signal, lookup_field_name, template: tmpl, }) => {
784
+ try {
785
+ if ((action === "add" || action === "replace") && !tmpl) {
786
+ return toolError(`template is required for action '${action}'.`);
787
+ }
788
+ const current = await fetchCurrentSettings(lead_group_id);
789
+ const list = Array.isArray(current.outreachTemplates)
790
+ ? [...current.outreachTemplates]
791
+ : [];
792
+ if (action === "add") {
793
+ list.push(tmpl);
794
+ }
795
+ else {
796
+ // Resolve lookup key
797
+ let idx = -1;
798
+ if (template_id) {
799
+ idx = list.findIndex((t) => t.id === template_id);
800
+ }
801
+ else if (lookup_channel && lookup_signal && lookup_field_name) {
802
+ idx = list.findIndex((t) => t.channel === lookup_channel &&
803
+ t.signal === lookup_signal &&
804
+ t.fieldName === lookup_field_name);
805
+ }
806
+ else {
807
+ return toolError("Provide template_id or (lookup_channel + lookup_signal + lookup_field_name) for replace/remove.");
808
+ }
809
+ if (idx === -1) {
810
+ return toolError("Template not found with the given lookup key.");
811
+ }
812
+ if (action === "replace") {
813
+ list[idx] = tmpl;
814
+ }
815
+ else {
816
+ list.splice(idx, 1);
817
+ }
818
+ }
819
+ const data = await putPartial(lead_group_id, { outreachTemplates: list });
820
+ return toolResult(data);
821
+ }
822
+ catch (error) {
823
+ return handleToolError(error);
824
+ }
825
+ });
826
+ // ── update_outreach_tone_instructions ──────────────────────────────────
827
+ server.tool("update_outreach_tone_instructions", "Replace the tone instructions (free-form markdown for voice / tone " +
828
+ "guidance). Pass null to clear.", {
829
+ lead_group_id: z.string().describe("Lead group ID."),
830
+ tone_instructions: z
831
+ .union([z.string(), z.null()])
832
+ .describe("Markdown tone guidance. Pass null to clear."),
833
+ }, async ({ lead_group_id, tone_instructions }) => {
834
+ try {
835
+ const data = await putPartial(lead_group_id, { toneInstructions: tone_instructions });
836
+ return toolResult(data);
837
+ }
838
+ catch (error) {
839
+ return handleToolError(error);
840
+ }
841
+ });
842
+ // ── update_outreach_messaging_guidelines ──────────────────────────────
843
+ server.tool("update_outreach_messaging_guidelines", "Patch the messaging guidelines. Reads current messagingGuidelines, " +
844
+ "shallow-merges the keys you pass, writes back. Common keys: tone, " +
845
+ "language, can_mention, must_not_mention, value_propositions.", {
846
+ lead_group_id: z.string().describe("Lead group ID."),
847
+ patch: z
848
+ .any()
849
+ .describe("Object whose keys are merged into existing messagingGuidelines (shallow). " +
850
+ "E.g. {\"tone\": \"professional\", \"can_mention\": [\"AI\", \"ROI\"]}."),
851
+ remove_keys: z
852
+ .array(z.string())
853
+ .optional()
854
+ .describe("Keys to delete from messagingGuidelines."),
855
+ }, async ({ lead_group_id, patch, remove_keys }) => {
856
+ try {
857
+ const current = await fetchCurrentSettings(lead_group_id);
858
+ const existing = (current.messagingGuidelines ?? {});
859
+ const merged = { ...existing, ...patch };
860
+ if (remove_keys) {
861
+ for (const k of remove_keys)
862
+ delete merged[k];
863
+ }
864
+ const data = await putPartial(lead_group_id, { messagingGuidelines: merged });
865
+ return toolResult(data);
866
+ }
867
+ catch (error) {
868
+ return handleToolError(error);
869
+ }
870
+ });
871
+ // ── update_outreach_signal_routing ────────────────────────────────────
872
+ server.tool("update_outreach_signal_routing", "Patch the outreach signal routing config. signalSourceField and " +
873
+ "defaultSignal are scalar — passing one replaces it. signals[] is " +
874
+ "replaced wholesale when passed; use add_signals / remove_signals to " +
875
+ "splice instead. WARNING: defaultSignal must be present in signals[] " +
876
+ "(API rejects otherwise).", {
877
+ lead_group_id: z.string().describe("Lead group ID."),
878
+ signal_source_field: z
879
+ .string()
880
+ .optional()
881
+ .describe("Card field to read (e.g. card_signals)."),
882
+ default_signal: z
883
+ .string()
884
+ .optional()
885
+ .describe("Default signal — must exist in signals[]."),
886
+ signals: z
887
+ .array(z.string())
888
+ .optional()
889
+ .describe("Full replacement of the signals list. Mutually exclusive with add_signals / remove_signals."),
890
+ add_signals: z
891
+ .array(z.string())
892
+ .optional()
893
+ .describe("Signals to add (deduped). Mutually exclusive with signals."),
894
+ remove_signals: z
895
+ .array(z.string())
896
+ .optional()
897
+ .describe("Signals to remove. Mutually exclusive with signals."),
898
+ }, async ({ lead_group_id, signal_source_field, default_signal, signals, add_signals, remove_signals, }) => {
899
+ try {
900
+ const current = await fetchCurrentSettings(lead_group_id);
901
+ const existing = (current.outreachSignalRouting ?? {});
902
+ const next = { ...existing };
903
+ if (signal_source_field !== undefined)
904
+ next.signalSourceField = signal_source_field;
905
+ if (default_signal !== undefined)
906
+ next.defaultSignal = default_signal;
907
+ if (signals !== undefined) {
908
+ next.signals = signals;
909
+ }
910
+ else {
911
+ const currentSignals = existing.signals ?? [];
912
+ let updatedSignals = [...currentSignals];
913
+ if (add_signals) {
914
+ for (const s of add_signals) {
915
+ if (!updatedSignals.includes(s))
916
+ updatedSignals.push(s);
917
+ }
918
+ }
919
+ if (remove_signals) {
920
+ updatedSignals = updatedSignals.filter((s) => !remove_signals.includes(s));
921
+ }
922
+ if (add_signals || remove_signals)
923
+ next.signals = updatedSignals;
924
+ }
925
+ const data = await putPartial(lead_group_id, { outreachSignalRouting: next });
926
+ return toolResult(data);
927
+ }
928
+ catch (error) {
929
+ return handleToolError(error);
930
+ }
931
+ });
932
+ // ── update_outreach_sender_override ───────────────────────────────────
933
+ server.tool("update_outreach_sender_override", "Set or clear the SDR identity override for this lead group. " +
934
+ "Pass {name, title?} to set. Pass null to clear.", {
935
+ lead_group_id: z.string().describe("Lead group ID."),
936
+ sender_override: z
937
+ .union([
938
+ z.object({ name: z.string().min(1), title: z.string().optional() }),
939
+ z.null(),
940
+ ])
941
+ .describe("SDR identity: {name, title?}. Pass null to clear."),
942
+ }, async ({ lead_group_id, sender_override }) => {
943
+ try {
944
+ const data = await putPartial(lead_group_id, { senderOverride: sender_override });
945
+ return toolResult(data);
946
+ }
947
+ catch (error) {
948
+ return handleToolError(error);
949
+ }
950
+ });
951
+ // ── update_outreach_extra_prompt_phase ────────────────────────────────
952
+ server.tool("update_outreach_extra_prompt_phase", "Add, replace, or remove a single extra prompt phase snippet. " +
953
+ "Entries have no stable id — addressed by zero-based index. " +
954
+ "Beware: indices shift after a remove.", {
955
+ lead_group_id: z.string().describe("Lead group ID."),
956
+ action: z
957
+ .enum(["add", "replace", "remove"])
958
+ .describe("add = append; replace = overwrite at index; remove = delete at index."),
959
+ index: z
960
+ .number()
961
+ .int()
962
+ .nonnegative()
963
+ .optional()
964
+ .describe("Zero-based index. Required for 'replace' and 'remove'."),
965
+ phase: extraPromptPhase
966
+ .optional()
967
+ .describe("Phase payload {scope, insertionPoint, content}. " +
968
+ "scope: qualify|outreach. insertionPoint: prefix|phase_1_5|phase_2_extra|suffix. " +
969
+ "Required for 'add' and 'replace'."),
970
+ }, async ({ lead_group_id, action, index, phase }) => {
971
+ try {
972
+ if ((action === "add" || action === "replace") && !phase) {
973
+ return toolError(`phase is required for action '${action}'.`);
974
+ }
975
+ if ((action === "replace" || action === "remove") && index === undefined) {
976
+ return toolError(`index is required for action '${action}'.`);
977
+ }
978
+ const current = await fetchCurrentSettings(lead_group_id);
979
+ const list = Array.isArray(current.extraPromptPhases)
980
+ ? [...current.extraPromptPhases]
981
+ : [];
982
+ if (action === "add") {
983
+ list.push(phase);
984
+ }
985
+ else if (action === "replace") {
986
+ if (index < 0 || index >= list.length) {
987
+ return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
988
+ }
989
+ list[index] = phase;
990
+ }
991
+ else {
992
+ if (index < 0 || index >= list.length) {
993
+ return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
994
+ }
995
+ list.splice(index, 1);
996
+ }
997
+ const data = await putPartial(lead_group_id, { extraPromptPhases: list });
998
+ return toolResult(data);
999
+ }
1000
+ catch (error) {
1001
+ return handleToolError(error);
1002
+ }
1003
+ });
410
1004
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "4.0.0",
3
+ "version": "5.0.1",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",