@agifyai/leadify-mcp 8.7.6 → 8.8.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/README.md CHANGED
@@ -266,6 +266,14 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
266
266
  | `preview_unenroll_campaign_audience` | Prévisualiser de façon déterministe les membres explicites sans message pour le canal de leur campagne ou sans aucun canal de contact. Retourne les `lead_ids` exacts, sans mutation. |
267
267
  | `unenroll_campaign_audience` | Désenrôler uniquement la liste complète de `lead_ids` renvoyée par le preview courant. Relit et refuse toute sélection partielle, étendue ou périmée ; aucun envoi. |
268
268
  | `export_campaign` | Exporter les statistiques en CSV ; après clôture définitive, l'export provient du rapport final figé. |
269
+ | `list_playbooks` / `get_playbook` | Lister et lire les « Messages types » (playbooks du rédacteur) d’une organisation, avec leur contenu complet à la dernière version. |
270
+ | `publish_playbook` | Publier un message type : création, duplication ou nouvelle version (avec la version relue). Les refus du contrat du rédacteur sont rendus avec leur chemin. |
271
+ | `list_campaign_senders` | Lister les comptes d’envoi (LinkedIn, boîtes mail) de la personne qui porte la clé : les messages partent à son nom. |
272
+ | `get_campaign_audience_options` | Compter les personnes d’un groupe et de chacun de ses segments pour choisir l’audience d’une campagne. |
273
+ | `get_contact_policy` | Lire la politique de contact effective de l’organisation ou d’une campagne, en lecture seule. |
274
+ | `save_campaign_draft` / `get_campaign_draft` | Créer, modifier et relire un brouillon par l’assistant de campagne : audience, messages types jamais épinglés, expéditeur, fenêtre, jours, horaires et plafond quotidien. Toujours en mode `REVIEW` : chaque message attend une validation humaine. |
275
+ | `preview_campaign_draft` | Évaluer l’audience avec le moteur d’éligibilité (personnes par message type, raisons de non-contact), sans rien écrire. |
276
+ | `launch_campaign` | Lancer un brouillon comme le bouton « Lancer la campagne » (inscription, activation, rédaction immédiate), avec la confirmation explicite `launch_campaign`. Les messages attendent dans la file « À valider ». |
269
277
  | `signal_upsert` | Créer ou mettre à jour un signal de business intelligence (INFO, CRITICAL, GOLDEN). Remet le state à `active`. |
270
278
  | `signal_expire` | Expirer un signal (événement périmé). Flip de `state` uniquement. |
271
279
  | `signal_disable` | Désactiver un signal (faux positif / écarté manuellement). Flip de `state` uniquement. |
package/dist/server.js CHANGED
@@ -4,6 +4,7 @@ import { registerLeadTools } from "./tools/leads.js";
4
4
  import { registerSchemaTools } from "./tools/schema.js";
5
5
  import { registerLogTools } from "./tools/logs.js";
6
6
  import { registerCampaignTools } from "./tools/campaigns.js";
7
+ import { registerCampaignWizardTools } from "./tools/campaign_wizard.js";
7
8
  import { registerSignalTools } from "./tools/signals.js";
8
9
  import { registerActivityTools } from "./tools/activities.js";
9
10
  import { registerDataSourceTools } from "./tools/datasources.js";
@@ -43,6 +44,7 @@ export function createServer() {
43
44
  registerSchemaTools(server);
44
45
  registerLogTools(server);
45
46
  registerCampaignTools(server);
47
+ registerCampaignWizardTools(server);
46
48
  registerSignalTools(server);
47
49
  registerActivityTools(server);
48
50
  registerContextWorkspaceTools(server);
@@ -0,0 +1,5 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type LeadifyClient } from "../client.js";
3
+ type WizardClient = Pick<LeadifyClient, "get" | "post">;
4
+ export declare function registerCampaignWizardTools(server: McpServer, injectedClient?: WizardClient): void;
5
+ export {};
@@ -0,0 +1,204 @@
1
+ import { z } from "zod";
2
+ import { getClient } from "../client.js";
3
+ import { toolResult, handleToolError } from "../types.js";
4
+ const organizationId = z.string().min(1).describe("Organization selected explicitly via discover_leadify_context or list_organizations.");
5
+ const time = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/);
6
+ function scoped(params) {
7
+ const query = new URLSearchParams();
8
+ for (const [key, value] of Object.entries(params))
9
+ if (value !== undefined)
10
+ query.set(key, value);
11
+ return query;
12
+ }
13
+ /** The server always stores REVIEW; refuse to report success if that ever changes. */
14
+ function assertReviewMode(data) {
15
+ const campaign = data && typeof data === "object" ? data.campaign : undefined;
16
+ const mode = campaign && typeof campaign === "object" ? campaign.autonomyMode : undefined;
17
+ if (mode !== "REVIEW") {
18
+ throw new Error(`Leadify returned a campaign whose autonomy mode is not REVIEW (${String(mode)}); stop and check the campaign in Leadify.`);
19
+ }
20
+ return data;
21
+ }
22
+ export function registerCampaignWizardTools(server, injectedClient) {
23
+ const client = () => injectedClient ?? getClient();
24
+ // ── Messages types (playbooks) ─────────────────────────────────────────
25
+ server.tool("list_playbooks", "List the organization's 'Messages types' (outreach playbooks) used by the outreach writer: id, name, kind " +
26
+ "(EVENT or GENERIC), latest version, validity window, linked event, channels with steps, and campaign usage.", {
27
+ organization_id: organizationId,
28
+ include_archived: z.boolean().optional().default(false).describe("Include archived playbooks."),
29
+ }, async ({ organization_id, include_archived }) => {
30
+ try {
31
+ return toolResult(await client().get("/api/playbooks", scoped({ organizationId: organization_id, includeArchived: include_archived ? "true" : undefined })));
32
+ }
33
+ catch (error) {
34
+ return handleToolError(error);
35
+ }
36
+ });
37
+ server.tool("get_playbook", "Read one 'Message type' at its latest version, with its full editable content (intent, eligibility, " +
38
+ "judgment guidance, promise, proofs, CTA, sequences and examples). Use it before publishing a new version.", {
39
+ organization_id: organizationId,
40
+ playbook_id: z.string().min(1).describe("Playbook ID from list_playbooks."),
41
+ }, async ({ organization_id, playbook_id }) => {
42
+ try {
43
+ return toolResult(await client().get(`/api/playbooks/${encodeURIComponent(playbook_id)}`, scoped({ organizationId: organization_id })));
44
+ }
45
+ catch (error) {
46
+ return handleToolError(error);
47
+ }
48
+ });
49
+ server.tool("publish_playbook", "Publish a 'Message type' (outreach playbook): create one (no playbook_id), duplicate one (duplicated_from_id), " +
50
+ "or publish a new version of an existing one (playbook_id plus the expected_version read by get_playbook). " +
51
+ "content follows the outreach contract without key, version, exclusivityKey and kind: name, intent, validity " +
52
+ "{startsAt?, endsAt?}, eligibility {all: [...]}, judgmentGuidance, promise, proofs [{id, statement, sourceUrl?}], " +
53
+ "cta {description, url?}, sequences [{channel: EMAIL|LINKEDIN, steps: [{step, kind: EMAIL|LINKEDIN_INVITE|LINKEDIN_MESSAGE, delayDays, purpose}]}], " +
54
+ "examples [{channel, step, subject?, body, commentary?}]. An EVENT playbook requires an eligibility condition " +
55
+ "{type: 'event_participation', eventId, status: 'VERIFIED'}. Contract refusals are returned with their JSON path.", {
56
+ organization_id: organizationId,
57
+ kind: z.enum(["EVENT", "GENERIC"]).describe("EVENT requires a verified event participation; GENERIC does not. Fixed after creation."),
58
+ content: z.record(z.unknown()).describe("Playbook content (see the tool description)."),
59
+ playbook_id: z.string().min(1).optional().describe("Existing playbook to publish a new version of."),
60
+ expected_version: z.number().int().min(1).optional().describe("Latest version read by get_playbook; required with playbook_id."),
61
+ duplicated_from_id: z.string().min(1).optional().describe("Playbook duplicated into a new one."),
62
+ }, async ({ organization_id, kind, content, playbook_id, expected_version, duplicated_from_id }) => {
63
+ try {
64
+ const body = { organizationId: organization_id, kind, content };
65
+ if (playbook_id !== undefined)
66
+ body.playbookId = playbook_id;
67
+ if (expected_version !== undefined)
68
+ body.expectedVersion = expected_version;
69
+ if (duplicated_from_id !== undefined)
70
+ body.duplicatedFromId = duplicated_from_id;
71
+ return toolResult(await client().post("/api/playbooks", body));
72
+ }
73
+ catch (error) {
74
+ return handleToolError(error);
75
+ }
76
+ });
77
+ // ── Campaign wizard ────────────────────────────────────────────────────
78
+ server.tool("list_campaign_senders", "List the sending accounts of the person who owns the API key in this organization (connected LinkedIn " +
79
+ "accounts and mailboxes). Messages are sent in that person's name, so a campaign can only use these senders.", { organization_id: organizationId }, async ({ organization_id }) => {
80
+ try {
81
+ return toolResult(await client().get("/api/campaign-wizard/senders", scoped({ organizationId: organization_id })));
82
+ }
83
+ catch (error) {
84
+ return handleToolError(error);
85
+ }
86
+ });
87
+ server.tool("get_campaign_audience_options", "Count the people of a lead group and of each saved view (segment), to choose a campaign audience. " +
88
+ "A campaign audience is the whole group or one saved view; create a view with create_lead_view to target a subset " +
89
+ "(for example an __event_participation filter for one event).", {
90
+ organization_id: organizationId,
91
+ lead_group_id: z.string().min(1).describe("Person lead group ID."),
92
+ }, async ({ organization_id, lead_group_id }) => {
93
+ try {
94
+ return toolResult(await client().get("/api/campaign-wizard/audience-options", scoped({ organizationId: organization_id, leadGroupId: lead_group_id })));
95
+ }
96
+ catch (error) {
97
+ return handleToolError(error);
98
+ }
99
+ });
100
+ server.tool("get_contact_policy", "Read the effective contact policy (cooldown days, maximum people per account and window, maximum qualification age) " +
101
+ "of the organization, or of one campaign when campaign_id is given. Read-only.", {
102
+ organization_id: organizationId,
103
+ campaign_id: z.string().min(1).optional().describe("Optional campaign ID for its effective policy."),
104
+ }, async ({ organization_id, campaign_id }) => {
105
+ try {
106
+ return toolResult(await client().get("/api/campaign-wizard/contact-policy", scoped({ organizationId: organization_id, campaignId: campaign_id })));
107
+ }
108
+ catch (error) {
109
+ return handleToolError(error);
110
+ }
111
+ });
112
+ server.tool("save_campaign_draft", "Create or update a DRAFT campaign through the Leadify campaign wizard: audience (group or saved view), " +
113
+ "'Messages types' (never pinned to a version), sender, window, send days, hours and daily limit. The campaign " +
114
+ "is always in REVIEW mode: every written message waits for a human validation before sending. Nothing is " +
115
+ "written or sent before launch_campaign. Pass campaign_id to update an existing draft.", {
116
+ organization_id: organizationId,
117
+ campaign_id: z.string().min(1).optional().describe("Existing DRAFT campaign to update."),
118
+ name: z.string().min(1).max(200).describe("Campaign name."),
119
+ lead_group_id: z.string().min(1).describe("Person lead group ID."),
120
+ audience_view_id: z.string().min(1).nullable().describe("Saved view used as audience, or null for the whole group."),
121
+ primary_playbook_ids: z.array(z.string().min(1)).max(20).describe("Primary 'Messages types', in priority order."),
122
+ fallback_playbook_id: z.string().min(1).nullable().optional().describe("Optional fallback 'Message type'."),
123
+ channel: z.enum(["LINKEDIN", "EMAIL"]).describe("Delivery channel."),
124
+ unipile_account_id: z.string().min(1).nullable().optional().describe("LinkedIn sender from list_campaign_senders."),
125
+ mailbox_provider: z.enum(["OUTLOOK", "GMAIL", "SMTP"]).nullable().optional().describe("Email sender provider from list_campaign_senders."),
126
+ mailbox_owner_user_id: z.string().min(1).nullable().optional().describe("Email sender owner from list_campaign_senders."),
127
+ start_at: z.string().datetime().nullable().describe("Inclusive start (ISO-8601 UTC); required before launch."),
128
+ end_at: z.string().datetime().nullable().describe("Exclusive end (ISO-8601 UTC); required before launch."),
129
+ timezone: z.string().min(1).describe("IANA timezone, for example Europe/Paris."),
130
+ workday_start: time.describe("Sending window start, HH:MM."),
131
+ workday_end: time.describe("Sending window end, HH:MM."),
132
+ send_days: z
133
+ .object({ monday: z.boolean(), tuesday: z.boolean(), wednesday: z.boolean(), thursday: z.boolean(), friday: z.boolean(), saturday: z.boolean(), sunday: z.boolean() })
134
+ .describe("Allowed sending days."),
135
+ daily_limit: z.number().int().min(1).describe("Daily sending limit for the channel."),
136
+ }, async (input) => {
137
+ try {
138
+ const linkedin = input.channel === "LINKEDIN";
139
+ const body = {
140
+ organizationId: input.organization_id,
141
+ ...(input.campaign_id ? { campaignId: input.campaign_id } : {}),
142
+ name: input.name,
143
+ leadGroupId: input.lead_group_id,
144
+ audienceViewId: input.audience_view_id,
145
+ primaryPlaybookIds: input.primary_playbook_ids,
146
+ fallbackEnabled: Boolean(input.fallback_playbook_id),
147
+ fallbackPlaybookId: input.fallback_playbook_id ?? null,
148
+ channel: input.channel,
149
+ unipileAccountId: linkedin ? input.unipile_account_id ?? null : null,
150
+ mailboxProvider: linkedin ? null : input.mailbox_provider ?? null,
151
+ mailboxOwnerUserId: linkedin ? null : input.mailbox_owner_user_id ?? null,
152
+ startAt: input.start_at,
153
+ endAt: input.end_at,
154
+ defaultTimezone: input.timezone,
155
+ workdayStart: input.workday_start,
156
+ workdayEnd: input.workday_end,
157
+ sendDays: input.send_days,
158
+ dailyLimit: input.daily_limit,
159
+ };
160
+ return toolResult(assertReviewMode(await client().post("/api/campaign-wizard", body)));
161
+ }
162
+ catch (error) {
163
+ return handleToolError(error);
164
+ }
165
+ });
166
+ server.tool("get_campaign_draft", "Read a campaign as the wizard sees it: audience, 'Messages types', sender, window, days, daily limit, " +
167
+ "autonomyMode (always REVIEW) and the launch blockers still missing.", {
168
+ organization_id: organizationId,
169
+ campaign_id: z.string().min(1).describe("Campaign ID."),
170
+ }, async ({ organization_id, campaign_id }) => {
171
+ try {
172
+ return toolResult(await client().get(`/api/campaign-wizard/${encodeURIComponent(campaign_id)}`, scoped({ organizationId: organization_id })));
173
+ }
174
+ catch (error) {
175
+ return handleToolError(error);
176
+ }
177
+ });
178
+ server.tool("preview_campaign_draft", "Dry-run the eligibility engine on the campaign audience: people per 'Message type' and people not contacted " +
179
+ "with their reasons. Nothing is written or sent.", {
180
+ organization_id: organizationId,
181
+ campaign_id: z.string().min(1).describe("Campaign ID."),
182
+ }, async ({ organization_id, campaign_id }) => {
183
+ try {
184
+ return toolResult(await client().get(`/api/campaign-wizard/${encodeURIComponent(campaign_id)}/preview`, scoped({ organizationId: organization_id })));
185
+ }
186
+ catch (error) {
187
+ return handleToolError(error);
188
+ }
189
+ });
190
+ server.tool("launch_campaign", "Launch a DRAFT campaign exactly like the 'Lancer la campagne' button: enroll the audience, activate the campaign " +
191
+ "and start the outreach writer immediately. In REVIEW mode every written message waits in the 'À valider' queue " +
192
+ "for a human validation before anything is sent. Confirm with the user before calling this tool.", {
193
+ organization_id: organizationId,
194
+ campaign_id: z.string().min(1).describe("DRAFT campaign ID."),
195
+ confirmation: z.literal("launch_campaign").describe("Exact acknowledgement required before launching."),
196
+ }, async ({ organization_id, campaign_id }) => {
197
+ try {
198
+ return toolResult(assertReviewMode(await client().post(`/api/campaign-wizard/${encodeURIComponent(campaign_id)}/launch`, { organizationId: organization_id })));
199
+ }
200
+ catch (error) {
201
+ return handleToolError(error);
202
+ }
203
+ });
204
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "8.7.6",
3
+ "version": "8.8.0",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",