@agifyai/leadify-mcp 8.7.4 → 8.7.6

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
@@ -231,7 +231,7 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
231
231
  | `get_lead_group` | Consulter les métadonnées compactes d'un groupe accessible par son ID. |
232
232
  | `create_lead_group` | Créer un groupe, choisir son type canonique optionnel et rattacher atomiquement `persona_id` avec l’`offer_id` ACTIVE du même tenant. |
233
233
  | `update_lead_group` | Modifier un groupe avec réconciliation du schéma ; lorsque le serveur exige un contexte complet, transmettre ensemble `persona_id` et `offer_id` sans inférer l’offre. |
234
- | `add_leads` | Ajouter un ou plusieurs leads à un groupe. |
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
237
  | `get_leads` | Rechercher et lister des leads avec filtres, recherche et pagination, dont le critère composé `event_filter`. |
@@ -325,6 +325,46 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
325
325
  | `set_fine_tuning_output_config` | Remplacer uniquement la configuration métier de sorties d’un Fine Tuning (`linkedinConnection`, `linkedinMessage`, `email`, activations et quantités). Préserve Markdown, langues et fallback ; ne génère, ne planifie, n’active ni n’envoie aucun outreach. |
326
326
  | `pipeline_next_lead` | Sélectionner le prochain lead à traiter (score descendant, sans message). Exclusion des IDs déjà vus, limit 1-5. |
327
327
 
328
- ### `source-icp-prospects` : preuve d’emploi
329
-
330
- Pour chaque candidat accepté, créer `create_person_employment_evidence` avec le tenant, la Company Lead, l’identité Person, la provenance et une clé idempotente. Relire si nécessaire avec `get_person_employment_evidence`, puis transmettre exclusivement l’`evidence.id` retourné dans `evidence_ids` de `upsert_person_leads_with_employment`.
328
+ ### `source-icp-prospects` : sourcing Personne rattachée à une Company
329
+
330
+ `add_leads` crée les identités minimales des fiches typées, mais aucun emploi.
331
+ Pour sourcer une personne liée à une entreprise, utiliser
332
+ `upsert_person_leads_with_employment` : il crée ou réutilise la Personne et
333
+ son emploi vers l’Account exact, conserve les preuves et ajoute l’historique.
334
+ Chaque entrée est enregistrée entièrement ou annulée. Aucun numéro légal ni
335
+ préparation juridique de l’entreprise n’est obligatoire.
336
+
337
+ 1. Choisir l’organisation, les groupes et la fiche entreprise exacts.
338
+ 2. Créer la preuve avec `create_person_employment_evidence`, en conservant
339
+ ses dates originales. Transmettre son identifiant dans `evidence_ids`.
340
+ 3. Appeler `upsert_person_leads_with_employment` avec les champs admis par
341
+ le groupe, l’historique à ajouter et une clé de répétition unique.
342
+ `person_lead_id` permet de réconcilier une fiche existante précise.
343
+ 4. Relire les deux fiches avec `get_person_employments` et vérifier le même
344
+ identifiant d’emploi. Les anciens champs `parent_lead` et `child_leads`
345
+ ne sont plus écrits ni utilisés pour déterminer le lien.
346
+
347
+ `get_lead` et `get_leads` conservent `canonicalIdentity` et
348
+ `personAccountRelationships`, indépendamment des champs CRM demandés.
349
+ Tous les emplois restent accessibles, avec leurs dates et leur confiance.
350
+ Une confiance inconnue ne signifie pas une absence de lien. L’entreprise
351
+ utilisée pour une qualification doit être choisie explicitement.
352
+
353
+ Pour modifier un emploi existant, utiliser `manage_person_employment` :
354
+ `attach` ajoute sans fermer les autres emplois ; `end` termine une période ;
355
+ `invalidate` conserve la trace d’un lien erroné ; `replace` remplace seulement
356
+ la relation désignée ; `evidence` actualise ses preuves et sa confiance.
357
+ Les modifications exigent `expected_version`, relu avec
358
+ `get_person_employments`, et une `idempotency_key` dont le contenu ne change
359
+ pas lors d’une répétition. Les droits administrateur de l’organisation sont
360
+ vérifiés par le serveur.
361
+
362
+ Les preuves détaillées se lisent à la demande avec
363
+ `get_person_employment_evidence_page`, pour une personne, un groupe et un
364
+ emploi précis. Suivre `page.nextCursor` tant que `page.hasMore` vaut `true`.
365
+ Les dates originales sont conservées ; la pagination n’efface aucune preuve.
366
+
367
+ Aucune mutation de sourcing ne qualifie le lead, n’enrichit les contacts
368
+ (email/téléphone), ne génère de message, n’inscrit en campagne et n’active
369
+ aucun canal externe : le readback porte `noSend: true` et
370
+ `externalActivation: 0`.
package/dist/server.js CHANGED
@@ -20,6 +20,7 @@ import { registerCommercialTruthTools } from "./tools/commercial_truth.js";
20
20
  import { registerEventTools } from "./tools/events.js";
21
21
  import { registerContextEntityTools } from "./tools/context_entities.js";
22
22
  import { registerPersonEmploymentTools } from "./tools/person_employment.js";
23
+ import { registerPersonAccountTools } from "./tools/person_account.js";
23
24
  import { registerClaimContradictionTools } from "./tools/claim_contradictions.js";
24
25
  import { MCP_SERVER_NAME, MCP_VERSION } from "./version.js";
25
26
  export function createServer() {
@@ -36,6 +37,7 @@ export function createServer() {
36
37
  registerCanonicalBackfillTools(server);
37
38
  registerClaimContradictionTools(server);
38
39
  registerPersonEmploymentTools(server);
40
+ registerPersonAccountTools(server);
39
41
  registerEventTools(server);
40
42
  registerCommercialTruthTools(server);
41
43
  registerSchemaTools(server);
@@ -80,6 +80,11 @@ function projectLead(lead, fields) {
80
80
  for (const field of fields)
81
81
  if (data[field] !== undefined)
82
82
  projected[field] = data[field];
83
+ // Relationship metadata belongs to the server contract, not selectable CRM fields.
84
+ for (const key of ["canonicalIdentity", "personAccountRelationships"]) {
85
+ if (source[key] !== undefined)
86
+ projected[key] = source[key];
87
+ }
83
88
  return projected;
84
89
  }
85
90
  function compactLeadsResponse(data, fields, includeSchema) {
@@ -116,11 +121,15 @@ export function registerLeadTools(server, client = getClient()) {
116
121
  });
117
122
  }
118
123
  // ── add_leads ──────────────────────────────────────────────────────────
119
- server.tool("add_leads", "Add one or more leads to a specific lead group in Leadify. Provide the group ID " +
124
+ server.tool("add_leads", "Flat generic lead import only: add one or more leads to a specific lead group in Leadify. " +
125
+ "It creates each typed lead with its minimal Person or Account identity, but creates no employment relationship. " +
126
+ "It must never be used to attach a Person to a Company: Person sourcing attached to a Company must use " +
127
+ "upsert_person_leads_with_employment (after create_person_employment_evidence; no legal identity backfill is required). " +
128
+ "Provide the group ID " +
120
129
  "and an array of lead objects with any combination of fields (email, firstName, " +
121
130
  "lastName, company, jobTitle, phone, custom fields, card_* fields, percent_* " +
122
131
  "fields, etc.). Canonical relation fields are read-only here: use " +
123
- "create_canonical_relationship or the migration tools instead. Optionally specify which fields should be " +
132
+ "manage_person_employment for employment links instead. Optionally specify which fields should be " +
124
133
  "treated as 'select' dropdowns in the UI. Returns the count of successfully " +
125
134
  "imported leads and any errors. Use this for both bulk imports and single lead creation.", {
126
135
  lead_group_id: z
@@ -130,7 +139,8 @@ export function registerLeadTools(server, client = getClient()) {
130
139
  .array(leadInputSchema)
131
140
  .describe("Array of lead objects. Each object is a key-value map of field names to values. " +
132
141
  "Common fields: email, firstName, lastName, company, jobTitle, phone, linkedin, " +
133
- "website, seniority, specialty, age. location must be {city, region?, postalCode?, countryCode, street?}; geo is computed by Leadify. Also supports card_* fields, " +
142
+ "website, seniority, specialty, age. The company field is flat display text only and never creates an employment relationship or parent link. " +
143
+ "location must be {city, region?, postalCode?, countryCode, street?}; geo is computed by Leadify. Also supports card_* fields, " +
134
144
  "percent_* fields and boolean flags (decisionMaking, excluded). Canonical relation fields are rejected."),
135
145
  is_select_fields: z
136
146
  .array(z.string())
@@ -260,7 +270,7 @@ export function registerLeadTools(server, client = getClient()) {
260
270
  "name, new value, and optionally whether the field is a 'select' dropdown type. " +
261
271
  "Supports all field types including special fields (card_*, percent_*, boolean flags " +
262
272
  "like decisionMaking/excluded). When a configured canonical relation field is changed, " +
263
- "the backend routes it through the typed relationship service and keeps the JSON field read-only. Returns the updated lead. " +
273
+ "employment fields are refused: use manage_person_employment. Other relationship types retain their own service. Returns the updated lead. " +
264
274
  "location must be the canonical object {city, region?, postalCode?, countryCode, street?}; never submit geo. " +
265
275
  "For bulk updates across many leads, call this tool once per lead.", {
266
276
  lead_id: z.string().describe("ID of the lead to update."),
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerPersonAccountTools(server: McpServer, client?: import("../client.js").LeadifyClient): void;
@@ -0,0 +1,56 @@
1
+ import { z } from "zod";
2
+ import { getClient } from "../client.js";
3
+ import { handleToolError, toolResult } from "../types.js";
4
+ const id = z.string().trim().min(1);
5
+ const instant = z.string().datetime({ offset: true });
6
+ const trust = z.enum(["CONFIRMED", "PROPOSED", "AMBIGUOUS", "CONFLICTED", "UNKNOWN"]);
7
+ const existing = { relationship_id: id, expected_version: z.number().int().positive() };
8
+ const link = { person_lead_id: id, company_lead_id: id, trust_state: trust.optional(),
9
+ evidence_ids: z.array(id).max(100).optional(), role_title: id.nullable().optional() };
10
+ const command = z.discriminatedUnion("operation", [
11
+ z.object({ operation: z.literal("attach"), ...link, valid_from: instant.nullable().optional() }).strict(),
12
+ z.object({ operation: z.literal("end"), ...existing, ended_at: instant }).strict(),
13
+ z.object({ operation: z.literal("invalidate"), ...existing, reason: id }).strict(),
14
+ z.object({ operation: z.literal("evidence"), ...existing, evidence_ids: z.array(id).min(1).max(100), trust_state: trust }).strict(),
15
+ z.object({ operation: z.literal("replace"), ...existing, ...link, effective_at: instant }).strict(),
16
+ ]);
17
+ const commandKeys = {
18
+ operation: "operation", relationship_id: "relationshipId", expected_version: "expectedVersion",
19
+ person_lead_id: "personLeadId", company_lead_id: "companyLeadId", trust_state: "trustState",
20
+ evidence_ids: "evidenceIds", role_title: "roleTitle", valid_from: "validFrom", ended_at: "endedAt",
21
+ reason: "reason", effective_at: "effectiveAt",
22
+ };
23
+ export function registerPersonAccountTools(server, client = getClient()) {
24
+ server.tool("get_person_employments", "Read all employment periods for one exact Person or Company lead in its organization, including historical and invalidated links. Both directions use the same Person Account registry. Preserve trust and dates; UNKNOWN or stale evidence does not erase a link. Read relationship version before updating it; never choose the first employer implicitly.", { organization_id: id, lead_id: id }, async ({ organization_id, lead_id }) => {
25
+ try {
26
+ return toolResult(await client.get(`/api/person-employment/links/${encodeURIComponent(lead_id)}`, new URLSearchParams({ organizationId: organization_id })));
27
+ }
28
+ catch (error) {
29
+ return handleToolError(error);
30
+ }
31
+ });
32
+ server.tool("manage_person_employment", "Attach, end, invalidate, replace one exact employment, or update its evidence through the shared Person Account command. Requires organization admin access. Minimal Person and Account identities are ensured without SIREN or legal identity prerequisites. attach preserves other jobs; replace ends only the named relationship and preserves other simultaneous jobs. end preserves history; invalidate records an erroneous link and cannot be automatically reversed. Existing links require their latest expected_version from get_person_employments. Retries require the same idempotency key and unchanged payload. This never sends messages, qualifies a lead or activates a campaign.", { organization_id: id, idempotency_key: id.max(180), command }, async ({ organization_id, idempotency_key, command: input }) => {
33
+ try {
34
+ const checked = command.parse(input);
35
+ const body = Object.fromEntries(Object.entries(checked).map(([key, value]) => [commandKeys[key], value]));
36
+ return toolResult(await client.post("/api/person-employment/command", {
37
+ organizationId: organization_id, idempotencyKey: idempotency_key, ...body,
38
+ }));
39
+ }
40
+ catch (error) {
41
+ return handleToolError(error);
42
+ }
43
+ });
44
+ server.tool("get_person_employment_evidence_page", "Read detailed evidence on demand for one employment belonging to the exact Person lead, group and organization. Follow page.nextCursor while page.hasMore is true. Original observation and collection dates are preserved; an empty page is not a claim that another employment has no evidence.", { organization_id: id.max(200), lead_id: id.max(200), lead_group_id: id.max(200), relationship_id: id.max(200),
45
+ cursor: id.max(200).optional(), limit: z.number().int().min(1).max(5).optional().default(5) }, async ({ organization_id, lead_id, lead_group_id, relationship_id, cursor, limit }) => {
46
+ try {
47
+ return toolResult(await client.post("/api/qualify-lead/employment-evidence", {
48
+ organizationId: organization_id, leadId: lead_id, leadGroupId: lead_group_id,
49
+ relationshipId: relationship_id, cursor, limit,
50
+ }));
51
+ }
52
+ catch (error) {
53
+ return handleToolError(error);
54
+ }
55
+ });
56
+ }
@@ -44,7 +44,7 @@ export function registerPersonEmploymentTools(server, client = getClient()) {
44
44
  return handleToolError(error);
45
45
  }
46
46
  });
47
- server.tool("upsert_person_leads_with_employment", "Atomically create or reconcile Person Leads with their canonical PERSON identity, EMPLOYED_BY relationship, parent_lead/child_leads projections, evidence and append-only card_history. company_lead_id must be exact and already project an Account plus Legal Entity or compatible Establishment. Each batch entry is independently transactional and idempotent. This is the only supported Person write path for sourcing; never fall back to add_leads, canonical backfill, or separate relationship writes. It never qualifies, enriches contact details, sends outreach, creates campaigns, or activates anything.", {
47
+ server.tool("upsert_person_leads_with_employment", "Atomically create or reconcile Person Leads with their minimal PERSON identity and an active EMPLOYED_BY relationship to the exact Company ACCOUNT, evidence and append-only card_history. company_lead_id must identify an existing Company lead in the same organization; no legal identity, SIREN or prior backfill is required. Never fuzzy-match identity. Pass person_lead_id to reconcile one exact existing Person Lead without creating a duplicate. fields must only contain keys admitted by the Person group schema; card_history_append preserves existing history. Each entry is independently transactional and idempotent by idempotency_key; changed payloads under the same key are refused. Other simultaneous jobs are preserved. Verify the returned personId, accountId and relationshipId through get_person_employments on both lead IDs. No parent_lead or child_leads values are written. This is the only supported Person write path for sourcing; never fall back to add_leads or legal backfill to attach a sourced person. For explicit changes to existing jobs, use manage_person_employment. It never qualifies a lead, enriches contacts, generates messages, enrolls in a campaign or activates an external channel.", {
48
48
  people: z.array(z.object({
49
49
  organization_id: z.string().trim().min(1),
50
50
  person_group_id: z.string().trim().min(1),
@@ -52,7 +52,7 @@ export function registerPersonEmploymentTools(server, client = getClient()) {
52
52
  person_lead_id: z.string().trim().min(1).optional(),
53
53
  fields: z.record(z.unknown()),
54
54
  card_history_append: z.string().trim().min(1).max(100_000),
55
- evidence_ids: z.array(z.string().trim().min(1)).min(1).max(100),
55
+ evidence_ids: z.array(z.string().trim().min(1)).min(1).max(100).optional(),
56
56
  idempotency_key: z.string().trim().min(1).max(220),
57
57
  }).strict()).min(1).max(100),
58
58
  }, async ({ people }) => {
@@ -65,7 +65,7 @@ export function registerPersonEmploymentTools(server, client = getClient()) {
65
65
  personLeadId: person.person_lead_id,
66
66
  fields: person.fields,
67
67
  cardHistoryAppend: person.card_history_append,
68
- evidenceIds: person.evidence_ids,
68
+ ...(person.evidence_ids !== undefined ? { evidenceIds: person.evidence_ids } : {}),
69
69
  idempotencyKey: person.idempotency_key,
70
70
  })),
71
71
  }));
@@ -10,7 +10,7 @@ const confidence = z.object({
10
10
  assessed_at: z.string().datetime(),
11
11
  }).strict();
12
12
  export function registerRelationshipTools(server, client = getClient()) {
13
- server.tool("list_canonical_relationships", "List tenant-scoped canonical relationships. The response is direction-independent for consumers: filter by one entity to find both incoming and outgoing links. Legacy lead relation fields are compatibility projections, not the source of truth.", {
13
+ server.tool("list_canonical_relationships", "List tenant-scoped canonical relationships. The response is direction-independent for consumers: filter by one entity to find both incoming and outgoing links. Employment links are PERSON EMPLOYED_BY ACCOUNT. Use get_person_employments to read them from lead IDs; stored parent_lead and child_leads are retired.", {
14
14
  organization_id: z.string().min(1),
15
15
  entity_kind: entityKind.optional(),
16
16
  entity_id: z.string().min(1).optional(),
@@ -37,7 +37,7 @@ export function registerRelationshipTools(server, client = getClient()) {
37
37
  return handleToolError(error);
38
38
  }
39
39
  });
40
- server.tool("create_canonical_relationship", "Create one typed canonical relationship in a tenant. Endpoints must already be strong canonical identities in that tenant. Retries with the same idempotency key and payload are safe; a changed payload is rejected. This tool never creates placeholder identities or activates outreach.", {
40
+ server.tool("create_canonical_relationship", "Create one typed canonical relationship in a tenant. Endpoints must already be strong canonical identities in that tenant. Retries with the same idempotency key and payload are safe; a changed payload is rejected. For EMPLOYED_BY use manage_person_employment instead; this generic write route rejects employment changes. This tool never creates placeholder identities or activates outreach.", {
41
41
  organization_id: z.string().min(1), relationship_type: relationshipType,
42
42
  source: entityRef, target: entityRef,
43
43
  trust_state: z.enum(["CONFIRMED", "PROPOSED", "AMBIGUOUS", "CONFLICTED", "UNKNOWN"]).optional().default("UNKNOWN"),
@@ -58,7 +58,7 @@ export function registerRelationshipTools(server, client = getClient()) {
58
58
  return handleToolError(error);
59
59
  }
60
60
  });
61
- server.tool("update_canonical_relationship", "Update, tombstone, or restore one canonical relationship through the shared transactional service. Every mutation is versioned in the immutable ledger and requires a unique idempotency key.", {
61
+ server.tool("update_canonical_relationship", "Update, tombstone, or restore one canonical relationship through the shared transactional service. Every mutation is versioned in the immutable ledger and requires a unique idempotency key. EMPLOYED_BY changes are refused here; use manage_person_employment with the current relationship version.", {
62
62
  organization_id: z.string().min(1), relationship_id: z.string().min(1),
63
63
  relationship_type: relationshipType.optional(), source: entityRef.optional(), target: entityRef.optional(),
64
64
  trust_state: z.enum(["CONFIRMED", "PROPOSED", "AMBIGUOUS", "CONFLICTED", "UNKNOWN"]).optional(),
@@ -78,7 +78,7 @@ export function registerRelationshipTools(server, client = getClient()) {
78
78
  return handleToolError(error);
79
79
  }
80
80
  });
81
- server.tool("tombstone_canonical_relationship", "Soft-delete one canonical relationship while preserving its evidence and immutable history. Use this instead of deleting legacy relation fields.", { organization_id: z.string().min(1), relationship_id: z.string().min(1), idempotency_key: z.string().min(1).max(255) }, async ({ organization_id, relationship_id, idempotency_key }) => {
81
+ server.tool("tombstone_canonical_relationship", "Soft-delete one canonical relationship while preserving its evidence and immutable history. For employment use manage_person_employment with operation invalidate or end; this generic route rejects EMPLOYED_BY.", { organization_id: z.string().min(1), relationship_id: z.string().min(1), idempotency_key: z.string().min(1).max(255) }, async ({ organization_id, relationship_id, idempotency_key }) => {
82
82
  try {
83
83
  return toolResult(await client.delete(`/api/canonical-relationships/${encodeURIComponent(relationship_id)}`, { organizationId: organization_id, idempotencyKey: idempotency_key }));
84
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "8.7.4",
3
+ "version": "8.7.6",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",