@agifyai/leadify-mcp 9.7.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 CHANGED
@@ -497,3 +497,149 @@ préparent ni n’envoient de message commercial.
497
497
  Pour les essais locaux, `LEADIFY_LOCAL_API_URL=http://localhost:<port>` dirige
498
498
  le MCP vers une origine HTTP locale. Les adresses distantes sont refusées par ce
499
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
+ }