@agifyai/leadify-mcp 8.6.9 → 8.6.11

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
@@ -235,6 +235,7 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
235
235
  | `get_leads` | Rechercher et lister des leads avec filtres, recherche et pagination, dont le critère composé `event_filter`. |
236
236
  | `get_lead` | Récupérer les détails complets d'un lead par son ID. |
237
237
  | `update_lead` | Mettre à jour un ou plusieurs champs d'un lead existant. |
238
+ | `clear_lead_field` | Effacer irréversiblement une valeur stockée, sans modifier le schéma du groupe. Exige la confirmation explicite `clear_lead_field`. |
238
239
  | `list_events` | Lister les événements tenant-scoped ; les événements archivés sont masqués par défaut. |
239
240
  | `get_event` | Lire un événement partagé et son état d’archivage. |
240
241
  | `create_event` | Créer un événement partagé avec une clé d’idempotence. |
@@ -246,7 +247,7 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
246
247
  | `preview_reset_ai_fields` / `execute_reset_ai_fields` | Prévisualiser puis effacer irréversiblement les champs IA d'un prospect, d'une liste explicite ou d'un groupe explicite, sans toucher aux contacts, flags manuels, activités ou campagnes. |
247
248
  | `delete_leads` | Supprimer définitivement des leads par leurs IDs. |
248
249
  | `update_schema` | Ajouter ou modifier les définitions de champs d'un groupe. |
249
- | `delete_columns` | Supprimer des colonnes du schéma et des données d'un groupe. |
250
+ | `delete_columns` | Supprimer des colonnes du schéma et des données d'un groupe. Avec `force: true`, supprimer aussi une clé de données orpheline absente du schéma. |
250
251
  | `update_hidden_columns` | Afficher ou masquer des colonnes dans la vue tableau (réversible). |
251
252
  | `list_crm_schema_packs` | Lister le catalogue canonique des CRM Schema Packs, leurs domaines et contraintes de compatibilité. |
252
253
  | `add_campaign_log` | Enregistrer une entrée de log de campagne pour un lead. |
@@ -254,10 +255,14 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
254
255
  | `delete_campaign_log` | Supprimer une entrée de log de campagne. |
255
256
  | `update_campaign_stats` | Mettre à jour les statistiques d'email d'une campagne pour un groupe de leads. |
256
257
  | `create_campaign` | Créer une campagne DRAFT mono-canal rattachée à un Lead Group qui possède déjà sa paire Persona–Offre. Aucun `persona_id` ou contexte indépendant n’est accepté au niveau Campagne. `channel` (`LINKEDIN` ou `EMAIL`) est obligatoire ; `start_at` inclusif, `end_at` exclusif et `timezone` IANA configurent la fenêtre métier. À `end_at`, Leadify met la campagne en pause réversible (`WINDOW_END`) sans la clôturer. |
257
- | `update_campaign_configuration` | Modifier la fenêtre de toute campagne encore `OPEN`; le canal reste modifiable uniquement en DRAFT. Prolonger `end_at` dans le futur ou le supprimer relance automatiquement une pause `WINDOW_END` après validation du fournisseur, mais jamais une pause manuelle. La fenêtre demandée reste enregistrée si cette validation échoue. |
258
+ | `update_campaign` / `update_campaign_configuration` | Modifier le nom, la description et la fenêtre de toute campagne encore `OPEN`; le canal reste modifiable uniquement en DRAFT. Prolonger `end_at` dans le futur ou le supprimer relance automatiquement une pause `WINDOW_END` après validation du fournisseur, mais jamais une pause manuelle. La fenêtre demandée reste enregistrée si cette validation échoue. La Persona est héritée du Lead Group et n’est pas modifiable au niveau Campagne. |
259
+ | `delete_campaign` | Supprimer irréversiblement une campagne `DRAFT` ou `PAUSED` encore `OPEN`, avec la confirmation explicite `delete_campaign`. Une campagne active ou clôturée est refusée, et aucun envoi n’est déclenché. |
258
260
  | `list_campaigns` | Lister compactement les campagnes d'une organisation explicitement sélectionnée, avec état effectif, fenêtre, motif de pause et clôture. |
259
261
  | `get_campaign` | Récupérer les détails d'une campagne, son état effectif, sa clôture et son éventuel rapport final figé, ainsi que ses KPIs temps réel. |
260
262
  | `update_campaign_status` | Changer le statut d'une campagne. `PAUSED` est une pause manuelle réversible ; `COMPLETED` déclenche la clôture définitive terminale et son rapport final immuable. |
263
+ | `get_campaign_audience` | Lire l'audience explicitement enrôlée d'une campagne dans une organisation explicitement sélectionnée, avec les seuls signaux nécessaires à la décision, sans contenu de message ni envoi. |
264
+ | `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. |
265
+ | `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. |
261
266
  | `export_campaign` | Exporter les statistiques en CSV ; après clôture définitive, l'export provient du rapport final figé. |
262
267
  | `signal_upsert` | Créer ou mettre à jour un signal de business intelligence (INFO, CRITICAL, GOLDEN). Remet le state à `active`. |
263
268
  | `signal_expire` | Expirer un signal (événement périmé). Flip de `state` uniquement. |
package/dist/client.js CHANGED
@@ -22,12 +22,13 @@ export class LeadifyClient {
22
22
  init.body = JSON.stringify(body);
23
23
  }
24
24
  const response = await fetch(url.toString(), init);
25
+ const text = await response.text();
25
26
  let data;
26
27
  try {
27
- data = await response.json();
28
+ data = JSON.parse(text);
28
29
  }
29
30
  catch {
30
- data = { raw: await response.text() };
31
+ data = { raw: text };
31
32
  }
32
33
  if (!response.ok) {
33
34
  throw new LeadifyApiError(response.status, data);
@@ -3,5 +3,6 @@ import { type LeadifyClient } from "../client.js";
3
3
  type CampaignReadClient = Pick<LeadifyClient, "get">;
4
4
  type CampaignWriteClient = Pick<LeadifyClient, "post">;
5
5
  type CampaignMutationClient = Pick<LeadifyClient, "put">;
6
- export declare function registerCampaignTools(server: McpServer, readClient?: CampaignReadClient, writeClient?: CampaignWriteClient, mutationClient?: CampaignMutationClient): void;
6
+ type CampaignDeleteClient = Pick<LeadifyClient, "delete">;
7
+ export declare function registerCampaignTools(server: McpServer, readClient?: CampaignReadClient, writeClient?: CampaignWriteClient, mutationClient?: CampaignMutationClient, deleteClient?: CampaignDeleteClient): void;
7
8
  export {};
@@ -27,7 +27,7 @@ function assertCampaignBelongsToOrganization(data, organizationId) {
27
27
  }
28
28
  return campaign;
29
29
  }
30
- export function registerCampaignTools(server, readClient, writeClient, mutationClient) {
30
+ export function registerCampaignTools(server, readClient, writeClient, mutationClient, deleteClient) {
31
31
  // ── update_campaign_stats ──────────────────────────────────────────────
32
32
  server.tool("update_campaign_stats", "Update email campaign statistics for a lead group. If the given campaignSlug " +
33
33
  "doesn't exist in the group's stats yet, it is created; otherwise provided fields " +
@@ -130,28 +130,50 @@ export function registerCampaignTools(server, readClient, writeClient, mutationC
130
130
  return handleToolError(error);
131
131
  }
132
132
  });
133
- server.tool("update_campaign_configuration", "Edit the lifecycle window of any OPEN campaign; changing channel remains limited to DRAFT. " +
133
+ const registerCampaignUpdateTool = (name, description) => {
134
+ server.tool(name, description, {
135
+ id: z.string().describe("Campaign ID."),
136
+ channel: z.enum(["LINKEDIN", "EMAIL"]).optional().describe("Delivery channel; mutable only while status is DRAFT."),
137
+ start_at: z.string().datetime().nullable().optional().describe("Inclusive UTC start timestamp, or null to remove the lower bound."),
138
+ end_at: z.string().datetime().nullable().optional().describe("Exclusive UTC end timestamp, or null to remove the upper bound. A future value or null can auto-resume only a WINDOW_END pause."),
139
+ timezone: z.string().min(1).optional().describe("IANA timezone used for the business window, for example Europe/Paris."),
140
+ name: z.string().min(1).optional().describe("New campaign name."),
141
+ description: z.string().nullable().optional().describe("New campaign description, or null to clear it."),
142
+ }, async ({ id, channel, start_at, end_at, timezone, name, description }) => {
143
+ try {
144
+ const body = {};
145
+ if (channel !== undefined)
146
+ body.channel = channel;
147
+ if (start_at !== undefined)
148
+ body.startAt = start_at;
149
+ if (end_at !== undefined)
150
+ body.endAt = end_at;
151
+ if (timezone !== undefined)
152
+ body.timezone = timezone;
153
+ if (name !== undefined)
154
+ body.name = name;
155
+ if (description !== undefined)
156
+ body.description = description;
157
+ return toolResult(await (mutationClient ?? getClient()).put(`/api/campaign/${encodeURIComponent(id)}/configuration`, body));
158
+ }
159
+ catch (error) {
160
+ return handleToolError(error);
161
+ }
162
+ });
163
+ };
164
+ const updateCampaignDescription = "Edit the name, description, and lifecycle window of any OPEN campaign; changing channel remains limited to DRAFT. " +
134
165
  "Extending endAt into the future or removing it automatically resumes only a campaign paused " +
135
166
  "because WINDOW_END, after provider validation and immediate replanning. A MANUAL pause never " +
136
167
  "auto-resumes. The requested window remains saved when provider validation fails, and a CLOSED " +
137
- "campaign is terminal.", {
168
+ "campaign is terminal. Persona is inherited from the Lead Group and cannot be changed here.";
169
+ registerCampaignUpdateTool("update_campaign", updateCampaignDescription);
170
+ registerCampaignUpdateTool("update_campaign_configuration", updateCampaignDescription);
171
+ server.tool("delete_campaign", "Irreversibly delete one OPEN campaign that is DRAFT or PAUSED. Active and permanently closed campaigns are refused. This operation never sends outreach. Always confirm with the user before calling this tool.", {
138
172
  id: z.string().describe("Campaign ID."),
139
- channel: z.enum(["LINKEDIN", "EMAIL"]).optional().describe("Delivery channel; mutable only while status is DRAFT."),
140
- start_at: z.string().datetime().nullable().optional().describe("Inclusive UTC start timestamp, or null to remove the lower bound."),
141
- end_at: z.string().datetime().nullable().optional().describe("Exclusive UTC end timestamp, or null to remove the upper bound. A future value or null can auto-resume only a WINDOW_END pause."),
142
- timezone: z.string().min(1).optional().describe("IANA timezone used for the business window, for example Europe/Paris."),
143
- }, async ({ id, channel, start_at, end_at, timezone }) => {
173
+ confirmation: z.literal("delete_campaign").describe("Exact acknowledgement required before deleting the campaign."),
174
+ }, async ({ id }) => {
144
175
  try {
145
- const body = {};
146
- if (channel !== undefined)
147
- body.channel = channel;
148
- if (start_at !== undefined)
149
- body.startAt = start_at;
150
- if (end_at !== undefined)
151
- body.endAt = end_at;
152
- if (timezone !== undefined)
153
- body.timezone = timezone;
154
- return toolResult(await (mutationClient ?? getClient()).put(`/api/campaign/${encodeURIComponent(id)}/configuration`, body));
176
+ return toolResult(await (deleteClient ?? getClient()).delete(`/api/campaign/${encodeURIComponent(id)}`));
155
177
  }
156
178
  catch (error) {
157
179
  return handleToolError(error);
@@ -262,6 +284,51 @@ export function registerCampaignTools(server, readClient, writeClient, mutationC
262
284
  return handleToolError(error);
263
285
  }
264
286
  });
287
+ server.tool("get_campaign_audience", "Read only the explicit audience of a campaign in an explicitly selected organization. " +
288
+ "Returns decision-safe presence signals, never outreach message content, and never sends.", {
289
+ organization_id: z.string().describe("Organization selected explicitly via list_organizations."),
290
+ campaign_id: z.string().describe("Campaign ID selected from list_campaigns or get_campaign."),
291
+ }, async ({ organization_id, campaign_id }) => {
292
+ try {
293
+ return toolResult(await (readClient ?? getClient()).get(`/api/campaign/${encodeURIComponent(campaign_id)}/audience`, new URLSearchParams({ organizationId: organization_id })));
294
+ }
295
+ catch (error) {
296
+ return handleToolError(error);
297
+ }
298
+ });
299
+ server.tool("preview_unenroll_campaign_audience", "Deterministically preview explicit campaign members that can be removed because the campaign-channel message is missing or no delivery channel exists. " +
300
+ "Read-only, no send, and returns the exact lead_ids required by unenroll_campaign_audience.", {
301
+ organization_id: z.string().describe("Organization selected explicitly via list_organizations."),
302
+ campaign_id: z.string().describe("Campaign ID selected from list_campaigns or get_campaign."),
303
+ }, async ({ organization_id, campaign_id }) => {
304
+ try {
305
+ const data = await (readClient ?? getClient()).get(`/api/campaign/${encodeURIComponent(campaign_id)}/audience`, new URLSearchParams({ organizationId: organization_id }));
306
+ const leadIds = Array.isArray(data.removableLeadIds) ? data.removableLeadIds : [];
307
+ return toolResult({ ...data, leadIds, noSend: true, persisted: false });
308
+ }
309
+ catch (error) {
310
+ return handleToolError(error);
311
+ }
312
+ });
313
+ server.tool("unenroll_campaign_audience", "Remove exactly the lead_ids returned by preview_unenroll_campaign_audience. Re-reads the preview first and refuses a stale, partial, or expanded selection. " +
314
+ "This only removes explicit membership; it never sends. An ACTIVE campaign may be replanned by Leadify solely to stop pending jobs.", {
315
+ organization_id: z.string().describe("Organization selected explicitly via list_organizations."),
316
+ campaign_id: z.string().describe("Campaign ID selected from preview_unenroll_campaign_audience."),
317
+ lead_ids: z.array(z.string()).min(1).describe("Exact, complete lead_ids returned by the preview."),
318
+ }, async ({ organization_id, campaign_id, lead_ids }) => {
319
+ try {
320
+ const data = await (readClient ?? getClient()).get(`/api/campaign/${encodeURIComponent(campaign_id)}/audience`, new URLSearchParams({ organizationId: organization_id }));
321
+ const expected = Array.isArray(data.removableLeadIds) ? data.removableLeadIds : [];
322
+ const selected = [...new Set(lead_ids)].sort();
323
+ if (selected.length !== lead_ids.length || JSON.stringify(selected) !== JSON.stringify([...expected].sort())) {
324
+ throw new Error("lead_ids must exactly match the current preview_unenroll_campaign_audience result");
325
+ }
326
+ return toolResult(await (deleteClient ?? getClient()).delete(`/api/campaign/${encodeURIComponent(campaign_id)}/prospects`, { leadIds: lead_ids }));
327
+ }
328
+ catch (error) {
329
+ return handleToolError(error);
330
+ }
331
+ });
265
332
  // ── export_campaign ────────────────────────────────────────────────────
266
333
  server.tool("export_campaign", "Export a campaign's statistics and metrics as CSV. A permanently closed campaign is exported " +
267
334
  "from its immutable final report, so late replies remain visible in current activity without " +
@@ -285,6 +285,22 @@ export function registerLeadTools(server, client = getClient()) {
285
285
  return handleToolError(error);
286
286
  }
287
287
  });
288
+ server.tool("clear_lead_field", "Irreversibly remove one stored value from a lead without changing its lead-group schema. " +
289
+ "Canonical relationship fields require their dedicated relationship mutation. Always confirm with the user before calling this tool.", {
290
+ lead_id: z.string().describe("ID of the lead to change."),
291
+ property_name: z.string().min(1).describe("Stored field name to clear. The field definition remains in the schema."),
292
+ confirmation: z.literal("clear_lead_field").describe("Exact acknowledgement required before removing the stored value."),
293
+ }, async ({ lead_id, property_name }) => {
294
+ try {
295
+ return toolResult(await client.delete("/clear-lead-field", {
296
+ leadId: lead_id,
297
+ propertyName: property_name,
298
+ }));
299
+ }
300
+ catch (error) {
301
+ return handleToolError(error);
302
+ }
303
+ });
288
304
  // ── delete_sequence_messages ──────────────────────────────────────────
289
305
  server.tool("upsert_lead_note", "Create a free-form note in a prospect's Notes tab. Idempotent for the same lead and exact note body: retrying the call returns the original note instead of adding a duplicate.", {
290
306
  lead_id: z.string().describe("ID of the prospect that owns the note."),
@@ -125,7 +125,8 @@ export function registerSchemaTools(server, client) {
125
125
  .array(z.string())
126
126
  .min(1)
127
127
  .describe("Array of column/field names to delete."),
128
- }, async ({ lead_group_id, column_names }) => {
128
+ force: z.literal(true).optional().describe("Required only to remove a rogue data key that is absent from the group schema."),
129
+ }, async ({ lead_group_id, column_names, force }) => {
129
130
  try {
130
131
  const body = {
131
132
  leadGroupId: lead_group_id,
@@ -136,6 +137,8 @@ export function registerSchemaTools(server, client) {
136
137
  else {
137
138
  body.columnNames = column_names;
138
139
  }
140
+ if (force)
141
+ body.force = true;
139
142
  const data = await (client ?? getClient()).post("/delete-column", body);
140
143
  return toolResult(data);
141
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "8.6.9",
3
+ "version": "8.6.11",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",