@agifyai/leadify-mcp 8.3.9 → 8.4.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
@@ -193,6 +193,11 @@ Conséquences pratiques :
193
193
  | `get_leads` | Rechercher et lister des leads avec filtres, recherche et pagination. |
194
194
  | `get_lead` | Récupérer les détails complets d'un lead par son ID. |
195
195
  | `update_lead` | Mettre à jour un ou plusieurs champs d'un lead existant. |
196
+
197
+ Pour `add_leads` et `update_lead`, `location` utilise l’objet canonique
198
+ `{ city, region?, postalCode?, countryCode, street? }`. `city` et le code pays
199
+ ISO-2 `countryCode` sont obligatoires. La projection `geo` est calculée par
200
+ Leadify et ne doit jamais être envoyée par un agent MCP.
196
201
  | `delete_sequence_messages` | Supprimer uniquement les messages de séquence générés d'un lead, après confirmation explicite. |
197
202
  | `delete_leads` | Supprimer définitivement des leads par leurs IDs. |
198
203
  | `update_schema` | Ajouter ou modifier les définitions de champs d'un groupe. |
@@ -215,6 +220,13 @@ Conséquences pratiques :
215
220
  | `add_activity` | Journaliser une interaction prospect (LinkedIn, email, call) dans le feed du lead. |
216
221
  | `compile_context_pack` | Compiler en lecture seule un Context Pack canonique depuis une cible explicite (`organization`, `lead_group`, `campaign`, `account`, `person`) ou un couple lead + campagne pour `WRITE_OUTREACH`. Retourne publications courantes, sources, freshness, contradictions et provenance. N’utilise jamais la Data Room legacy ni ne choisit de cible implicite. |
217
222
  | `get_context_workspace` | Lire le Context Workspace canonique versionné (`company_brain` ou `gtm_playbook`) d'une organisation explicitement sélectionnée. Retourne le brouillon et la dernière version publiée. |
223
+ | `list_canonical_relationships` | Lire les liens canoniques typés d'une organisation, dans les deux directions. |
224
+ | `create_canonical_relationship` | Créer un lien canonique tenant-scoped et idempotent entre deux identités fortes. |
225
+ | `update_canonical_relationship` | Modifier, restaurer ou tombstoner un lien via le ledger transactionnel partagé. |
226
+ | `tombstone_canonical_relationship` | Retirer réversiblement un lien sans supprimer son historique ni ses preuves. |
227
+ | `preview_canonical_relationship_migration` | Prévisualiser une migration de champs historiques, divergences et quarantaines comprises, sans mutation. |
228
+ | `apply_canonical_relationship_migration` | Appliquer exactement un plan prévisualisé et borné grâce à son digest. |
229
+ | `rollback_canonical_relationship_migration` | Tombstoner les liens créés par un plan de migration précis. |
218
230
  | `update_company_brain_sections` | Modifier uniquement les sections indiquées du Company Brain : le MCP relit le brouillon ou la dernière publication, fusionne les sections ciblées, puis sauvegarde avec révision attendue et clé d'idempotence. |
219
231
  | `update_gtm_playbook_sections` | Modifier uniquement les sections indiquées du GTM Playbook, avec la même lecture-fusion-révision atomiquement contrôlée. |
220
232
  | `publish_context_workspace` | Publier une révision prête du Context Workspace après confirmation explicite (`confirm_publish: true`). Admin de l'organisation requis ; les raisons de non-readiness sont renvoyées par le serveur. |
package/dist/client.d.ts CHANGED
@@ -7,6 +7,6 @@ export declare class LeadifyClient {
7
7
  post(path: string, body: unknown): Promise<unknown>;
8
8
  put(path: string, body: unknown): Promise<unknown>;
9
9
  patch(path: string, body: unknown): Promise<unknown>;
10
- delete(path: string): Promise<unknown>;
10
+ delete(path: string, body?: unknown): Promise<unknown>;
11
11
  }
12
12
  export declare function getClient(): LeadifyClient;
package/dist/client.js CHANGED
@@ -17,7 +17,7 @@ export class LeadifyClient {
17
17
  };
18
18
  const init = { method, headers };
19
19
  if (body !== undefined &&
20
- (method === "POST" || method === "PUT" || method === "PATCH")) {
20
+ (method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE")) {
21
21
  headers["Content-Type"] = "application/json";
22
22
  init.body = JSON.stringify(body);
23
23
  }
@@ -53,9 +53,9 @@ export class LeadifyClient {
53
53
  const url = new URL(path, this.baseUrl);
54
54
  return this.request("PATCH", url, body);
55
55
  }
56
- async delete(path) {
56
+ async delete(path, body) {
57
57
  const url = new URL(path, this.baseUrl);
58
- return this.request("DELETE", url);
58
+ return this.request("DELETE", url, body);
59
59
  }
60
60
  }
61
61
  let _client = null;
package/dist/server.js CHANGED
@@ -14,16 +14,18 @@ import { registerPipelineTools } from "./tools/pipeline.js";
14
14
  import { registerFineTuningTools } from "./tools/fine_tuning.js";
15
15
  import { registerContextWorkspaceTools } from "./tools/context_workspace.js";
16
16
  import { registerLeadViewTools } from "./tools/views.js";
17
+ import { registerRelationshipTools } from "./tools/relationships.js";
17
18
  export function createServer() {
18
19
  const server = new McpServer({
19
20
  name: "leadify",
20
- version: "8.3.8",
21
+ version: "8.4.0",
21
22
  });
22
23
  registerAuthTools(server);
23
24
  registerOrganizationTools(server);
24
25
  registerLeadGroupTools(server);
25
26
  registerLeadViewTools(server);
26
27
  registerLeadTools(server);
28
+ registerRelationshipTools(server);
27
29
  registerSchemaTools(server);
28
30
  registerLogTools(server);
29
31
  registerCampaignTools(server);
@@ -2,6 +2,37 @@ import { z } from "zod";
2
2
  import { getClient } from "../client.js";
3
3
  import { toolResult, handleToolError } from "../types.js";
4
4
  const DEFAULT_LEAD_FIELDS = ["firstName", "lastName", "email", "company", "jobTitle", "linkedin"];
5
+ const canonicalLocationSchema = z.object({
6
+ city: z.string().trim().min(1).max(200),
7
+ region: z.string().trim().min(1).max(500).optional(),
8
+ postalCode: z.string().trim().min(1).max(500).optional(),
9
+ countryCode: z.string().trim().regex(/^[A-Za-z]{2}$/).transform((value) => value.toUpperCase()),
10
+ street: z.string().trim().min(1).max(500).optional(),
11
+ }).strict().describe("Canonical Leadify location source. city and ISO-2 countryCode are required; region, postalCode and street are optional. geo is system-owned and forbidden.");
12
+ const leadInputSchema = z.record(z.unknown()).superRefine((lead, context) => {
13
+ if (!Object.hasOwn(lead, "location"))
14
+ return;
15
+ const parsed = canonicalLocationSchema.safeParse(lead.location);
16
+ if (!parsed.success) {
17
+ for (const issue of parsed.error.issues) {
18
+ context.addIssue({ ...issue, path: ["location", ...issue.path] });
19
+ }
20
+ }
21
+ });
22
+ const leadUpdateSchema = z.object({
23
+ property_name: z.string().describe("Name of the field to update (e.g. 'email', 'status', 'card_analysis')."),
24
+ value: z.unknown().describe("New value for the field. location must use the canonical object; location.geo is forbidden."),
25
+ is_select: z.boolean().optional().describe("If true, the field is created/updated as a 'select' dropdown type. Use for categorical fields like status, source, etc."),
26
+ }).superRefine((update, context) => {
27
+ if (update.property_name !== "location")
28
+ return;
29
+ const parsed = canonicalLocationSchema.safeParse(update.value);
30
+ if (!parsed.success) {
31
+ for (const issue of parsed.error.issues) {
32
+ context.addIssue({ ...issue, path: ["value", ...issue.path] });
33
+ }
34
+ }
35
+ });
5
36
  function projectLead(lead, fields) {
6
37
  const source = lead && typeof lead === "object" ? lead : {};
7
38
  const data = source.data && typeof source.data === "object" ? source.data : {};
@@ -24,18 +55,19 @@ export function registerLeadTools(server, client = getClient()) {
24
55
  server.tool("add_leads", "Add one or more leads to a specific lead group in Leadify. Provide the group ID " +
25
56
  "and an array of lead objects with any combination of fields (email, firstName, " +
26
57
  "lastName, company, jobTitle, phone, custom fields, card_* fields, percent_* " +
27
- "fields, relation fields, etc.). Optionally specify which fields should be " +
58
+ "fields, etc.). Canonical relation fields are read-only here: use " +
59
+ "create_canonical_relationship or the migration tools instead. Optionally specify which fields should be " +
28
60
  "treated as 'select' dropdowns in the UI. Returns the count of successfully " +
29
61
  "imported leads and any errors. Use this for both bulk imports and single lead creation.", {
30
62
  lead_group_id: z
31
63
  .string()
32
64
  .describe("ID of the lead group to add leads to."),
33
65
  leads: z
34
- .array(z.record(z.unknown()))
66
+ .array(leadInputSchema)
35
67
  .describe("Array of lead objects. Each object is a key-value map of field names to values. " +
36
68
  "Common fields: email, firstName, lastName, company, jobTitle, phone, linkedin, " +
37
- "website, location, seniority, specialty, age. Also supports card_* fields, " +
38
- "percent_* fields, boolean flags (decisionMaking, excluded), and relation fields."),
69
+ "website, seniority, specialty, age. location must be {city, region?, postalCode?, countryCode, street?}; geo is computed by Leadify. Also supports card_* fields, " +
70
+ "percent_* fields and boolean flags (decisionMaking, excluded). Canonical relation fields are rejected."),
39
71
  is_select_fields: z
40
72
  .array(z.string())
41
73
  .optional()
@@ -149,23 +181,13 @@ export function registerLeadTools(server, client = getClient()) {
149
181
  server.tool("update_lead", "Update one or more fields on an existing lead. Each update specifies the field " +
150
182
  "name, new value, and optionally whether the field is a 'select' dropdown type. " +
151
183
  "Supports all field types including special fields (card_*, percent_*, boolean flags " +
152
- "like decisionMaking/excluded), and relation fields. Returns the updated lead. " +
184
+ "like decisionMaking/excluded). When a configured canonical relation field is changed, " +
185
+ "the backend routes it through the typed relationship service and keeps the JSON field read-only. Returns the updated lead. " +
186
+ "location must be the canonical object {city, region?, postalCode?, countryCode, street?}; never submit geo. " +
153
187
  "For bulk updates across many leads, call this tool once per lead.", {
154
188
  lead_id: z.string().describe("ID of the lead to update."),
155
189
  updates: z
156
- .array(z.object({
157
- property_name: z
158
- .string()
159
- .describe("Name of the field to update (e.g. 'email', 'status', 'card_analysis')."),
160
- value: z
161
- .unknown()
162
- .describe("New value for the field. Type depends on the field."),
163
- is_select: z
164
- .boolean()
165
- .optional()
166
- .describe("If true, the field is created/updated as a 'select' dropdown type. " +
167
- "Use for categorical fields like status, source, etc."),
168
- }))
190
+ .array(leadUpdateSchema)
169
191
  .min(1)
170
192
  .describe("Array of field updates to apply."),
171
193
  }, async ({ lead_id, updates }) => {
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerRelationshipTools(server: McpServer, client?: import("../client.js").LeadifyClient): void;
@@ -0,0 +1,124 @@
1
+ import { z } from "zod";
2
+ import { getClient } from "../client.js";
3
+ import { handleToolError, toolResult } from "../types.js";
4
+ const entityKind = z.enum(["ACCOUNT", "LEGAL_ENTITY", "ESTABLISHMENT", "PERSON", "EMPLOYMENT"]);
5
+ const relationshipType = z.enum(["EMPLOYED_BY", "AFFILIATED_WITH", "PART_OF", "COLLEAGUE_OF"]);
6
+ const entityRef = z.object({ kind: entityKind, id: z.string().min(1) }).strict();
7
+ const confidence = z.object({
8
+ value: z.number().min(0).max(1),
9
+ method: z.string().min(1).max(100),
10
+ assessed_at: z.string().datetime(),
11
+ }).strict();
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.", {
14
+ organization_id: z.string().min(1),
15
+ entity_kind: entityKind.optional(),
16
+ entity_id: z.string().min(1).optional(),
17
+ relationship_type: relationshipType.optional(),
18
+ state: z.enum(["ACTIVE", "TOMBSTONED"]).optional().default("ACTIVE"),
19
+ limit: z.number().int().min(1).max(500).optional().default(100),
20
+ cursor: z.string().optional(),
21
+ }, async ({ organization_id, entity_kind, entity_id, relationship_type, state, limit, cursor }) => {
22
+ try {
23
+ if (Boolean(entity_kind) !== Boolean(entity_id))
24
+ return toolResult({ error: "entity_kind and entity_id must be provided together" });
25
+ const params = new URLSearchParams({ organizationId: organization_id, state, limit: String(limit) });
26
+ if (entity_kind)
27
+ params.set("entityKind", entity_kind);
28
+ if (entity_id)
29
+ params.set("entityId", entity_id);
30
+ if (relationship_type)
31
+ params.set("relationshipType", relationship_type);
32
+ if (cursor)
33
+ params.set("cursor", cursor);
34
+ return toolResult(await client.get("/api/canonical-relationships", params));
35
+ }
36
+ catch (error) {
37
+ return handleToolError(error);
38
+ }
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.", {
41
+ organization_id: z.string().min(1), relationship_type: relationshipType,
42
+ source: entityRef, target: entityRef,
43
+ trust_state: z.enum(["CONFIRMED", "PROPOSED", "AMBIGUOUS", "CONFLICTED", "UNKNOWN"]).optional().default("UNKNOWN"),
44
+ confidence: confidence.optional(), evidence_ids: z.array(z.string()).max(100).optional().default([]),
45
+ valid_from: z.string().datetime().nullable().optional(), valid_to: z.string().datetime().nullable().optional(),
46
+ attributes: z.record(z.unknown()).optional(), idempotency_key: z.string().min(1).max(255),
47
+ }, async (params) => {
48
+ try {
49
+ return toolResult(await client.post("/api/canonical-relationships", {
50
+ organizationId: params.organization_id, relationshipType: params.relationship_type,
51
+ source: params.source, target: params.target, trustState: params.trust_state,
52
+ confidence: params.confidence ? { ...params.confidence, assessedAt: params.confidence.assessed_at, assessed_at: undefined } : undefined,
53
+ evidenceIds: params.evidence_ids, validFrom: params.valid_from, validTo: params.valid_to,
54
+ attributes: params.attributes, idempotencyKey: params.idempotency_key,
55
+ }));
56
+ }
57
+ catch (error) {
58
+ return handleToolError(error);
59
+ }
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.", {
62
+ organization_id: z.string().min(1), relationship_id: z.string().min(1),
63
+ relationship_type: relationshipType.optional(), source: entityRef.optional(), target: entityRef.optional(),
64
+ trust_state: z.enum(["CONFIRMED", "PROPOSED", "AMBIGUOUS", "CONFLICTED", "UNKNOWN"]).optional(),
65
+ state: z.enum(["ACTIVE", "TOMBSTONED"]).optional(), evidence_ids: z.array(z.string()).max(100).optional(),
66
+ valid_from: z.string().datetime().nullable().optional(), valid_to: z.string().datetime().nullable().optional(),
67
+ attributes: z.record(z.unknown()).nullable().optional(), idempotency_key: z.string().min(1).max(255),
68
+ }, async (params) => {
69
+ try {
70
+ return toolResult(await client.patch(`/api/canonical-relationships/${encodeURIComponent(params.relationship_id)}`, {
71
+ organizationId: params.organization_id, relationshipType: params.relationship_type,
72
+ source: params.source, target: params.target, trustState: params.trust_state, state: params.state,
73
+ evidenceIds: params.evidence_ids, validFrom: params.valid_from, validTo: params.valid_to,
74
+ attributes: params.attributes, idempotencyKey: params.idempotency_key,
75
+ }));
76
+ }
77
+ catch (error) {
78
+ return handleToolError(error);
79
+ }
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 }) => {
82
+ try {
83
+ return toolResult(await client.delete(`/api/canonical-relationships/${encodeURIComponent(relationship_id)}`, { organizationId: organization_id, idempotencyKey: idempotency_key }));
84
+ }
85
+ catch (error) {
86
+ return handleToolError(error);
87
+ }
88
+ });
89
+ const migrationFields = {
90
+ organization_id: z.string().min(1), source_group_id: z.string().min(1), field: z.string().min(1),
91
+ relationship_type: relationshipType, endpoint_role: z.enum(["SOURCE", "TARGET"]),
92
+ source_label: z.string().min(1).max(120), target_label: z.string().min(1).max(120),
93
+ limit: z.number().int().min(1).max(500).optional().default(500),
94
+ };
95
+ const migrationBody = (params) => ({
96
+ organizationId: params.organization_id, sourceGroupId: params.source_group_id, field: params.field,
97
+ relationshipType: params.relationship_type, endpointRole: params.endpoint_role,
98
+ sourceLabel: params.source_label, targetLabel: params.target_label, limit: params.limit,
99
+ });
100
+ server.tool("preview_canonical_relationship_migration", "Read-only planner for a legacy LeadGroup relation. Returns the exact forward/inverse divergence matrix, unresolved identities, quarantine candidates, and a digest. It creates no identities, links, placeholders, campaigns, or outreach.", migrationFields, async (params) => {
101
+ try {
102
+ return toolResult(await client.post("/api/canonical-relationships/migration/preview", migrationBody(params)));
103
+ }
104
+ catch (error) {
105
+ return handleToolError(error);
106
+ }
107
+ });
108
+ server.tool("apply_canonical_relationship_migration", "Apply exactly a previously reviewed canonical relationship migration plan. Requires its digest; the backend rejects drift, limits the batch, quarantines unresolved identities, and keeps external activation at zero.", { ...migrationFields, expected_plan_digest: z.string().regex(/^[a-f0-9]{64}$/) }, async (params) => {
109
+ try {
110
+ return toolResult(await client.post("/api/canonical-relationships/migration/apply", { ...migrationBody(params), expectedPlanDigest: params.expected_plan_digest }));
111
+ }
112
+ catch (error) {
113
+ return handleToolError(error);
114
+ }
115
+ });
116
+ server.tool("rollback_canonical_relationship_migration", "Tombstone every active relationship created by one migration digest. This is reversible and does not delete identities, evidence, leads, groups, or history.", { organization_id: z.string().min(1), plan_digest: z.string().regex(/^[a-f0-9]{64}$/), idempotency_key: z.string().min(1).max(200) }, async ({ organization_id, plan_digest, idempotency_key }) => {
117
+ try {
118
+ return toolResult(await client.post("/api/canonical-relationships/migration/rollback", { organizationId: organization_id, planDigest: plan_digest, idempotencyKey: idempotency_key }));
119
+ }
120
+ catch (error) {
121
+ return handleToolError(error);
122
+ }
123
+ });
124
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "8.3.9",
3
+ "version": "8.4.1",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",