@agifyai/leadify-mcp 9.14.1 → 9.15.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
@@ -277,6 +277,7 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
277
277
  | `get_follow_up_cadence` | Rythme de relance de l'organisation (lecture seule). |
278
278
  | `get_prospect_approach` | Avant d'écrire à une personne : son Approche (quotidien, problématique, ce qu'elle veut, ce qu'on lui apporte). |
279
279
  | `list_outreach_review_queue` | La file « À valider » de Leadify (page `/to-review`) en un appel, en lecture seule : filtres campagne, canal, type de message et état (`TO_REVIEW` par défaut), `total`, `counts`, `truncated`. Le texte des messages seulement avec `include_text`. |
280
+ | `revise_review_queue_message` | Remplacer le texte d'**un** message en `TO_REVIEW`, à la demande explicite du relecteur, au nom de l'agent nommé par `LEADIFY_EXTERNAL_AGENT_NAME` : relecture complète en réponse, version attendue, clé d'idempotence, limites du canal. Le message reste à valider : rien ne valide, ne programme, n'envoie ni ne refuse. Organisation d'Agify seulement. |
280
281
  | `preview_move_leads` | Essai à blanc du déplacement de fiches vers un autre groupe de la même organisation : sélection par identifiants, vue ou filtre (500 fiches au plus par appel, lots ensuite), refus par fiche avec motif, doublons dans le groupe cible, champs de schéma et relations à ajouter, blocages et empreinte du plan. |
281
282
  | `move_leads` | Appliquer un déplacement prévisualisé ; exige l'empreinte de la prévisualisation et une clé d'idempotence. La fiche garde son identifiant et tout son historique ; jamais de fusion. |
282
283
  | `undo_move_leads` / `get_lead_move` | Annuler ou relire une opération de déplacement par son identifiant. |
@@ -502,17 +503,38 @@ de son extrait de 500 caractères.
502
503
  les messages de campagne qui attendent une décision humaine, dans l'ordre de la
503
504
  page, au plus 500, sans pagination. Filtres : `campaign_id`, `channel` (`EMAIL`,
504
505
  `LINKEDIN`), `message_type_id` et `state` (`TO_REVIEW` par défaut, `VALIDATED`,
505
- `CORRECTED`, `REFUSED`, ou `ALL`). Les identifiants des filtres viennent de
506
+ `CORRECTED`, `REWRITTEN`, `REFUSED`, ou `ALL`). Les identifiants des filtres viennent de
506
507
  `filters.campaigns` et `filters.messageTypes`. Chaque message garde l'essentiel
507
508
  (prospect, entreprise, campagne, canal, étape, type, départ prévu, créneau
508
- manqué, statut, objet) ; la révision, la date de création et les identifiants de
509
- campagne et de type, déjà dans `filters`, sont retirés. Sans le texte des
509
+ manqué, statut, objet), sa `version`, et l'auteur de sa dernière modification
510
+ (`lastEditedBy` : `none`, `human` ou `agent`, avec `lastEditedByName`) ; la date de
511
+ création et les identifiants de campagne et de type, déjà dans `filters`, sont
512
+ retirés. `decisions` sépare les messages validés : `validatedAsIs`,
513
+ `validatedAfterAgentRewrite`, `correctedByHuman`. Sans le texte des
510
514
  messages, la file réelle de 73 messages tient en environ 30 000 caractères, sous
511
515
  la limite de 50 000 d'Hermès ; `include_text: true` ajoute `body`, le message
512
516
  entier (environ 500 caractères de plus par message) : au-delà d'une quarantaine
513
517
  de messages, filtrer d'abord. Si `truncated` vaut `true`, filtrer par campagne.
514
- Aucun outil ne valide, corrige ou refuse : cela reste humain. Route :
515
- `GET /api/outreach-review/queue` (permission `read`).
518
+ Route : `GET /api/outreach-review/queue` (permission `read`).
519
+
520
+ `revise_review_queue_message` remplace le texte d'un seul message en `TO_REVIEW`,
521
+ à la demande explicite du relecteur : le corps, et l'objet d'un email. Le message
522
+ reste à valider ; l'état, l'heure de départ, la campagne, le destinataire et le
523
+ canal ne changent pas. Entrées : `organization_id`, `message_id`, `body`,
524
+ `subject` (facultatif), `expected_version`, `idempotency_key`. La réponse est une
525
+ relecture complète : texte enregistré, `version`, auteur `agent`, état et
526
+ `history` (les textes précédents). Chaque écriture crée la version suivante sous
527
+ un **nouvel identifiant** : utiliser `messageId` et `version` de la réponse pour la
528
+ suite. Rejouer la même clé rend le même résultat (`replayed: true`). Refus sans
529
+ écriture : `MESSAGE_NOT_TO_REVIEW`, `MESSAGE_VERSION_STALE`,
530
+ `IDEMPOTENCY_KEY_REUSED`, `EDIT_INVALID` (note d'invitation LinkedIn : 300 caractères
531
+ au plus ; message LinkedIn et email : 1 500 ; objet : 90 ; pas d'objet sur
532
+ LinkedIn), `AGENT_REVISION_ORGANIZATION_FORBIDDEN` (organisation d'Agify seulement).
533
+ Exige `LEADIFY_EXTERNAL_AGENT_NAME`, et une clé avec la permission `write`. Route :
534
+ `POST /api/outreach-review/messages/:id/revise`.
535
+
536
+ Aucun outil ne valide, ne programme, n'envoie ni ne refuse : cela reste humain, dans
537
+ Leadify.
516
538
 
517
539
  ### Déplacer des prospects vers un autre groupe (PRD-2478)
518
540
 
@@ -9,7 +9,7 @@ export const EXTERNAL_MISSION_VERSION = "external-agent.v1";
9
9
  const PROCESS_SESSION = randomUUID().replace(/-/g, "").slice(0, 16);
10
10
  export class ExternalAgentNameMissingError extends Error {
11
11
  constructor() {
12
- super(`${EXTERNAL_AGENT_ENV} n'est pas défini : une écriture de qualification doit être faite au nom d'un agent identifié. ` +
12
+ super(`${EXTERNAL_AGENT_ENV} n'est pas défini : une écriture de qualification ou une réécriture de message doit être faite au nom d'un agent identifié. ` +
13
13
  `Lancez le MCP avec ${EXTERNAL_AGENT_ENV}=<Nom> (par exemple Louise). Les lectures restent disponibles sans ce nom.`);
14
14
  this.name = "ExternalAgentNameMissingError";
15
15
  }
@@ -1,11 +1,11 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { type LeadifyClient } from "../client.js";
3
- export declare const OUTREACH_REVIEW_TOOL_NAMES: readonly ["list_outreach_review_queue"];
4
- export declare const OUTREACH_REVIEW_STATES: readonly ["ALL", "TO_REVIEW", "VALIDATED", "CORRECTED", "REFUSED"];
3
+ export declare const OUTREACH_REVIEW_TOOL_NAMES: readonly ["list_outreach_review_queue", "revise_review_queue_message"];
4
+ export declare const OUTREACH_REVIEW_STATES: readonly ["ALL", "TO_REVIEW", "VALIDATED", "CORRECTED", "REWRITTEN", "REFUSED"];
5
5
  export declare const OUTREACH_REVIEW_CHANNELS: readonly ["EMAIL", "LINKEDIN"];
6
- type Client = Pick<LeadifyClient, "get">;
6
+ type Client = Pick<LeadifyClient, "get" | "post">;
7
7
  /**
8
- * One queue item without what only a write would need (revision, createdAt) and
8
+ * One queue item, with version (the revision a write must name), without createdAt and
9
9
  * without the campaign and message type ids, which filters.campaigns and
10
10
  * filters.messageTypes carry once. The body only with include_text.
11
11
  */
@@ -1,29 +1,32 @@
1
1
  import { z } from "zod";
2
2
  import { getClient } from "../client.js";
3
+ import { ExternalAgentNameMissingError, externalAgentName } from "../correlation.js";
3
4
  import { handleToolError } from "../types.js";
4
- // PRD-2815 — la file « À valider » de Leadify (page /to-review) en lecture seule,
5
- // par la route serveur GET /api/outreach-review/queue. Valider, corriger ou
6
- // refuser reste humain : aucune écriture ici.
7
- export const OUTREACH_REVIEW_TOOL_NAMES = ["list_outreach_review_queue"];
8
- export const OUTREACH_REVIEW_STATES = ["ALL", "TO_REVIEW", "VALIDATED", "CORRECTED", "REFUSED"];
5
+ // PRD-2815 — la file « À valider » de Leadify (page /to-review), par la route serveur
6
+ // GET /api/outreach-review/queue. Une seule écriture : un agent nommé remplace le texte
7
+ // d'un message à valider, par POST /api/outreach-review/messages/:id/revise. Valider,
8
+ // programmer, envoyer ou refuser reste humain : aucun outil ici ne le fait.
9
+ export const OUTREACH_REVIEW_TOOL_NAMES = ["list_outreach_review_queue", "revise_review_queue_message"];
10
+ export const OUTREACH_REVIEW_STATES = ["ALL", "TO_REVIEW", "VALIDATED", "CORRECTED", "REWRITTEN", "REFUSED"];
9
11
  export const OUTREACH_REVIEW_CHANNELS = ["EMAIL", "LINKEDIN"];
10
12
  const id = z.string().trim().min(1).max(200);
11
13
  const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
14
+ const REVISE = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true };
12
15
  const object = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
13
16
  /** One line of JSON: an agent runtime sets aside tool results above 50,000 characters. */
14
17
  const compact = (data) => ({ content: [{ type: "text", text: JSON.stringify(data) }] });
15
18
  /**
16
- * One queue item without what only a write would need (revision, createdAt) and
19
+ * One queue item, with version (the revision a write must name), without createdAt and
17
20
  * without the campaign and message type ids, which filters.campaigns and
18
21
  * filters.messageTypes carry once. The body only with include_text.
19
22
  */
20
23
  export function projectReviewItem(item, includeText) {
21
24
  const i = object(item);
22
25
  const result = {
23
- id: i.id, leadId: i.leadId, leadName: i.leadName, companyName: i.companyName, campaignName: i.campaignName,
26
+ id: i.id, version: i.revision, leadId: i.leadId, leadName: i.leadName, companyName: i.companyName, campaignName: i.campaignName,
24
27
  channel: i.channel, step: i.step, stepKind: i.stepKind, messageTypeName: object(i.messageType).name ?? null,
25
28
  plannedDepartureAt: i.plannedDepartureAt, missedSlot: i.missedSlot, status: i.status, editedByHuman: i.editedByHuman,
26
- subject: i.subject,
29
+ lastEditedBy: i.lastEditedBy, lastEditedByName: i.lastEditedByName, subject: i.subject,
27
30
  };
28
31
  if (includeText)
29
32
  result.body = i.body;
@@ -32,16 +35,19 @@ export function projectReviewItem(item, includeText) {
32
35
  export function registerOutreachReviewTools(server, client = getClient()) {
33
36
  server.tool("list_outreach_review_queue", "The Leadify 'À valider' queue (page /to-review) in ONE call: the campaign messages waiting for a human decision, in " +
34
37
  "the page's order. Read-only: validating, correcting or refusing a message stays human, in Leadify. Answer: " +
35
- "{organizationId, state, limit, total, counts {all, toReview, validated, corrected, refused}, truncated, items, " +
36
- "filters {campaigns [{id, name}], messageTypes [{id, name}]}, notContactedToday {count, reasons}}. Each item: id, " +
38
+ "{organizationId, state, limit, total, counts {all, toReview, validated, corrected, rewritten, refused}, " +
39
+ "decisions {validatedAsIs, validatedAfterAgentRewrite, correctedByHuman}, truncated, items, " +
40
+ "filters {campaigns [{id, name}], messageTypes [{id, name}]}, notContactedToday {count, reasons}}. decisions splits the " +
41
+ "validated messages by who last wrote their text. Each item: id, version (what revise_review_queue_message expects), " +
37
42
  "leadId, leadName, companyName, campaignName, channel, step, stepKind, messageTypeName, plannedDepartureAt, " +
38
- "missedSlot, status (TO_REVIEW, APPROVED, SCHEDULED, SENT, REJECTED), editedByHuman, subject. To stay well under " +
43
+ "missedSlot, status (TO_REVIEW, APPROVED, SCHEDULED, SENT, REJECTED), editedByHuman, lastEditedBy (none, human or agent), " +
44
+ "lastEditedByName (the agent's name), subject. To stay well under " +
39
45
  "50,000 characters, the message text is left out by default (about 30,000 characters for 73 messages); " +
40
46
  "include_text=true adds body, the whole message, about 500 characters more per message (about 68,000 for those 73): " +
41
47
  "above about 40 messages, narrow with campaign_id, channel or message_type_id first. At most 500 messages, no pagination: when truncated " +
42
48
  "is true, filter by campaign_id. Pass the ids found in filters.campaigns and filters.messageTypes.", {
43
49
  organization_id: id.describe("Organization to read. Must be in the API key's scope."),
44
- state: z.enum(OUTREACH_REVIEW_STATES).optional().describe("TO_REVIEW (default): waiting for validation. VALIDATED, CORRECTED, REFUSED: decided in the last 7 days. ALL: TO_REVIEW plus the messages validated, scheduled, sent or refused in the last 7 days."),
50
+ state: z.enum(OUTREACH_REVIEW_STATES).optional().describe("TO_REVIEW (default): waiting for validation. VALIDATED, CORRECTED, REFUSED: decided in the last 7 days. CORRECTED: last written by a human. REWRITTEN: last written by an agent. ALL: TO_REVIEW plus the messages validated, scheduled, sent or refused in the last 7 days."),
45
51
  campaign_id: id.optional().describe("Only this campaign (filters.campaigns[].id)."),
46
52
  channel: z.enum(OUTREACH_REVIEW_CHANNELS).optional().describe("EMAIL or LINKEDIN."),
47
53
  message_type_id: id.optional().describe("Only this message type (filters.messageTypes[].id)."),
@@ -59,4 +65,34 @@ export function registerOutreachReviewTools(server, client = getClient()) {
59
65
  return handleToolError(error);
60
66
  }
61
67
  }));
68
+ server.tool("revise_review_queue_message", "Replace the text of ONE campaign message waiting in the Leadify 'À valider' queue, at the reviewer's explicit request. " +
69
+ "Changes only the body, and the subject of an email. The message stays TO_REVIEW: state, planned departure, campaign, " +
70
+ "recipient and channel never change, and the reviewer still validates it by hand in Leadify. Nothing here validates, " +
71
+ "schedules, sends or refuses. Each write creates the next version under a NEW id: use the returned messageId and version " +
72
+ "for anything after it. The previous text stays in history. Answer, read back after writing: {organizationId, messageId, " +
73
+ "previousMessageId, version, status, campaignId, leadId, channel, step, subject, body, lastEditedBy, lastEditedByName, " +
74
+ "unchanged, replayed, latestMessageId, updatedAt, history [{messageId, version, status, subject, body, lastEditedBy, " +
75
+ "lastEditedByName, createdAt}]}. Replaying the same idempotency_key with the same call returns the same message " +
76
+ "(replayed: true). Refusals, nothing written: MESSAGE_NOT_TO_REVIEW (APPROVED, SCHEDULED, SENT, REJECTED...), " +
77
+ "MESSAGE_VERSION_STALE (reread the queue, latestMessageId gives the current one), IDEMPOTENCY_KEY_REUSED, EDIT_INVALID " +
78
+ "(empty, too long: LinkedIn invitation note 300 characters, LinkedIn message 1500, email 1500 and subject 90; or a subject " +
79
+ "on LinkedIn), AGENT_REVISION_ORGANIZATION_FORBIDDEN. Requires LEADIFY_EXTERNAL_AGENT_NAME: the author is noted as that agent.", {
80
+ organization_id: id.describe("Organization of the message. Must be in the API key's scope."),
81
+ message_id: id.describe("id of the message in list_outreach_review_queue."),
82
+ body: z.string().min(1).max(16000).describe("The whole new message text, not a diff."),
83
+ subject: z.string().min(1).max(600).optional().describe("New subject, email only. Omit it to keep the current subject."),
84
+ expected_version: z.number().int().min(1).describe("version of the message as read in list_outreach_review_queue."),
85
+ idempotency_key: z.string().trim().min(8).max(200).describe("A new unique key per new text; reuse it only to replay the same call after an uncertain result."),
86
+ }, REVISE, (async ({ organization_id, message_id, body, subject, expected_version, idempotency_key }) => {
87
+ try {
88
+ if (!externalAgentName())
89
+ throw new ExternalAgentNameMissingError();
90
+ return compact(await client.post(`/api/outreach-review/messages/${encodeURIComponent(message_id)}/revise`, {
91
+ organizationId: organization_id, body, ...(subject === undefined ? {} : { subject }), expectedVersion: expected_version, idempotencyKey: idempotency_key,
92
+ }));
93
+ }
94
+ catch (error) {
95
+ return handleToolError(error);
96
+ }
97
+ }));
62
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "9.14.1",
3
+ "version": "9.15.0",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",