@agifyai/leadify-mcp 8.6.11 → 8.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
@@ -232,6 +232,7 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
232
232
  | `create_lead_group` | Créer un groupe et choisir son type canonique optionnel (`GENERIC`, `COMPANY` ou `PERSON`) via `entity_kind`. |
233
233
  | `update_lead_group` | Modifier un groupe, y compris son `entity_kind`, avec réconciliation du schéma côté Leadify. |
234
234
  | `add_leads` | Ajouter un ou plusieurs leads à un groupe. |
235
+ | `upsert_person_leads_with_employment` | Créer ou réconcilier atomiquement des Person Leads, PERSON, EMPLOYED_BY, projections et historique, sans effet commercial. |
235
236
  | `get_leads` | Rechercher et lister des leads avec filtres, recherche et pagination, dont le critère composé `event_filter`. |
236
237
  | `get_lead` | Récupérer les détails complets d'un lead par son ID. |
237
238
  | `update_lead` | Mettre à jour un ou plusieurs champs d'un lead existant. |
@@ -299,10 +300,10 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
299
300
  | `apply_canonical_identity_backfill` | Appliquer exactement un backfill d'identité relu par digest, sans outreach ni activation. |
300
301
  | `update_company_brain_sections` | Modifier uniquement les sections globales encore actives (`identity`, `positioning`, `allowedVocabulary`, `prohibitedVocabulary`, `legalConstraints`). Les anciennes sections Offre/Verticale sont filtrées et refusées. |
301
302
  | `publish_context_workspace` | Publier une révision prête du Company Brain après confirmation explicite (`confirm_publish: true`). Admin de l'organisation requis ; les raisons de non-readiness sont renvoyées par le serveur. |
302
- | `list_persona_contracts` / `get_persona_contract` | Lister ou lire les contrats Persona canoniques d’une organisation explicitement sélectionnée. |
303
- | `create_persona_contract` | Créer un Persona tenant-scoped et lié à une Verticale depuis un contrat canonique v2 complet et strict. Toute clé inconnue est refusée. |
304
- | `replace_persona_contract` / `patch_persona_contract` | Remplacer ou modifier un contrat canonique v2 avec verrou optimiste. Le document est revalidé intégralement avant l’écriture et aucune colonne Persona historique n’est exposée. |
305
- | `change_persona_status` | Passer un Persona entre `DRAFT`, `ACTIVE` et `ARCHIVED` avec le même verrou `updated_at`, readiness et protection des références. |
303
+ | `list_persona_contracts` / `get_persona_contract` | Lister ou lire les contrats Persona canoniques v2 d’une organisation explicitement sélectionnée. `intelligence` ne contient que `painPoints`, `icpStrategy` et `cardAnalysisSections` ; `activeTools` n’appartient pas au contrat. |
304
+ | `create_persona_contract` | Créer un Persona tenant-scoped et lié à une Verticale depuis un contrat canonique v2 complet et strict. `tool_pack_id` est requis à la création et reste hors contrat. Toute clé inconnue est refusée. |
305
+ | `replace_persona_contract` / `patch_persona_contract` | Remplacer ou modifier un contrat canonique v2 avec verrou optimiste. `name` / `description` sont acceptés sur replace. `tool_pack_id` est optionnel : s’il est omis, le pack persisté est réutilisé. Une Persona ACTIVE mutée repasse en DRAFT. |
306
+ | `change_persona_status` | Passer un Persona entre `DRAFT`, `ACTIVE` et `ARCHIVED` avec le même verrou `updated_at`, readiness et protection des références. Après un replace/patch, rappeler ce tool vers `ACTIVE` pour que les groupes résolvent à nouveau la Persona. |
306
307
  | `list_data_sources` | Lister toutes les sources de données configurées (par pays puis nom). |
307
308
  | `create_data_source` | Créer une nouvelle source de données (admin uniquement). |
308
309
  | `update_data_source` | Modifier une source de données existante (admin uniquement). |
package/dist/server.js CHANGED
@@ -19,6 +19,7 @@ import { registerCanonicalBackfillTools } from "./tools/canonical_backfill.js";
19
19
  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
+ import { registerPersonEmploymentTools } from "./tools/person_employment.js";
22
23
  import { MCP_SERVER_NAME, MCP_VERSION } from "./version.js";
23
24
  export function createServer() {
24
25
  const server = new McpServer({
@@ -32,6 +33,7 @@ export function createServer() {
32
33
  registerLeadTools(server);
33
34
  registerRelationshipTools(server);
34
35
  registerCanonicalBackfillTools(server);
36
+ registerPersonEmploymentTools(server);
35
37
  registerEventTools(server);
36
38
  registerCommercialTruthTools(server);
37
39
  registerSchemaTools(server);
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerPersonEmploymentTools(server: McpServer, client?: import("../client.js").LeadifyClient): void;
@@ -0,0 +1,35 @@
1
+ import { z } from "zod";
2
+ import { getClient } from "../client.js";
3
+ import { handleToolError, toolResult } from "../types.js";
4
+ export function registerPersonEmploymentTools(server, client = getClient()) {
5
+ 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.", {
6
+ people: z.array(z.object({
7
+ organization_id: z.string().trim().min(1),
8
+ person_group_id: z.string().trim().min(1),
9
+ company_lead_id: z.string().trim().min(1),
10
+ person_lead_id: z.string().trim().min(1).optional(),
11
+ fields: z.record(z.unknown()),
12
+ card_history_append: z.string().trim().min(1).max(100_000),
13
+ evidence_ids: z.array(z.string().trim().min(1)).min(1).max(100),
14
+ idempotency_key: z.string().trim().min(1).max(220),
15
+ }).strict()).min(1).max(100),
16
+ }, async ({ people }) => {
17
+ try {
18
+ return toolResult(await client.post("/api/person-employment/upsert", {
19
+ people: people.map((person) => ({
20
+ organizationId: person.organization_id,
21
+ personGroupId: person.person_group_id,
22
+ companyLeadId: person.company_lead_id,
23
+ personLeadId: person.person_lead_id,
24
+ fields: person.fields,
25
+ cardHistoryAppend: person.card_history_append,
26
+ evidenceIds: person.evidence_ids,
27
+ idempotencyKey: person.idempotency_key,
28
+ })),
29
+ }));
30
+ }
31
+ catch (error) {
32
+ return handleToolError(error);
33
+ }
34
+ });
35
+ }
@@ -5,6 +5,10 @@ import { LeadifyApiError, handleToolError, toolResult } from "../types.js";
5
5
  * PRD-1423 hard cutover: Persona business content is represented only by the
6
6
  * versioned `personaContract`. These tools deliberately do not expose any
7
7
  * historical Persona field or field-specific patch operation.
8
+ *
9
+ * The published MCP contract must match the server `personaContractSchema`.
10
+ * `intelligence.activeTools` is not part of that document; the tool pack lives
11
+ * on `toolPackId`, outside the contract.
8
12
  */
9
13
  const tierSchema = z.object({
10
14
  rule: z.string(),
@@ -38,7 +42,6 @@ const commonContractSchema = z.object({
38
42
  enableSignals: z.boolean(),
39
43
  }).strict(),
40
44
  intelligence: z.object({
41
- activeTools: z.array(z.string()),
42
45
  painPoints: z.unknown().nullable(),
43
46
  icpStrategy: z.unknown().nullable(),
44
47
  cardAnalysisSections: z.unknown().nullable(),
@@ -67,7 +70,24 @@ const personExtensionSchema = z.object({
67
70
  const contractSchema = z.discriminatedUnion("kind", [
68
71
  commonContractSchema.extend({ kind: z.literal("COMPANY"), person: z.null() }).strict(),
69
72
  commonContractSchema.extend({ kind: z.literal("PERSON"), person: personExtensionSchema }).strict(),
70
- ]).describe("Complete strict Persona contract, schemaVersion 2.");
73
+ ]);
74
+ function withoutOutOfContractKeys(value) {
75
+ if (!value || typeof value !== "object" || Array.isArray(value))
76
+ return value;
77
+ const contract = structuredClone(value);
78
+ const intelligence = contract.intelligence;
79
+ if (intelligence && typeof intelligence === "object" && !Array.isArray(intelligence)) {
80
+ delete intelligence.activeTools;
81
+ }
82
+ return contract;
83
+ }
84
+ function parsePersistedContract(value) {
85
+ return contractSchema.parse(withoutOutOfContractKeys(value));
86
+ }
87
+ function parseWrittenContract(value) {
88
+ return parsePersistedContract(value);
89
+ }
90
+ const contractInputSchema = z.preprocess(withoutOutOfContractKeys, contractSchema).describe("Complete strict Persona contract, schemaVersion 2. intelligence.activeTools is not a contract field and is stripped if sent.");
71
91
  const tenantScope = {
72
92
  organization_id: z
73
93
  .string()
@@ -82,15 +102,23 @@ const optimisticScope = {
82
102
  .datetime({ offset: true })
83
103
  .describe("Exact updatedAt obtained from a preceding read."),
84
104
  };
105
+ function requireToolPackId(persona, override) {
106
+ const toolPackId = override?.trim() || persona.toolPackId?.trim();
107
+ if (!toolPackId)
108
+ throw new Error("PERSONA_TOOL_PACK_REQUIRED");
109
+ return toolPackId;
110
+ }
85
111
  function canonicalPersona(persona) {
86
112
  if (!persona.personaContract || typeof persona.personaContract !== "object") {
87
113
  throw new Error("PERSONA_CONTRACT_REQUIRED: run the audited Persona migration before using this MCP.");
88
114
  }
89
- const contract = contractSchema.parse(persona.personaContract);
115
+ const contract = parsePersistedContract(persona.personaContract);
90
116
  return {
91
117
  id: persona.id,
92
118
  name: persona.name,
119
+ description: persona.description ?? null,
93
120
  verticalId: persona.verticalId,
121
+ toolPackId: persona.toolPackId ?? null,
94
122
  updatedAt: persona.updatedAt,
95
123
  revision: persona.revision ?? 0,
96
124
  status: persona.status,
@@ -139,11 +167,13 @@ async function replaceContract(input) {
139
167
  }
140
168
  return input.client.post("/api/persona", {
141
169
  id: persona.id,
142
- name: persona.name,
170
+ name: input.name ?? persona.name,
143
171
  organizationId: input.organizationId,
144
172
  verticalId: persona.verticalId,
145
173
  expectedUpdatedAt: input.expectedUpdatedAt,
146
- personaContract: input.contract,
174
+ toolPackId: requireToolPackId(persona, input.toolPackId),
175
+ ...(input.description !== undefined ? { description: input.description } : {}),
176
+ personaContract: parseWrittenContract(input.contract),
147
177
  });
148
178
  }
149
179
  export function registerPersonaTools(server, injectedClient) {
@@ -168,33 +198,54 @@ export function registerPersonaTools(server, injectedClient) {
168
198
  return handleToolError(error);
169
199
  }
170
200
  });
171
- server.tool("create_persona_contract", "Create a Persona from one complete canonical contract. Historical Persona fields are not accepted.", {
201
+ server.tool("create_persona_contract", "Create a Persona from one complete canonical contract v2. intelligence.activeTools is not part of the contract. tool_pack_id is required on create and lives outside the contract.", {
172
202
  ...tenantScope,
173
203
  vertical_id: z.string().trim().min(1),
174
204
  name: z.string().trim().min(1),
175
205
  description: z.string().optional(),
176
- contract: contractSchema,
177
- }, async ({ organization_id, vertical_id, name, description, contract }) => {
206
+ tool_pack_id: z
207
+ .string()
208
+ .trim()
209
+ .min(1)
210
+ .describe("Tenant-scoped tool pack. Not part of personaContract."),
211
+ contract: contractInputSchema,
212
+ }, async ({ organization_id, vertical_id, name, description, tool_pack_id, contract }) => {
178
213
  try {
179
214
  return toolResult(await client.post("/api/persona", {
180
215
  organizationId: organization_id,
181
216
  verticalId: vertical_id,
182
217
  name,
218
+ toolPackId: tool_pack_id,
183
219
  ...(description !== undefined ? { description } : {}),
184
- personaContract: contract,
220
+ personaContract: parseWrittenContract(contract),
185
221
  }));
186
222
  }
187
223
  catch (error) {
188
224
  return handleToolError(error);
189
225
  }
190
226
  });
191
- server.tool("replace_persona_contract", "Replace the complete canonical contract using optimistic concurrency.", { ...optimisticScope, id: z.string().min(1), contract: contractSchema }, async ({ organization_id, id, expected_updated_at, contract }) => {
227
+ server.tool("replace_persona_contract", "Replace the complete canonical contract using optimistic concurrency. Omitting tool_pack_id reuses the persisted pack. Mutating an ACTIVE Persona returns it to DRAFT; call change_persona_status to reactivate.", {
228
+ ...optimisticScope,
229
+ id: z.string().min(1),
230
+ name: z.string().trim().min(1).optional(),
231
+ description: z.string().optional(),
232
+ tool_pack_id: z
233
+ .string()
234
+ .trim()
235
+ .min(1)
236
+ .optional()
237
+ .describe("Optional pack change. When omitted, the persisted toolPackId is reused."),
238
+ contract: contractInputSchema,
239
+ }, async ({ organization_id, id, expected_updated_at, name, description, tool_pack_id, contract }) => {
192
240
  try {
193
241
  return toolResult(await replaceContract({
194
242
  organizationId: organization_id,
195
243
  id,
196
244
  expectedUpdatedAt: expected_updated_at,
197
- contract,
245
+ name,
246
+ description,
247
+ toolPackId: tool_pack_id,
248
+ contract: parseWrittenContract(contract),
198
249
  client,
199
250
  }));
200
251
  }
@@ -202,12 +253,18 @@ export function registerPersonaTools(server, injectedClient) {
202
253
  return handleToolError(error);
203
254
  }
204
255
  });
205
- server.tool("patch_persona_contract", "Change one canonical contract path using optimistic concurrency. Removing a field is forbidden because the contract is strict and complete.", {
256
+ server.tool("patch_persona_contract", "Change one canonical contract path using optimistic concurrency. Removing a field is forbidden because the contract is strict and complete. intelligence.activeTools is not a contract path. Mutating an ACTIVE Persona returns it to DRAFT; call change_persona_status to reactivate.", {
206
257
  ...optimisticScope,
207
258
  id: z.string().min(1),
208
259
  path: z.string().trim().min(1),
209
260
  value: z.unknown(),
210
- }, async ({ organization_id, id, expected_updated_at, path, value }) => {
261
+ tool_pack_id: z
262
+ .string()
263
+ .trim()
264
+ .min(1)
265
+ .optional()
266
+ .describe("Optional pack change. When omitted, the persisted toolPackId is reused."),
267
+ }, async ({ organization_id, id, expected_updated_at, path, value, tool_pack_id }) => {
211
268
  try {
212
269
  const persona = await fetchPersona(id, organization_id, client);
213
270
  const contract = canonicalPersona(persona).contract;
@@ -215,6 +272,7 @@ export function registerPersonaTools(server, injectedClient) {
215
272
  organizationId: organization_id,
216
273
  id,
217
274
  expectedUpdatedAt: expected_updated_at,
275
+ toolPackId: tool_pack_id,
218
276
  contract: contractSchema.parse(setContractPath(contract, path, value)),
219
277
  client,
220
278
  }));
@@ -223,7 +281,7 @@ export function registerPersonaTools(server, injectedClient) {
223
281
  return handleToolError(error);
224
282
  }
225
283
  });
226
- server.tool("change_persona_status", "Change only Persona lifecycle status; it never changes business contract content.", { ...optimisticScope, id: z.string().min(1), status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]) }, async ({ organization_id, id, expected_updated_at, status }) => {
284
+ server.tool("change_persona_status", "Change only Persona lifecycle status; it never changes business contract content. After replace/patch, an ACTIVE Persona is typically returned to DRAFT and must be set back to ACTIVE for groups to resolve it again.", { ...optimisticScope, id: z.string().min(1), status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]) }, async ({ organization_id, id, expected_updated_at, status }) => {
227
285
  try {
228
286
  return toolResult(await client.post(`/api/persona/${encodeURIComponent(id)}/status?organizationId=${encodeURIComponent(organization_id)}`, { expectedUpdatedAt: expected_updated_at, status }));
229
287
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "8.6.11",
3
+ "version": "8.7.1",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",