@agifyai/leadify-mcp 9.2.0 → 9.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -234,6 +234,9 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
234
234
  | `add_leads` | Import plat générique vers un groupe : ne crée aucune relation d’emploi, aucun rattachement parent ni identité canonique. Ne jamais l’utiliser pour rattacher une Personne à une Company (voir `upsert_person_leads_with_employment`). |
235
235
  | `upsert_person_leads_with_employment` | Créer ou réconcilier atomiquement des Person Leads, PERSON, EMPLOYED_BY, projections et historique, sans effet commercial. |
236
236
  | `create_person_employment_evidence` / `get_person_employment_evidence` | Créer puis relire une preuve d’emploi individuelle tenant-scopée ; transmettre l’`evidence.id` retourné tel quel à l’upsert Person. |
237
+ | `exclude_account` | Exclure une entreprise (établissement) et toutes ses personnes liées : cause protégée `ACCOUNT_EXCLUDED`, motif et URL source obligatoires, `level: "establishment"`. |
238
+ | `exclude_lead` / `reintegrate_lead` | Exclure ou réintégrer une personne avec les mêmes commandes que l'interface ; raison obligatoire, aucun motif protégé levé. |
239
+ | `get_outreach_state` | Lire causes actives, version et date de 1 à 100 fiches. |
237
240
  | `get_leads` | Rechercher et lister des leads avec filtres, recherche et pagination, dont le critère composé `event_filter`. |
238
241
  | `get_lead` | Récupérer les détails complets d'un lead par son ID. |
239
242
  | `update_lead` | Mettre à jour un ou plusieurs champs d'un lead existant. |
@@ -375,6 +378,30 @@ Aucune mutation de sourcing ne qualifie le lead, n’enrichit les contacts
375
378
  aucun canal externe : le readback porte `noSend: true` et
376
379
  `externalActivation: 0`.
377
380
 
381
+ ### Domaine d'une organisation existante (PRD-2401)
382
+
383
+ `update_organization(organization_id, domain)` renseigne le domaine d'une organisation
384
+ créée sans domaine, pour que `list_organizations(domain)` la retrouve. Il exige le droit
385
+ d'écriture et l'administration de l'organisation ; il ne remplace jamais un domaine déjà
386
+ posé et refuse un domaine pris par une autre organisation (409). `create_organization`
387
+ renvoie `warnings` (`SAME_NAME_WITHOUT_DOMAIN`) quand une organisation accessible de même
388
+ nom existe sans domaine : la compléter plutôt que garder un doublon.
389
+
390
+ ### Exclusions de prospection (PRD-2404)
391
+
392
+ Une exclusion ne s'écrit jamais en texte (Context Workspace, notes, persona) ni dans les
393
+ champs `excluded`, `hide_from_campaign` ou `outreach_status` : le serveur refuse
394
+ ces champs (409 `PROTECTED_OUTREACH_FIELD`). Utiliser `exclude_account` pour un
395
+ client existant ou un compte à ne pas contacter : motif et `source_url` publique
396
+ obligatoires, les personnes liées sont exclues tant que le lien existe et leurs
397
+ campagnes s'arrêtent. Leadify ne distingue pas les services : la réponse dit
398
+ toujours `level: "establishment"`. `exclude_lead` et `reintegrate_lead` agissent
399
+ sur une personne ; la réintégration ne lève jamais une opposition, une protection
400
+ de délivrabilité ni l'exclusion d'une entreprise. Chaque écriture accepte
401
+ `idempotency_key` et `expected_version`, et rend l'état relu ;
402
+ `get_outreach_state` relit 1 à 100 fiches. Routes serveur : `/api/outreach/*`
403
+ (PRD-2337), auteur tracé dans Leadify : propriétaire de la clé, source Agent.
404
+
378
405
  ### Création d’organisation et qualification (PRD-2398)
379
406
 
380
407
  Dans Leadify, **Réglages → Clés API → Modifier** permet de changer les actions
package/dist/server.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { registerQualificationRunTools } from "./tools/qualification_runs.js";
2
+ import { registerOutreachExclusionTools } from "./tools/outreach_exclusion.js";
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { registerAuthTools } from "./tools/auth.js";
4
5
  import { registerLeadTools } from "./tools/leads.js";
@@ -53,5 +54,6 @@ export function createServer() {
53
54
  registerPersonaTools(server);
54
55
  registerPipelineTools(server);
55
56
  registerQualificationRunTools(server);
57
+ registerOutreachExclusionTools(server);
56
58
  return server;
57
59
  }
@@ -3,4 +3,4 @@ import { type LeadifyClient } from "../client.js";
3
3
  export declare function assertLeadGroupsBelongToOrganization(data: unknown, organizationId: string): Array<Record<string, unknown>>;
4
4
  /** Keep group reads navigable: never return schema, profiles, or lead data. */
5
5
  export declare function compactLeadGroup(group: unknown): Record<string, unknown>;
6
- export declare function registerOrganizationTools(server: McpServer, client?: Pick<LeadifyClient, "get" | "post">): void;
6
+ export declare function registerOrganizationTools(server: McpServer, client?: Pick<LeadifyClient, "get" | "post" | "put">): void;
@@ -48,7 +48,9 @@ export function registerOrganizationTools(server, client = getClient()) {
48
48
  server.tool("create_organization", "Create an organization through Leadify using an explicitly authorized API key. The caller becomes its " +
49
49
  "administrator. Repeating the same normalized domain returns the existing organization with created=false. " +
50
50
  "The server grants this key access to the new organization without removing its existing bindings. " +
51
- "This does not create a Context Workspace or start qualification. Manage key permissions in Leadify settings.", { name: z.string().trim().min(1).max(200), domain: z.string().trim().min(1),
51
+ "This does not create a Context Workspace or start qualification. Manage key permissions in Leadify settings. " +
52
+ "The result may carry `warnings` (code SAME_NAME_WITHOUT_DOMAIN): an accessible organization of the same name " +
53
+ "already exists without a domain. Complete it with update_organization instead of keeping a duplicate.", { name: z.string().trim().min(1).max(200), domain: z.string().trim().min(1),
52
54
  external_ref: z.string().trim().min(1).max(200).optional() }, async ({ name, domain, external_ref }) => {
53
55
  try {
54
56
  return toolResult(await client.post("/api/organizations", { name, domain, externalRef: external_ref }));
@@ -57,6 +59,19 @@ export function registerOrganizationTools(server, client = getClient()) {
57
59
  return handleToolError(error);
58
60
  }
59
61
  });
62
+ server.tool("update_organization", "Set the domain of an EXISTING organization that has none (created before domains existed), so " +
63
+ "list_organizations(domain) finds it and create_organization stops duplicating it. Needs write permission and " +
64
+ "administration of that organization. The domain is normalized (no URL). Repeating the same domain changes " +
65
+ "nothing (changed=false). It never replaces a domain already set (409) and refuses a domain used by another " +
66
+ "organization (409). Verify afterwards with list_organizations(domain).", { organization_id: z.string().trim().min(1).describe("Organization ID from list_organizations."),
67
+ domain: z.string().trim().min(1).describe("Domain like exemple.fr, without page address.") }, async ({ organization_id, domain }) => {
68
+ try {
69
+ return toolResult(await client.put(`/api/organizations/${encodeURIComponent(organization_id)}/domain`, { domain }));
70
+ }
71
+ catch (error) {
72
+ return handleToolError(error);
73
+ }
74
+ });
60
75
  // ── list_organizations ─────────────────────────────────────────────────
61
76
  server.tool("list_organizations", "List every organization accessible to the caller, returning each with id, name, and " +
62
77
  "optional slug. Use this as the starting point when you need an organization_id — " +
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type LeadifyClient } from "../client.js";
3
+ export declare function registerOutreachExclusionTools(server: McpServer, client?: Pick<LeadifyClient, "post">): void;
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ import { getClient } from "../client.js";
3
+ import { toolResult, handleToolError } from "../types.js";
4
+ // PRD-2404 — vraies exclusions de prospection, branchées sur /api/outreach
5
+ // (PRD-2337) : mêmes commandes que les boutons de l'interface Leadify.
6
+ const NEVER_IN_TEXT = "Never write an exclusion as text (Context Workspace, company brain, notes, persona) and never write excluded, " +
7
+ "hide_from_campaign or outreach_status on a lead: text excludes nothing and Leadify refuses those fields (409 PROTECTED_OUTREACH_FIELD).";
8
+ const id = z.string().trim().min(1).max(200);
9
+ const writeShape = {
10
+ organization_id: id.describe("Organization that owns the lead. Must be in the API key's scope."),
11
+ idempotency_key: z.string().trim().min(8).max(200).optional().describe("Stable key for this decision. Retrying with the same key replays the first result; reusing it for another lead or reason returns 409 OUTREACH_IDEMPOTENCY_KEY_REUSED."),
12
+ expected_version: z.number().int().min(0).optional().describe("state.version read with get_outreach_state. If the state changed meanwhile, the server refuses with 409 OUTREACH_VERSION_CONFLICT: re-read, then decide again."),
13
+ };
14
+ const reason = z.string().trim().min(3).max(2000);
15
+ export function registerOutreachExclusionTools(server, client = getClient()) {
16
+ server.tool("exclude_account", "Exclude a company (a hospital, clinic or other establishment) from all prospecting: sets the protected cause " +
17
+ "ACCOUNT_EXCLUDED on the company lead, and every person linked to it is excluded too (now and when linked later) and " +
18
+ "their running campaigns stop. Use it for existing customers, competitors or accounts the client must not contact. " +
19
+ "reason and source_url (the public page proving it) are required. Leadify has no department level: the response " +
20
+ "always says level=\"establishment\", with levelNote when service was given. Idempotent. An agent cannot lift this " +
21
+ "exclusion; only a human in Leadify can. " + NEVER_IN_TEXT, {
22
+ ...writeShape,
23
+ company_lead_id: id.describe("Lead id of the company (COMPANY group)."),
24
+ reason: reason.describe("Why the account is excluded, e.g. 'Déjà client RDS Diag'."),
25
+ source_url: z.string().trim().url().max(2000).refine(value => /^https?:\/\//i.test(value), "http(s) URL required")
26
+ .describe("Public http(s) page proving the reason."),
27
+ service: z.string().trim().min(1).max(200).optional().describe("Department targeted, if any. Recorded in the note; the exclusion still covers the whole establishment."),
28
+ }, async ({ organization_id, company_lead_id, reason, source_url, service, idempotency_key, expected_version }) => {
29
+ try {
30
+ return toolResult(await client.post("/api/outreach/accounts/exclude", {
31
+ organizationId: organization_id, companyLeadId: company_lead_id, reason, sourceUrl: source_url, service,
32
+ idempotencyKey: idempotency_key, expectedVersion: expected_version, origin: "AGENT",
33
+ }));
34
+ }
35
+ catch (error) {
36
+ return handleToolError(error);
37
+ }
38
+ });
39
+ server.tool("exclude_lead", "Exclude one person from prospecting: no message is drafted, enrolled or sent, and their running campaigns stop. " +
40
+ "Same command as the Exclude button in Leadify; the history shows the API key owner as author. caused_by is MANUAL " +
41
+ "(default) or TARGET_NOT_FIT (out of target). For a company use exclude_account instead (this tool refuses company leads " +
42
+ "with 409 OUTREACH_COMPANY_USE_ACCOUNT_ROUTE). Returns the re-read state. " + NEVER_IN_TEXT, {
43
+ ...writeShape,
44
+ lead_id: id.describe("Lead id of the person."),
45
+ caused_by: z.enum(["MANUAL", "TARGET_NOT_FIT"]).default("MANUAL"),
46
+ reason: reason.describe("Why the person is excluded; shown in the Leadify history."),
47
+ }, async ({ organization_id, lead_id, caused_by, reason, idempotency_key, expected_version }) => {
48
+ try {
49
+ return toolResult(await client.post("/api/outreach/leads/exclude", {
50
+ organizationId: organization_id, leadId: lead_id, causedBy: caused_by, reason,
51
+ idempotencyKey: idempotency_key, expectedVersion: expected_version, origin: "AGENT",
52
+ }));
53
+ }
54
+ catch (error) {
55
+ return handleToolError(error);
56
+ }
57
+ });
58
+ server.tool("reintegrate_lead", "Allow again a person excluded by hand (MANUAL, out of target, or a migrated historical flag). A justification is " +
59
+ "required. It never lifts an opt-out, a deliverability protection or a company exclusion: if one remains, state.status " +
60
+ "stays \"excluded\" and state.causes says why. No campaign is restarted. Returns the re-read state.", {
61
+ ...writeShape,
62
+ lead_id: id.describe("Lead id of the person."),
63
+ reason: reason.describe("Why the person may be contacted again; shown in the Leadify history."),
64
+ }, async ({ organization_id, lead_id, reason, idempotency_key, expected_version }) => {
65
+ try {
66
+ return toolResult(await client.post("/api/outreach/leads/reintegrate", {
67
+ organizationId: organization_id, leadId: lead_id, reason,
68
+ idempotencyKey: idempotency_key, expectedVersion: expected_version, origin: "AGENT",
69
+ }));
70
+ }
71
+ catch (error) {
72
+ return handleToolError(error);
73
+ }
74
+ });
75
+ server.tool("get_outreach_state", "Read the prospecting state of 1 to 100 leads (people or companies): status allowed or excluded, active causes with " +
76
+ "their origin and note, version (pass it as expected_version to a write), lastTransitionAt and accountSources " +
77
+ "(companies whose exclusion the person inherits). Leads absent from the organization are listed in missingLeadIds. " +
78
+ "Read this before deciding, instead of looking for exclusions in text.", {
79
+ organization_id: id,
80
+ lead_ids: z.array(id).min(1).max(100),
81
+ }, { readOnlyHint: true }, async ({ organization_id, lead_ids }) => {
82
+ try {
83
+ return toolResult(await client.post("/api/outreach/state", { organizationId: organization_id, leadIds: lead_ids }));
84
+ }
85
+ catch (error) {
86
+ return handleToolError(error);
87
+ }
88
+ });
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "9.2.0",
3
+ "version": "9.4.0",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",