@agifyai/leadify-mcp 9.7.0 → 9.7.2
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 +157 -0
- 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 +1397 -0
- package/dist/tools/agent_outreach_schemas.js +95 -0
- package/dist/tools/auth.js +15 -2
- package/dist/tools/context_entities.d.ts +70 -70
- package/dist/tools/lead_move.d.ts +64 -64
- package/dist/tools/signals.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -497,3 +497,160 @@ 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.
|
|
646
|
+
|
|
647
|
+
La décision accepte aussi `agent_cycle_id`, facultatif : transmettre uniquement
|
|
648
|
+
l’identifiant réel du cycle courant (1 à 200 caractères, sans espaces ni caractères
|
|
649
|
+
de contrôle). Il devient `agentCycleId` côté serveur ; absent, il reste absent.
|
|
650
|
+
Les lectures des messages conservent `decision.agentCycleId` et, dans chaque
|
|
651
|
+
tentative, `triggerRunId`, `reconciliationRunId` et `reconciliationErrorCode`.
|
|
652
|
+
Une valeur absente ou nulle ne prouve aucune exécution ; ces références ne
|
|
653
|
+
changent pas l’état du message et n’exposent pas l’enveloppe fournisseur brute.
|
|
654
|
+
|
|
655
|
+
Pour un signal daté, `signal_upsert.date_evidence` fournit le passage source qui
|
|
656
|
+
établit `date`. Le serveur conserve cette preuve ; une date sans preuve est refusée.
|
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", "agentCycleId", "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", "triggerRunId", "reconciliationRunId", "reconciliationErrorCode"]);
|
|
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
|
+
}
|