@agifyai/leadify-mcp 9.6.0 → 9.7.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.
- package/README.md +185 -10
- package/dist/server.js +2 -0
- package/dist/tools/agent_outreach.d.ts +19 -0
- package/dist/tools/agent_outreach.js +88 -0
- package/dist/tools/agent_outreach_schemas.d.ts +1394 -0
- package/dist/tools/agent_outreach_schemas.js +94 -0
- package/dist/tools/auth.js +15 -2
- package/dist/tools/context_entities.d.ts +70 -70
- package/dist/tools/events.d.ts +6 -0
- package/dist/tools/events.js +71 -22
- package/dist/tools/lead_move.d.ts +64 -64
- package/dist/tools/leads.js +8 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,8 +195,12 @@ Conséquences pratiques :
|
|
|
195
195
|
|
|
196
196
|
## Sémantique des événements
|
|
197
197
|
|
|
198
|
-
Une participation
|
|
199
|
-
|
|
198
|
+
Une participation porte sur une entreprise ou sur une personne (PRD-2475,
|
|
199
|
+
serveur PRD-2473). Un `account_id` ou le `lead_id` d’un prospect Entreprise
|
|
200
|
+
écrit, comme avant, sur l’entreprise canonique (`Account`). Le `lead_id` d’un
|
|
201
|
+
prospect Personne, ou un `person_id` (`canonicalIdentity.id` d’un prospect Personne, `kind: PERSON`), écrit une
|
|
202
|
+
participation propre à la personne canonique : elle la suit dans tous ses
|
|
203
|
+
groupes. Les outils d’events d’entreprise fonctionnent comme avant. `get_leads.event_filter` et
|
|
200
204
|
`list_event_participations` ajoutent explicitement `VERIFIED` lorsque le statut
|
|
201
205
|
n’est pas fourni. Pour relire des faits `UNVERIFIED`, `DISPUTED` ou `REFUTED`,
|
|
202
206
|
l’agent doit demander ces statuts explicitement. Une donnée incertaine ou
|
|
@@ -211,6 +215,31 @@ des sélecteurs courants sans effacer son historique. Les champs legacy
|
|
|
211
215
|
restent disponibles pour compatibilité/export, mais ne sont jamais synchronisés
|
|
212
216
|
depuis ce modèle.
|
|
213
217
|
|
|
218
|
+
Événements d’une personne :
|
|
219
|
+
|
|
220
|
+
- Rôles d’une personne : `REGISTERED` (inscrit), `ATTENDED` (présent),
|
|
221
|
+
`INVITED` (invité), `SPEAKER`, `ORGANIZER`, `OTHER`. Les trois premiers sont
|
|
222
|
+
refusés pour une entreprise. Inscrit, présent et invité sont trois faits
|
|
223
|
+
distincts : une inscription ne prouve jamais une présence.
|
|
224
|
+
- Une liste d’inscrits fournie par le client est une preuve valable de
|
|
225
|
+
l’inscription, y compris pour une personne désinscrite ensuite ou inscrite
|
|
226
|
+
plusieurs fois : rôle `REGISTERED` et `client_document { file_name,
|
|
227
|
+
received_on, provided_by }`, sans adresse web. `VERIFIED` exige une adresse
|
|
228
|
+
web ou un `client_document`.
|
|
229
|
+
- Un event de l’employeur n’est jamais une participation de la personne. Lue
|
|
230
|
+
avec le `lead_id` d’une personne, `list_event_participations` rend ses
|
|
231
|
+
participations propres (`origin: PERSON`) puis celles de son employeur
|
|
232
|
+
(`origin: EMPLOYER`). Sans `target`, `person_ids` ni `lead_id` de personne,
|
|
233
|
+
la liste ne rend que les entreprises, comme avant ; `target: PERSON` ou
|
|
234
|
+
`person_ids` lisent les personnes.
|
|
235
|
+
- `get_leads.event_filter.subject` vaut `EMPLOYER` (défaut, comportement
|
|
236
|
+
historique) ou `PERSON` (seules les personnes qui portent elles-mêmes la
|
|
237
|
+
participation, par exemple les inscrits d’un webinar).
|
|
238
|
+
- Les events ont un type facultatif `event_type` : `WEBINAR`, `CONGRESS`,
|
|
239
|
+
`TRADE_SHOW`, `OTHER` (filtre `event_types` dans `list_events`).
|
|
240
|
+
`get_event` rend `participationCounts { companies, persons }`.
|
|
241
|
+
- L’absence d’enregistrement ne prouve rien.
|
|
242
|
+
|
|
214
243
|
Il n’existe plus de score de confiance événementiel : le statut et ses preuves
|
|
215
244
|
portent seuls la validation utile. `Event.description` explique l’audience et
|
|
216
245
|
l’utilité de l’événement ; `activity_summary` explique ce que l’entreprise y
|
|
@@ -244,17 +273,17 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
|
|
|
244
273
|
| `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. |
|
|
245
274
|
| `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. |
|
|
246
275
|
| `undo_move_leads` / `get_lead_move` | Annuler ou relire une opération de déplacement par son identifiant. |
|
|
247
|
-
| `get_leads` | Rechercher et lister des leads avec filtres, recherche et pagination, dont le critère composé `event_filter
|
|
276
|
+
| `get_leads` | Rechercher et lister des leads avec filtres, recherche et pagination, dont le critère composé `event_filter` (`subject` : employeur ou personne). |
|
|
248
277
|
| `get_lead` | Récupérer les détails complets d'un lead par son ID. |
|
|
249
278
|
| `update_lead` | Mettre à jour un ou plusieurs champs d'un lead existant. |
|
|
250
279
|
| `clear_lead_field` | Effacer irréversiblement une valeur stockée, sans modifier le schéma du groupe. Exige la confirmation explicite `clear_lead_field`. |
|
|
251
|
-
| `list_events` | Lister les événements tenant-scoped ; les événements archivés sont masqués par défaut. |
|
|
252
|
-
| `get_event` | Lire un événement
|
|
253
|
-
| `create_event` | Créer un événement
|
|
254
|
-
| `update_event` | Modifier ou archiver sans suppression un événement partagé, avec version attendue. |
|
|
255
|
-
| `list_event_participations` | Lister les participations d’entreprise ;
|
|
256
|
-
| `get_event_participation` | Lire un snapshot courant et son historique ; `VERIFIED` par défaut, statut incertain à demander explicitement. |
|
|
257
|
-
| `record_event_participations` | Valider à blanc ou enregistrer un lot idempotent de 1 à 100 observations/corrections. |
|
|
280
|
+
| `list_events` | Lister les événements tenant-scoped, filtrables par type (`event_types`) ; les événements archivés sont masqués par défaut. |
|
|
281
|
+
| `get_event` | Lire un événement partagé, son type, son état d’archivage et ses nombres d’entreprises et de personnes rattachées. |
|
|
282
|
+
| `create_event` | Créer un événement partagé, avec son type facultatif, et une clé d’idempotence. |
|
|
283
|
+
| `update_event` | Modifier (dont le type) ou archiver sans suppression un événement partagé, avec version attendue. |
|
|
284
|
+
| `list_event_participations` | Lister les participations d’entreprise et de personne, avec leur provenance ; filtres `target`, `person_ids` ; `VERIFIED` par défaut. |
|
|
285
|
+
| `get_event_participation` | Lire un snapshot courant d’entreprise ou de personne et son historique ; `VERIFIED` par défaut, statut incertain à demander explicitement. |
|
|
286
|
+
| `record_event_participations` | Valider à blanc ou enregistrer un lot idempotent de 1 à 100 observations/corrections d’entreprise ou de personne (inscrit, présent, invité, preuve « document du client »). |
|
|
258
287
|
| `preview_reset_sequence_messages` / `execute_reset_sequence_messages` | Prévisualiser puis effacer irréversiblement les messages générés d'un prospect, d'une liste explicite ou d'un groupe explicite. L'exécution exige le digest de preview et une clé d'idempotence. |
|
|
259
288
|
| `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. |
|
|
260
289
|
| `delete_leads` | Supprimer définitivement des leads par leurs IDs. |
|
|
@@ -468,3 +497,149 @@ préparent ni n’envoient de message commercial.
|
|
|
468
497
|
Pour les essais locaux, `LEADIFY_LOCAL_API_URL=http://localhost:<port>` dirige
|
|
469
498
|
le MCP vers une origine HTTP locale. Les adresses distantes sont refusées par ce
|
|
470
499
|
réglage ; sans ce réglage, l’adresse habituelle de Leadify est utilisée.
|
|
500
|
+
|
|
501
|
+
## Prospection autonome : contrat MCP `leadify-outreach-mcp.v1`
|
|
502
|
+
|
|
503
|
+
Ces outils déposent des décisions fraîches dans Leadify. Louise écrit la décision du
|
|
504
|
+
moment pour une personne, avec au plus un message Gmail et un message ou une
|
|
505
|
+
invitation LinkedIn. Aucune séquence ni étape future. Le MCP ne contacte jamais
|
|
506
|
+
Gmail ou Unipile pour envoyer : Leadify contrôle et exécute les dépôts acceptés.
|
|
507
|
+
|
|
508
|
+
`get_mcp_runtime_info` annonce les 16 capacités du paquet et les versions serveur
|
|
509
|
+
attendues. `probe_outreach: true` vérifie en plus le contrat des programmes dans
|
|
510
|
+
l'organisation choisie, sans lire de prospect. `PROGRAM_CONTRACT_VERIFIED` ne
|
|
511
|
+
prouve ni les autres routes ni une autorisation commerciale. Chaque réponse
|
|
512
|
+
versionnée est vérifiée ; une ancienne route absente et un contrat incompatible
|
|
513
|
+
produisent des erreurs distinctes des refus de droits.
|
|
514
|
+
|
|
515
|
+
Chaque appel exige `organization_id`. Leadify vérifie la portée de la clé
|
|
516
|
+
(organisations autorisées, y compris futures si cette portée est configurée), puis
|
|
517
|
+
les autorisations propres aux comptes expéditeurs. Aucune restriction de groupe
|
|
518
|
+
supplémentaire n'est ajoutée à la clé. Le programme fixe les groupes utilisables.
|
|
519
|
+
Les outils ne peuvent pas autoriser un programme, relever un plafond, choisir un
|
|
520
|
+
expéditeur, s'accorder un droit ou reprendre une intervention humaine.
|
|
521
|
+
|
|
522
|
+
Dans la matrice, A signifie `/api/agent-outreach/programs/:programId` et C signifie
|
|
523
|
+
`/api/agent-outreach-context/programs/:programId`. L'organisation figure toujours
|
|
524
|
+
dans la query `organizationId`, y compris pour POST. R signifie lecture avec
|
|
525
|
+
portée organisation et autorisation du compte expéditeur ; W ajoute écriture.
|
|
526
|
+
Les tests de contrat et de transport sont dans `test/agent-outreach.test.mjs` ;
|
|
527
|
+
`test/auth.test.mjs` vérifie le diagnostic de compatibilité.
|
|
528
|
+
|
|
529
|
+
| Capacité | Outil | Permission | Route | Vérification |
|
|
530
|
+
|---|---|---|---|---|
|
|
531
|
+
| Programmes accessibles | `list_outreach_programs` | R | GET /api/agent-outreach/programs | version, organisation, pagination |
|
|
532
|
+
| Règles et comptes | `get_outreach_program_context` | R | GET A | contexte, sans pouvoir administratif |
|
|
533
|
+
| Sélection avant pagination | `query_outreach_prospects` | R | POST A/prospects/query | filtres complets, pages, erreurs |
|
|
534
|
+
| Résumé d'une personne | `get_outreach_prospect_summary` | R | GET A/prospects/:leadId | identité explicite |
|
|
535
|
+
| Historique des contacts | `get_outreach_contact_history` | R | GET A/contacts/:leadId | comptes accessibles, couverture |
|
|
536
|
+
| Contexte frais de rédaction | `get_outreach_prospect_context` | R | GET C/people/:leadId/context | contrat, versions, état prêt ou attente |
|
|
537
|
+
| Fil exact | `get_outreach_thread` | R | GET C/people/:leadId/thread | account_key, thread_id, curseur |
|
|
538
|
+
| Décisions présentes | `submit_outreach_messages` | W | POST A/messages/batch | schéma, texte intact, reprises, lot partiel |
|
|
539
|
+
| Messages de cette clé | `list_outreach_messages` | read + organisation | GET A/messages | pagination, projection sûre |
|
|
540
|
+
| Détail et tentatives | `get_outreach_message` | read + organisation | GET A/messages/:messageId | reçu, preuve fournisseur sans enveloppe brute |
|
|
541
|
+
| Annulation avant départ | `cancel_outreach_message` | read/write + organisation | POST A/messages/:messageId/cancel | révision attendue, TOO_LATE |
|
|
542
|
+
| Événements à traiter | `list_outreach_events` | R | GET C/changes | curseurs, lecture sans acquittement |
|
|
543
|
+
| Résultat du traitement | `acknowledge_outreach_events` | W | POST C/changes/acknowledge | préfixe complet, reprise exacte |
|
|
544
|
+
| Santé de réception | `get_outreach_reception_health` | R | GET A/reception-health | couverture depuis l'inscription |
|
|
545
|
+
| Conversations reçues | `list_outreach_conversations` | R | GET A/conversations | personne facultative, pagination |
|
|
546
|
+
| Messages et prise en main | `get_outreach_conversation` | R | GET A/conversations/:conversationId | curseur décimal, contrôle humain en lecture |
|
|
547
|
+
|
|
548
|
+
Les reçus et messages d'une clé restent consultables après retrait de son
|
|
549
|
+
autorisation sur un expéditeur, tant que la clé et sa portée organisation restent
|
|
550
|
+
valides. La réception est soumise aux autorisations d'expéditeurs ; elle reste
|
|
551
|
+
consultable quand la distribution des envois est arrêtée. Ne pas confondre ces
|
|
552
|
+
lectures avec un droit de reprendre la prospection.
|
|
553
|
+
|
|
554
|
+
### Recherche et contexte
|
|
555
|
+
|
|
556
|
+
Pour une relance après sept jours :
|
|
557
|
+
|
|
558
|
+
```json
|
|
559
|
+
{"organization_id":"org-fictive","program_id":"programme-fictif","recipe":"followup_due","limit":25}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Pour une recherche au 12 octobre 2026 à 12:00 UTC, après 72 heures sans message
|
|
563
|
+
sortant confirmé, sans action en attente ni réponse à traiter :
|
|
564
|
+
|
|
565
|
+
```json
|
|
566
|
+
{
|
|
567
|
+
"organization_id":"org-fictive","program_id":"programme-fictif",
|
|
568
|
+
"filters":{
|
|
569
|
+
"last_confirmed_outbound_before":{"channel":"any","before":"2026-10-09T12:00:00Z"},
|
|
570
|
+
"pending":false,"unknown":false,"reply":"WAITING","excluded":false
|
|
571
|
+
},
|
|
572
|
+
"limit":25
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
La recette `channel_gap` cherche au moins deux messages LinkedIn et zéro message
|
|
577
|
+
Gmail confirmé, sans message Gmail en attente ou inconnu. Les invitations sont
|
|
578
|
+
comptées séparément. `replies_to_handle` sélectionne les réponses à traiter dans
|
|
579
|
+
leur propre fil. `filters.signal_filter` accepte `detected_since`,
|
|
580
|
+
`detected_before`, `levels` (`STRONG`, `GOLDEN`, `CRITICAL`) et `scope`
|
|
581
|
+
(`PERSON`, `COMPANY`, `BOTH`). L'absence d'historique connu n'est pas la preuve
|
|
582
|
+
qu'une personne n'a jamais été contactée.
|
|
583
|
+
|
|
584
|
+
La sélection expire après dix minutes. Passer son `nextCursor` dans `cursor` avec
|
|
585
|
+
les mêmes filtres, recette et limite. Un curseur expiré, modifié ou lié à d'autres
|
|
586
|
+
droits est refusé ; aucun redémarrage caché n'a lieu. Programmes, messages et
|
|
587
|
+
conversations utilisent `after_id` pour leur page suivante. Les événements et les
|
|
588
|
+
fils utilisent `cursor`. Les curseurs sont opaques et ne doivent pas être inventés.
|
|
589
|
+
|
|
590
|
+
`get_outreach_prospect_summary` est un résumé. Avant d'écrire, lire
|
|
591
|
+
`get_outreach_prospect_context`, vérifier `readiness.ready`, puis les fils exacts
|
|
592
|
+
nécessaires avec `get_outreach_thread`. Un corps indisponible ou un simple extrait
|
|
593
|
+
ne vaut pas une conversation complète. Le contexte n'effectue aucune qualification.
|
|
594
|
+
|
|
595
|
+
### Dépôt et reprise
|
|
596
|
+
|
|
597
|
+
`submit_outreach_messages` accepte `decisions` (1 à 20). Chaque décision contient
|
|
598
|
+
`request_key`, `lead_id`, `person_id`, `purpose` (`FIRST_CONTACT`, `FOLLOW_UP`,
|
|
599
|
+
`REPLY`), `reason`, `sources` (label et url), `authorization_revision`, `context`
|
|
600
|
+
et `messages` (1 à 2, un par canal).
|
|
601
|
+
|
|
602
|
+
Le contexte transmis comporte `contract_version: "agent-writing-context.v1"`,
|
|
603
|
+
`context_version`, `facts_version`, `read_at` et l'objet `versions` exact reçu du
|
|
604
|
+
serveur. Les clés de cet objet restent inchangées, dont `contactState`. Un message
|
|
605
|
+
contient `channel`, `action`, `sender_id`, `account_key`, `recipient`, `subject`,
|
|
606
|
+
`body`, `thread_id`, `expected_inbound_id`, `technical_parts` (signature et
|
|
607
|
+
unsubscribe). Les champs nullables doivent être présents. Une réponse désigne
|
|
608
|
+
son fil et son message reçu. Gmail exige un sujet ; LinkedIn exige un sujet null
|
|
609
|
+
et des parties techniques vides. Le texte écrit est transmis sans modification.
|
|
610
|
+
|
|
611
|
+
Reprendre un appel interrompu avec **la même request_key et le même contenu**.
|
|
612
|
+
Une clé réutilisée avec un contenu différent est refusée. Chaque reçu du lot doit
|
|
613
|
+
être examiné : `ACCEPTED` signifie enregistré, pas envoyé ; `WAIT` et `REFUSED`
|
|
614
|
+
conservent leurs raisons. Un message `UNKNOWN` doit être réconcilié depuis les
|
|
615
|
+
preuves fournisseur ; ne jamais le redéposer comme une nouvelle tentative.
|
|
616
|
+
`cancel_outreach_message` demande la dernière `expected_revision` ; `TOO_LATE`
|
|
617
|
+
signifie que l'annulation n'a pas eu lieu.
|
|
618
|
+
|
|
619
|
+
Le journal `list_outreach_events` renvoie `changes`, `cursor`, `nextCursor` et
|
|
620
|
+
`processedCursor`. Lire ne marque rien comme traité. Après traitement de chaque
|
|
621
|
+
changement du préfixe, `acknowledge_outreach_events` reçoit le `cursor` et les
|
|
622
|
+
`receipts` (change_id, status PROCESSED/DISMISSED, reason). Une reprise exige les
|
|
623
|
+
mêmes résultats. Ce journal est distinct de l'historique administratif.
|
|
624
|
+
|
|
625
|
+
### Catalogue conservé et preuve locale
|
|
626
|
+
|
|
627
|
+
Les 141 outils de la version 9.7.0 restent disponibles : fiches et groupes,
|
|
628
|
+
dossiers, Company/Person/emplois, signaux, exclusions, Context Workspace et
|
|
629
|
+
lectures commerciales (`email_read_thread`, `linkedin_read_conversation`,
|
|
630
|
+
`unipile_read_messages`, `leadify_read_activity_feed`, `get_commercial_truth`).
|
|
631
|
+
`test/catalogue-9.7.0.json` vérifie leur conservation exacte dans une vraie
|
|
632
|
+
réponse `tools/list`. Les outils de campagne et de qualification existants ne
|
|
633
|
+
font pas partie du parcours autonome et ne sont pas appelés par ces nouveaux outils.
|
|
634
|
+
|
|
635
|
+
Pour tester le MCP avec Leadify local, lancer serveur et web avec les données
|
|
636
|
+
fictives du projet, puis démarrer un nouveau processus MCP avec
|
|
637
|
+
`LEADIFY_LOCAL_API_URL=http://127.0.0.1:<port>` et une clé locale fictive.
|
|
638
|
+
Sans cette variable, le client vise l'API publique : ne pas utiliser une vraie clé
|
|
639
|
+
pour la recette. `npm test` couvre les schémas, erreurs, projections et un client
|
|
640
|
+
MCP stdio relié à un serveur HTTP simulé local ; cela ne remplace pas la recette
|
|
641
|
+
avec l'application, ses écritures enregistrées et sa vérification visuelle.
|
|
642
|
+
|
|
643
|
+
La publication suit `publish.yml` après fusion : registre npm, archive de version
|
|
644
|
+
explicite et découverte dans un processus neuf doivent être vérifiés séparément.
|
|
645
|
+
Une conversation Louise déjà ouverte peut conserver un ancien catalogue.
|
package/dist/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerAgentOutreachTools } from "./tools/agent_outreach.js";
|
|
1
2
|
import { registerQualificationRunTools } from "./tools/qualification_runs.js";
|
|
2
3
|
import { registerOutreachExclusionTools } from "./tools/outreach_exclusion.js";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -33,6 +34,7 @@ export function createServer() {
|
|
|
33
34
|
version: MCP_VERSION,
|
|
34
35
|
});
|
|
35
36
|
registerAuthTools(server);
|
|
37
|
+
registerAgentOutreachTools(server);
|
|
36
38
|
registerOrganizationTools(server);
|
|
37
39
|
registerLeadGroupTools(server);
|
|
38
40
|
registerLeadGroupFolderTools(server);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type LeadifyClient } from "../client.js";
|
|
3
|
+
import { type ToolResult } from "../types.js";
|
|
4
|
+
export declare const OUTREACH_MCP_CONTRACT = "leadify-outreach-mcp.v1";
|
|
5
|
+
export declare const OUTREACH_TOOL_NAMES: readonly ["list_outreach_programs", "get_outreach_program_context", "query_outreach_prospects", "get_outreach_prospect_summary", "get_outreach_contact_history", "get_outreach_prospect_context", "get_outreach_thread", "submit_outreach_messages", "list_outreach_messages", "get_outreach_message", "cancel_outreach_message", "list_outreach_events", "acknowledge_outreach_events", "get_outreach_reception_health", "list_outreach_conversations", "get_outreach_conversation"];
|
|
6
|
+
export declare const OUTREACH_SERVER_CONTRACTS: {
|
|
7
|
+
readonly program: "1.0.0";
|
|
8
|
+
readonly selection: "1.0.0";
|
|
9
|
+
readonly context: "agent-writing-context.v1";
|
|
10
|
+
readonly receipt: "agent-message-receipt.v1";
|
|
11
|
+
readonly journal: "1.0.0";
|
|
12
|
+
};
|
|
13
|
+
type Client = Pick<LeadifyClient, "get" | "post">;
|
|
14
|
+
export declare function outreachError(error: unknown): ToolResult;
|
|
15
|
+
export declare function requireOutreachVersion(data: unknown, expected: string): unknown;
|
|
16
|
+
/** Provider envelopes/MIME and context snapshots are not evidence needed by the caller. */
|
|
17
|
+
export declare function projectOutreachMessage(data: unknown): Record<string, unknown>;
|
|
18
|
+
export declare function registerAgentOutreachTools(server: McpServer, client?: Client): void;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getClient } from "../client.js";
|
|
3
|
+
import { LeadifyApiError, toolResult, toolError, handleToolError } from "../types.js";
|
|
4
|
+
import { identifier, scope, querySchema, queryBody, submitSchema, decisionBody, acknowledgeSchema } from "./agent_outreach_schemas.js";
|
|
5
|
+
export const OUTREACH_MCP_CONTRACT = "leadify-outreach-mcp.v1";
|
|
6
|
+
export const OUTREACH_TOOL_NAMES = ["list_outreach_programs", "get_outreach_program_context", "query_outreach_prospects",
|
|
7
|
+
"get_outreach_prospect_summary", "get_outreach_contact_history", "get_outreach_prospect_context", "get_outreach_thread",
|
|
8
|
+
"submit_outreach_messages", "list_outreach_messages", "get_outreach_message", "cancel_outreach_message", "list_outreach_events",
|
|
9
|
+
"acknowledge_outreach_events", "get_outreach_reception_health", "list_outreach_conversations", "get_outreach_conversation"];
|
|
10
|
+
export const OUTREACH_SERVER_CONTRACTS = { program: "1.0.0", selection: "1.0.0", context: "agent-writing-context.v1", receipt: "agent-message-receipt.v1", journal: "1.0.0" };
|
|
11
|
+
const object = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
12
|
+
const pick = (v, keys) => Object.fromEntries(keys.filter(k => k in object(v)).map(k => [k, object(v)[k]]));
|
|
13
|
+
const encoded = encodeURIComponent;
|
|
14
|
+
const programPath = (id, context = false) => `/api/agent-outreach${context ? "-context" : ""}/programs/${encoded(id)}`;
|
|
15
|
+
function params(org, extra = {}) {
|
|
16
|
+
return new URLSearchParams(Object.entries({ organizationId: org, ...extra }).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)]));
|
|
17
|
+
}
|
|
18
|
+
const postPath = (path, org) => `${path}?${params(org)}`;
|
|
19
|
+
const readNotice = " Explicit organization required; access is checked by Leadify against the key's organization scope and sender grants. Discovery grants no administrative authority. No qualification launch, campaign change or direct provider send.";
|
|
20
|
+
const receiptNotice = " ACCEPTED means stored, not sent. UNKNOWN must be reconciled, never retried as a new send.";
|
|
21
|
+
export function outreachError(error) {
|
|
22
|
+
if (error instanceof z.ZodError)
|
|
23
|
+
return toolError("Vérifiez les paramètres de prospection.", { code: "INVALID_INPUT", issues: error.issues });
|
|
24
|
+
if (error instanceof LeadifyApiError) {
|
|
25
|
+
const code = object(error.responseBody).code;
|
|
26
|
+
if ((error.statusCode === 404 && !code) || code === "HUMAN_AUTHORITY_REQUIRED") {
|
|
27
|
+
return toolError("Cette version de Leadify ne propose pas cet outil de prospection. Vérifiez la version du serveur.", { code: "OUTREACH_TOOL_UNAVAILABLE", statusCode: error.statusCode });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return handleToolError(error);
|
|
31
|
+
}
|
|
32
|
+
export function requireOutreachVersion(data, expected) {
|
|
33
|
+
if (object(data).contractVersion !== expected)
|
|
34
|
+
throw new LeadifyApiError(409, { code: "OUTREACH_CONTRACT_INCOMPATIBLE",
|
|
35
|
+
error: "Le contrat de prospection du serveur est incompatible. Vérifiez sa version avant de continuer.", expected, received: object(data).contractVersion ?? null });
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
/** Provider envelopes/MIME and context snapshots are not evidence needed by the caller. */
|
|
39
|
+
export function projectOutreachMessage(data) {
|
|
40
|
+
const m = object(data);
|
|
41
|
+
const result = pick(m, ["id", "organizationId", "programId", "decisionId", "senderId", "conversationId", "channel", "action", "recipient", "subject", "body", "expectedInboundId", "providerThreadId", "accountKey", "technicalParts", "state", "revision", "reasonCode", "expiresAt", "notBefore", "sentAt", "providerMessageId", "createdAt", "updatedAt"]);
|
|
42
|
+
if (m.decision)
|
|
43
|
+
result.decision = pick(m.decision, ["id", "personId", "leadId", "requestKey", "purpose", "reason", "sources", "contextVersion", "authorizationRevision", "contextReadAt", "receipt", "createdAt"]);
|
|
44
|
+
if (Array.isArray(m.attempts))
|
|
45
|
+
result.attempts = m.attempts.map(a => {
|
|
46
|
+
const attempt = pick(a, ["id", "ordinal", "startedAt", "providerBarrierAt", "finishedAt", "outcome", "providerMessageId", "errorCode", "lastReconciledAt", "reconciliationResult"]);
|
|
47
|
+
const receipt = object(object(object(a).providerRequest).receipt);
|
|
48
|
+
const safeReceipt = Object.fromEntries([["providerMessageId", receipt.id], ["providerThreadId", receipt.threadId]].filter(([, value]) => typeof value === "string"));
|
|
49
|
+
if (Object.keys(safeReceipt).length)
|
|
50
|
+
attempt.providerReceipt = safeReceipt;
|
|
51
|
+
return attempt;
|
|
52
|
+
});
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
export function registerAgentOutreachTools(server, client = getClient()) {
|
|
56
|
+
function register(name, description, schema, action, write = false) {
|
|
57
|
+
server.registerTool(name, { description: description + readNotice, inputSchema: schema,
|
|
58
|
+
annotations: { readOnlyHint: !write && name !== "query_outreach_prospects", destructiveHint: false, idempotentHint: name !== "query_outreach_prospects", openWorldHint: true } }, (async (input) => { try {
|
|
59
|
+
return toolResult(await action(schema.parse(input)));
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return outreachError(e);
|
|
63
|
+
} }));
|
|
64
|
+
}
|
|
65
|
+
register("list_outreach_programs", "List compact accessible autonomous outreach programs and the server contract version. Pagination: pass nextCursor unchanged as after_id.", z.object({ organization_id: identifier, after_id: identifier.optional(), limit: z.number().int().min(1).max(50).default(25) }).strict(), async (v) => requireOutreachVersion(await client.get("/api/agent-outreach/programs", params(v.organization_id, { afterId: v.after_id, limit: v.limit })), "1.0.0"));
|
|
66
|
+
register("get_outreach_program_context", "Read one program's rules, authorization revision and permitted sender identities. Cannot authorize, resume, raise caps, grant rights or change senders.", z.object(scope).strict(), async (v) => requireOutreachVersion(await client.get(programPath(v.program_id), params(v.organization_id)), "1.0.0"));
|
|
67
|
+
register("query_outreach_prospects", "Select canonical people server-side before pagination. Recipes: followup_due = seven days without pending/unknown/reply; channel_gap = LinkedIn messages >=2 and Gmail=0; replies_to_handle. For 72 hours set filters.last_confirmed_outbound_before.before to an explicit ISO timestamp. Signal filters support detection dates, levels and person/company scope. Keep cursor, filters and limit identical on continuation; selections expire after ten minutes. An incomplete history never proves no previous contact.", querySchema, async (v) => requireOutreachVersion(await client.post(postPath(`${programPath(v.program_id)}/prospects/query`, v.organization_id), queryBody(v)), "1.0.0"));
|
|
68
|
+
const personSchema = z.object({ ...scope, lead_id: identifier }).strict();
|
|
69
|
+
register("get_outreach_prospect_summary", "Read a selected person's compact identity, contact counts and selection reasons. This is NOT the writing context: use get_outreach_prospect_context before drafting.", personSchema, v => client.get(`${programPath(v.program_id)}/prospects/${encoded(v.lead_id)}`, params(v.organization_id)));
|
|
70
|
+
register("get_outreach_contact_history", "Read confirmed contact evidence for one person across accessible accounts, including pending, unknown and manual activity. Coverage limits remain explicit.", personSchema, v => client.get(`${programPath(v.program_id)}/contacts/${encoded(v.lead_id)}`, params(v.organization_id)));
|
|
71
|
+
register("get_outreach_prospect_context", "Read fresh writing context, full-thread readiness, identity, qualification, signals, exclusion, authorizations and Context Workspace. Draft only when readiness.ready=true; WAIT requires resolving the stated reasons. Copy all versions, factsVersion, contextVersion and readAt into the current decision. Never starts qualification. A human takeover can only be resumed by a human in Leadify.", personSchema, async (v) => requireOutreachVersion(await client.get(`${programPath(v.program_id, true)}/people/${encoded(v.lead_id)}/context`, params(v.organization_id)), "agent-writing-context.v1"));
|
|
72
|
+
register("get_outreach_thread", "Read one exact person's account/thread with complete bodies or explicit unavailable coverage. Pass nextCursor unchanged as cursor. An excerpt is not a complete conversation.", z.object({ ...scope, lead_id: identifier, account_key: z.string().min(1).max(1000), thread_id: identifier, cursor: z.string().min(1).max(8000).optional(), limit: z.number().int().min(1).max(100).default(50) }).strict(), v => client.get(`${programPath(v.program_id, true)}/people/${encoded(v.lead_id)}/thread`, params(v.organization_id, { accountKey: v.account_key, threadId: v.thread_id, cursor: v.cursor, limit: v.limit })));
|
|
73
|
+
register("submit_outreach_messages", "Deposit 1–20 fresh decisions to Leadify for now, at most one message per channel per person/decision, never a sequence or scheduled steps. Preserve authored text exactly. A reply requires thread_id and expected_inbound_id. Use the SAME request_key and identical payload after interruption; a changed payload needs a new decision after rereading context. Inspect EVERY receipt: a successful call may contain ACCEPTED, WAIT and REFUSED decisions." + receiptNotice, submitSchema, async (v) => requireOutreachVersion(await client.post(postPath(`${programPath(v.program_id)}/messages/batch`, v.organization_id), { decisions: v.decisions.map(decisionBody) }), "agent-message-receipt.v1"), true);
|
|
74
|
+
register("list_outreach_messages", "List this key's deposited messages and decision receipts, including after sender grant revocation. Pagination uses nextCursor as after_id." + receiptNotice, z.object({ ...scope, after_id: identifier.optional(), limit: z.number().int().min(1).max(50).default(25) }).strict(), async (v) => {
|
|
75
|
+
const data = object(await client.get(`${programPath(v.program_id)}/messages`, params(v.organization_id, { afterId: v.after_id, limit: v.limit })));
|
|
76
|
+
return { ...data, messages: Array.isArray(data.messages) ? data.messages.map(projectOutreachMessage) : data.messages };
|
|
77
|
+
});
|
|
78
|
+
register("get_outreach_message", "Read one deposited message, its decision receipt and safe provider attempt evidence. Raw provider envelopes and context snapshots are omitted." + receiptNotice, z.object({ ...scope, message_id: identifier }).strict(), async (v) => projectOutreachMessage(await client.get(`${programPath(v.program_id)}/messages/${encoded(v.message_id)}`, params(v.organization_id))));
|
|
79
|
+
register("cancel_outreach_message", "Cancel this key's message only before dispatch starts, with expected_revision from its latest read. SENDING, UNKNOWN and SENT return TOO_LATE; never treat that as cancellation.", z.object({ ...scope, message_id: identifier, expected_revision: z.number().int().nonnegative() }).strict(), async (v) => {
|
|
80
|
+
const data = object(await client.post(postPath(`${programPath(v.program_id)}/messages/${encoded(v.message_id)}/cancel`, v.organization_id), { expectedRevision: v.expected_revision }));
|
|
81
|
+
return { ...data, ...(data.message ? { message: projectOutreachMessage(data.message) } : {}) };
|
|
82
|
+
}, true);
|
|
83
|
+
register("list_outreach_events", "Read the durable consumer change journal, not the administrative audit log. Reading never advances processedCursor. Keep cursor/nextCursor unchanged; acknowledge every visible change through the chosen cursor only after handling it. Omit cursor to resume from this key's acknowledged position.", z.object({ ...scope, cursor: z.string().min(1).max(2000).optional(), limit: z.number().int().min(1).max(100).default(50) }).strict(), async (v) => requireOutreachVersion(await client.get(`${programPath(v.program_id, true)}/changes`, params(v.organization_id, { cursor: v.cursor, limit: v.limit })), "1.0.0"));
|
|
84
|
+
register("acknowledge_outreach_events", "Record PROCESSED or DISMISSED with a reason for every event in the complete visible prefix through cursor. This advances only this key's checkpoint. Retry the identical cursor and receipts after interruption; changed outcomes conflict. Never acknowledge an unhandled event.", acknowledgeSchema, async (v) => requireOutreachVersion(await client.post(postPath(`${programPath(v.program_id, true)}/changes/acknowledge`, v.organization_id), { cursor: v.cursor, receipts: v.receipts.map(r => ({ changeId: r.change_id, status: r.status, reason: r.reason })) }), "1.0.0"), true);
|
|
85
|
+
register("get_outreach_reception_health", "Read reception coverage and freshness for granted senders even while dispatch is off. READY covers only since program registration, not the whole historical mailbox.", z.object(scope).strict(), v => client.get(`${programPath(v.program_id)}/reception-health`, params(v.organization_id)));
|
|
86
|
+
register("list_outreach_conversations", "List received conversations for granted senders, optionally one person_id. Pagination uses nextCursor as after_id. This does not mark replies as handled.", z.object({ ...scope, after_id: identifier.optional(), person_id: identifier.optional(), limit: z.number().int().min(1).max(100).default(50) }).strict(), v => client.get(`${programPath(v.program_id)}/conversations`, params(v.organization_id, { afterId: v.after_id, personId: v.person_id, limit: v.limit })));
|
|
87
|
+
register("get_outreach_conversation", "Read persisted reception messages and human takeover state. Pass decimal nextCursor unchanged as after_id. This tool cannot take over or resume automation; human control belongs to Leadify.", z.object({ ...scope, conversation_id: identifier, after_id: z.string().regex(/^[0-9]{1,18}$/).optional(), limit: z.number().int().min(1).max(100).default(50) }).strict(), v => client.get(`${programPath(v.program_id)}/conversations/${encoded(v.conversation_id)}`, params(v.organization_id, { afterId: v.after_id, limit: v.limit })));
|
|
88
|
+
}
|