@agifyai/leadify-mcp 9.10.0 → 9.12.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
@@ -258,11 +258,11 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
258
258
  |------|-------------|
259
259
  | `test_api_key` | Vérifier que la clé API configurée est valide (health check). |
260
260
  | `list_lead_groups` | Lister les groupes accessibles d'une organisation explicitement sélectionnée, en vue compacte, avec le sous-menu de chacun (`folderId`, `folder`). `folder_id` garde exactement les groupes d'un sous-menu, `none` ceux qui n'en ont aucun. |
261
- | `get_lead_group` | Consulter les métadonnées compactes d'un groupe accessible par son ID, avec son sous-menu. |
261
+ | `get_lead_group` | Consulter les métadonnées compactes d'un groupe accessible par son ID, avec son sous-menu et `relations` : les groupes auxquels il est relié (`field`, `leadGroupId`, `canonicalType`, `canonicalEndpointRole`, `inverseField` s'il existe). |
262
262
  | `list_lead_group_folders` | Lister les sous-menus partagés de la barre latérale d'une organisation (nom, emoji, description, groupes). Toute l'équipe voit les mêmes. |
263
263
  | `create_lead_group_folder` / `update_lead_group_folder` / `delete_lead_group_folder` | Créer, renommer, changer l'emoji (caractère ou code) ou la description, ajouter ou retirer des groupes, supprimer un sous-menu. La suppression garde les groupes. Droit d'écriture requis. |
264
264
  | `move_lead_groups_to_folder` | Ranger des groupes dans un sous-menu, ou les en sortir avec `folder_id: null` ; tous bougent ou aucun. Un groupe est dans un seul sous-menu. |
265
- | `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. |
265
+ | `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. Pour un groupe PERSON, `employer_lead_group_id` le relie dès la création au groupe d'entreprises (COMPANY, même organisation). |
266
266
  | `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. |
267
267
  | `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`). |
268
268
  | `upsert_person_leads_with_employment` | Créer ou réconcilier atomiquement des Person Leads, PERSON, EMPLOYED_BY, projections et historique, sans effet commercial. |
@@ -352,7 +352,7 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
352
352
  | `preview_canonical_relationship_migration` | Prévisualiser une migration de champs historiques, divergences et quarantaines comprises, sans mutation. |
353
353
  | `apply_canonical_relationship_migration` | Appliquer exactement un plan prévisualisé et borné grâce à son digest. |
354
354
  | `rollback_canonical_relationship_migration` | Tombstoner les liens créés par un plan de migration précis. |
355
- | `configure_lead_group_relations` | Configurer, dans un tenant explicitement vérifié, la projection legacy relue par une migration canonique. |
355
+ | `configure_lead_group_relations` | Relier un groupe de personnes au groupe de leurs entreprises (ou deux groupes entre eux) dans une organisation vérifiée. Appelé sur le groupe de personnes ; l'inverse est écrit seul sur le groupe d'entreprises. `relations` remplace toute la configuration du groupe : renvoyer les relations lues avec `get_lead_group`. |
356
356
  | `preview_canonical_identity_backfill` | Prévisualiser le backfill d'identité tenant-scoped d'un groupe et de ses parents Company configurés. |
357
357
  | `apply_canonical_identity_backfill` | Appliquer exactement un backfill d'identité relu par digest, sans outreach ni activation. |
358
358
  | `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. |
@@ -386,6 +386,9 @@ Chaque entrée est enregistrée entièrement ou annulée. Aucun numéro légal n
386
386
  préparation juridique de l’entreprise n’est obligatoire.
387
387
 
388
388
  1. Choisir l’organisation, les groupes et la fiche entreprise exacts.
389
+ Vérifier avec `get_lead_group` que le groupe de personnes est relié au
390
+ groupe d’entreprises (voir « Relier un groupe de personnes à ses
391
+ entreprises » ci-dessous) ; sinon, le relier avant toute écriture.
389
392
  2. Créer la preuve avec `create_person_employment_evidence`, en conservant
390
393
  ses dates originales. Transmettre son identifiant dans `evidence_ids`.
391
394
  3. Appeler `upsert_person_leads_with_employment` avec les champs admis par
@@ -401,6 +404,25 @@ Tous les emplois restent accessibles, avec leurs dates et leur confiance.
401
404
  Une confiance inconnue ne signifie pas une absence de lien. L’entreprise
402
405
  utilisée pour une qualification doit être choisie explicitement.
403
406
 
407
+ #### Relier un groupe de personnes à ses entreprises (PRD-2811)
408
+
409
+ Un groupe de personnes (par exemple « Dirigeants ») doit être relié au
410
+ groupe de leurs entreprises. Sans cette liaison, la qualification refuse
411
+ les entreprises (`QUALIFICATION_EMPLOYEE_REQUIRED`) et les tableaux
412
+ n’affichent ni l’employeur d’une personne ni les employés d’une entreprise.
413
+
414
+ - À la création : `create_lead_group` avec `entity_kind: "PERSON"` et
415
+ `employer_lead_group_id: <groupe d’entreprises>` (groupe COMPANY de la
416
+ même organisation).
417
+ - Pour un groupe existant : `configure_lead_group_relations` sur le groupe
418
+ de personnes, avec `{ field: "parent_lead", lead_group_id: <groupe
419
+ d’entreprises>, canonical_type: "EMPLOYED_BY", canonical_endpoint_role:
420
+ "SOURCE", inverse_field: "child_leads" }`. L’inverse est écrit seul sur le
421
+ groupe d’entreprises. `relations` remplace toute la configuration : lire
422
+ d’abord les relations existantes avec `get_lead_group` et les renvoyer
423
+ avec la nouvelle.
424
+ - Vérifier ensuite avec `get_lead_group` sur les deux groupes.
425
+
404
426
  Pour modifier un emploi existant, utiliser `manage_person_employment` :
405
427
  `attach` ajoute sans fermer les autres emplois ; `end` termine une période ;
406
428
  `invalidate` conserve la trace d’un lien erroné ; `replace` remplace seulement
@@ -460,6 +482,19 @@ les réponses du serveur sont rendues telles quelles, en JSON compact. Routes :
460
482
  `POST /api/contact-state`, `GET /api/prospection-tracking/{follow-up-feed,activity,coverage}`,
461
483
  `GET /api/campaign-wizard/follow-up-cadence`, `GET /api/prospect-approach/{leadId}`.
462
484
 
485
+ ### Messages complets (PRD-2708)
486
+
487
+ Un message capté (email, LinkedIn, InMail, note d'invitation) est lu en entier.
488
+ `organizationContact.exchanges` rend chaque texte avec ses paragraphes ; les
489
+ textes d'une réponse partagent un budget de 30 000 caractères, les plus récents
490
+ d'abord, et un échange coupé par ce budget porte `textTruncated: true` et sa
491
+ longueur réelle `textLength`. `email_read_thread`, `linkedin_read_conversation`,
492
+ `unipile_read_messages` et `leadify_read_activity_feed` gardent 20 000
493
+ caractères par champ par défaut (`max_content_chars`, jusqu'à 100 000) ; un champ
494
+ plus long se termine par `… [truncated: N characters in total]`. `list_follow_ups`
495
+ accepte `include_text` pour ajouter le message entier (`lastMessage.text`) à côté
496
+ de son extrait de 500 caractères.
497
+
463
498
  ### Déplacer des prospects vers un autre groupe (PRD-2478)
464
499
 
465
500
  Pour réorganiser une base, **déplacer** les fiches plutôt que les copier
@@ -505,7 +540,9 @@ de 1 à 20 entreprises et un `request_id` UUID. Commencer avec `dry_run: true`
505
540
  (valeur par défaut), puis transmettre le `expectedSchemaHash` retourné dans
506
541
  `expected_schema_hash` avec `dry_run: false`. La clé doit avoir les droits
507
542
  `write` et `start_qualification`, et son propriétaire doit administrer
508
- l’organisation. Chaque entreprise doit avoir un employé vérifié et relié.
543
+ l’organisation. Le groupe d’entreprises doit être relié à un groupe de
544
+ personnes (`configure_lead_group_relations`), et chaque entreprise doit avoir
545
+ un employé vérifié et relié.
509
546
  Avant l’accord du pilote dans les réglages du groupe, deux entreprises distinctes
510
547
  au plus sont admises, même avec plusieurs appels. Une répétition conserve le
511
548
  même `request_id` et la même sélection. Aucun groupe entier n’est déduit.
@@ -74,7 +74,7 @@ const CONTEXT_MODULE_DESCRIPTION = {
74
74
  playbook: "The published sales PLAYBOOK (GTM) of the organisation.",
75
75
  qualification: "What is KNOWN about the person: qualification document, freshness, failures and any refresh requested. Read the company module for the company cards.",
76
76
  signals: "WHY NOW: recent signals about the person and her company, newest first. Paged.",
77
- history: "What was ALREADY SAID: messages exchanged with their text, known threads, the program's own messages, coverage. Newest first. Paged. The first page (no cursor) also carries organizationContact, outside the module version: alreadyInExchange, firstContactAllowed and exchanges[] captured across the whole organization, including emails and LinkedIn messages written by hand (at, channel, direction, origin MANUAL/CAMPAIGN/AGENT, author, subject, text).",
77
+ history: "What was ALREADY SAID: messages exchanged with their text, known threads, the program's own messages, coverage. Newest first. Paged. The first page (no cursor) also carries organizationContact, outside the module version: alreadyInExchange, firstContactAllowed and exchanges[] captured across the whole organization, including emails and LinkedIn messages written by hand (at, channel, direction, origin MANUAL/CAMPAIGN/AGENT, author, subject, text: the whole message with its paragraphs; when the 30,000-character budget of the response is reached, the oldest carry textTruncated and textLength).",
78
78
  crm: "CRM situation, exclusion and contact state: commercial status, reception, human takeover, thread-read readiness, last confirmed outbound and inbound. Check it before any first contact.",
79
79
  authorization: "What is PERMITTED: program state, caps, senders and the key's rights, with the authorization revision to copy into the decision.",
80
80
  };
@@ -95,7 +95,7 @@ export function registerAgentOutreachTools(server, client = getClient()) {
95
95
  const personSchema = z.object({ ...scope, lead_id: identifier }).strict();
96
96
  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)));
97
97
  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." +
98
- " Also organizationContact (whole organization, every mailbox and LinkedIn account of every member, not only the program senders): alreadyInExchange, firstContactAllowed, lastContactAt, reasonCode and exchanges[], the messages captured by Leadify, including emails and LinkedIn messages written by hand: at, channel, direction, kind, origin (MANUAL, CAMPAIGN, AGENT), author (member, Louise, Inconnu, or the person for a reply), subject and text (readable, quoted history removed, cut at 600 characters). firstContactAllowed=false forbids a first contact.", personSchema, v => client.get(`${programPath(v.program_id)}/contacts/${encoded(v.lead_id)}`, params(v.organization_id)));
98
+ " Also organizationContact (whole organization, every mailbox and LinkedIn account of every member, not only the program senders): alreadyInExchange, firstContactAllowed, lastContactAt, reasonCode and exchanges[], the messages captured by Leadify, including emails and LinkedIn messages written by hand: at, channel, direction, kind, origin (MANUAL, CAMPAIGN, AGENT), author (member, Louise, Inconnu, or the person for a reply), subject and text (the whole readable message, paragraphs kept, quoted history removed; the texts of one response share a 30,000-character budget, newest first, and an exchange cut by that budget carries textTruncated=true and its real textLength). firstContactAllowed=false forbids a first contact.", personSchema, v => client.get(`${programPath(v.program_id)}/contacts/${encoded(v.lead_id)}`, params(v.organization_id)));
99
99
  register("get_outreach_prospect_context", "LEGACY whole writing context, about 90 KB: runtimes that set aside tool results above 50,000 characters never see it. Agents must NOT call it: read get_outreach_context_summary, then the get_outreach_context_<module> tools. Kept for clients that can read very large results. 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"));
100
100
  // PRD-2632 : the same context in pieces. Summary first, then one tool per module.
101
101
  const contextPath = (v, tail) => `${programPath(v.program_id, true)}/people/${encoded(v.lead_id)}/context/${tail}`;
@@ -34,7 +34,14 @@ const backfillScope = {
34
34
  lead_ids: z.array(z.string().trim().min(1)).min(1).max(100),
35
35
  };
36
36
  export function registerCanonicalBackfillTools(server, client = getClient()) {
37
- server.tool("configure_lead_group_relations", "Configure the legacy projection that a canonical migration reads. The organization is mandatory and verified against both groups; canonical_type and canonical_endpoint_role must be specified together. This changes no lead, identity, canonical relationship, campaign, or outreach.", { organization_id: z.string().trim().min(1), lead_group_id: z.string().trim().min(1), relations: z.array(relation).max(100) }, async ({ organization_id, lead_group_id, relations }) => {
37
+ server.tool("configure_lead_group_relations", "Link (connect, pair) a person group to the company group of their employers, or set any other relation between two lead groups of the same organization. " +
38
+ "Common use: link a person group (for example \"Dirigeants\") to its company group. Call it on the person group with " +
39
+ "relations: [{ field: \"parent_lead\", lead_group_id: <company group id>, canonical_type: \"EMPLOYED_BY\", canonical_endpoint_role: \"SOURCE\", inverse_field: \"child_leads\" }]. " +
40
+ "Leadify writes the inverse relation on the company group by itself: do not call it again on the company group. " +
41
+ "Without this link, qualification refuses the companies (QUALIFICATION_EMPLOYEE_REQUIRED) and the tables show neither the employer of a person nor the employees of a company. " +
42
+ "relations replaces the whole relation configuration of the group: first read the existing relations with get_lead_group, then send them back together with the new one; a relation left out is removed. " +
43
+ "The organization is mandatory and verified against both groups; canonical_type and canonical_endpoint_role must be specified together. " +
44
+ "The same configuration is read by canonical relationship migrations. It changes no lead, identity, canonical relationship, campaign, or outreach.", { organization_id: z.string().trim().min(1), lead_group_id: z.string().trim().min(1), relations: z.array(relation).max(100) }, async ({ organization_id, lead_group_id, relations }) => {
38
45
  try {
39
46
  return toolResult(await client.put(`/api/lead-group/${encodeURIComponent(lead_group_id)}/relations`, {
40
47
  organizationId: organization_id,
@@ -3,8 +3,10 @@ import { getClient } from "../client.js";
3
3
  import { handleToolError, toolError, toolResult } from "../types.js";
4
4
  const DEFAULT_LIMIT = 25;
5
5
  const MAX_LIMIT = 50;
6
- const DEFAULT_CONTENT_LIMIT = 1_000;
7
- const MAX_CONTENT_LIMIT = 5_000;
6
+ // PRD-2708: a message is read whole by default (a real message is a few thousand
7
+ // characters at most); beyond the limit the field says it is cut, never silently.
8
+ const DEFAULT_CONTENT_LIMIT = 20_000;
9
+ const MAX_CONTENT_LIMIT = 100_000;
8
10
  const DEFAULT_FRESHNESS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
9
11
  const MAX_FRESHNESS_MAX_AGE_MS = 90 * 24 * 60 * 60 * 1_000;
10
12
  const MAX_TO_ADDRESSES = 10;
@@ -31,10 +33,10 @@ function stringValue(value, max = 500) {
31
33
  const trimmed = value.trim();
32
34
  if (!trimmed)
33
35
  return null;
34
- const marker = "… [truncated]";
35
- return trimmed.length > max
36
- ? `${trimmed.slice(0, Math.max(0, max - marker.length))}${marker}`
37
- : trimmed;
36
+ if (trimmed.length <= max)
37
+ return trimmed;
38
+ const marker = `… [truncated: ${trimmed.length} characters in total]`;
39
+ return `${trimmed.slice(0, Math.max(0, max - marker.length))}${marker}`;
38
40
  }
39
41
  function firstString(source, keys, max = 500) {
40
42
  return keys.map((key) => stringValue(source[key], max)).find((value) => value !== null) ?? null;
@@ -1036,7 +1038,7 @@ function readSchema() {
1036
1038
  page: z.number().int().positive().max(10_000).optional().describe("One-based page number when the source uses page pagination."),
1037
1039
  limit: z.number().int().min(1).max(MAX_LIMIT).optional().default(DEFAULT_LIMIT).describe(`Maximum records returned; hard-bounded to ${MAX_LIMIT}.`),
1038
1040
  cursor: z.string().trim().min(1).max(500).optional().describe("Opaque cursor returned by the previous page."),
1039
- max_content_chars: z.number().int().min(100).max(MAX_CONTENT_LIMIT).optional().default(DEFAULT_CONTENT_LIMIT).describe(`Maximum characters kept in each content field; hard-bounded to ${MAX_CONTENT_LIMIT}.`),
1041
+ max_content_chars: z.number().int().min(100).max(MAX_CONTENT_LIMIT).optional().default(DEFAULT_CONTENT_LIMIT).describe(`Maximum characters kept in each content field (message body, text, subject). Default ${DEFAULT_CONTENT_LIMIT}: a message is returned whole. A longer field ends with "… [truncated: N characters in total]"; raise this value, up to ${MAX_CONTENT_LIMIT}, to read it whole.`),
1040
1042
  freshness_max_age_ms: z.number().int().positive().max(MAX_FRESHNESS_MAX_AGE_MS).optional().default(DEFAULT_FRESHNESS_MAX_AGE_MS).describe("Maximum source age used by the common freshness projection; at most 90 days."),
1041
1043
  };
1042
1044
  }
@@ -15,7 +15,9 @@ export function registerLeadGroupTools(server, writeClient) {
15
15
  server.tool("create_lead_group", "Create a new lead group in the specified organization. A lead group holds leads, a " +
16
16
  "schema, and an optional persona assignment. Use list_organizations first to find the " +
17
17
  "organization_id if you don't have it. Assigning a persona at creation time scopes " +
18
- "scoring/messaging immediately.", {
18
+ "scoring/messaging immediately. For a PERSON group, pass employer_lead_group_id to link it at " +
19
+ "creation to the company group of their employers (otherwise link it afterwards with " +
20
+ "configure_lead_group_relations).", {
19
21
  name: z.string().describe("Group name."),
20
22
  organization_id: z
21
23
  .string()
@@ -53,6 +55,14 @@ export function registerLeadGroupTools(server, writeClient) {
53
55
  .nullable()
54
56
  .optional()
55
57
  .describe("Active offer ID to assign with persona_id. Must belong to the same organization."),
58
+ employer_lead_group_id: z
59
+ .string()
60
+ .trim()
61
+ .min(1)
62
+ .optional()
63
+ .describe("For a PERSON group: ID of the COMPANY group of the same organization that holds their employers. " +
64
+ "Links the new person group to that company group at creation, the same link as " +
65
+ "configure_lead_group_relations (parent_lead / child_leads, EMPLOYED_BY). Check it with get_lead_group."),
56
66
  }, async (params) => {
57
67
  try {
58
68
  const body = {
@@ -73,6 +83,8 @@ export function registerLeadGroupTools(server, writeClient) {
73
83
  body.personaId = params.persona_id;
74
84
  if (params.offer_id !== undefined)
75
85
  body.offerId = params.offer_id;
86
+ if (params.employer_lead_group_id !== undefined)
87
+ body.employerLeadGroupId = params.employer_lead_group_id;
76
88
  const data = await (writeClient ?? getClient()).post("/api/lead-group", body);
77
89
  return toolResult(data);
78
90
  }
@@ -10,6 +10,12 @@ export declare function leadGroupFolder(group: Record<string, unknown>): {
10
10
  emoji: unknown;
11
11
  } | null;
12
12
  };
13
+ /**
14
+ * Compact view of the groups this group is linked to (for example a person
15
+ * group linked to the company group of its employers). Display mapping
16
+ * details (targetField, displayField, canonicalLabel) stay out of the read.
17
+ */
18
+ export declare function compactLeadGroupRelations(relations: unknown): Record<string, unknown>[];
13
19
  /** Keep group reads navigable: never return schema, profiles, or lead data. */
14
20
  export declare function compactLeadGroup(group: unknown): Record<string, unknown>;
15
21
  export declare function registerOrganizationTools(server: McpServer, client?: Pick<LeadifyClient, "get" | "post" | "put">): void;
@@ -42,6 +42,24 @@ function listedLeadGroup(group) {
42
42
  ...leadGroupFolder(group),
43
43
  };
44
44
  }
45
+ /**
46
+ * Compact view of the groups this group is linked to (for example a person
47
+ * group linked to the company group of its employers). Display mapping
48
+ * details (targetField, displayField, canonicalLabel) stay out of the read.
49
+ */
50
+ export function compactLeadGroupRelations(relations) {
51
+ if (!Array.isArray(relations))
52
+ return [];
53
+ return relations
54
+ .filter((relation) => Boolean(relation) && typeof relation === "object")
55
+ .map((relation) => ({
56
+ field: relation.field,
57
+ leadGroupId: relation.leadGroupId,
58
+ canonicalType: relation.canonicalType ?? null,
59
+ canonicalEndpointRole: relation.canonicalEndpointRole ?? null,
60
+ ...(typeof relation.inverseField === "string" && relation.inverseField ? { inverseField: relation.inverseField } : {}),
61
+ }));
62
+ }
45
63
  /** Keep group reads navigable: never return schema, profiles, or lead data. */
46
64
  export function compactLeadGroup(group) {
47
65
  const source = group && typeof group === "object" ? group : {};
@@ -62,6 +80,7 @@ export function compactLeadGroup(group) {
62
80
  refreshEnabled: source.refreshEnabled ?? false,
63
81
  refreshFrequencyDays: source.refreshFrequencyDays ?? null,
64
82
  createdAt: source.createdAt ?? null,
83
+ relations: compactLeadGroupRelations(source.relations),
65
84
  ...leadGroupFolder(source),
66
85
  };
67
86
  }
@@ -132,10 +151,14 @@ export function registerOrganizationTools(server, client = getClient()) {
132
151
  }
133
152
  });
134
153
  server.tool("get_lead_group", "Read the compact metadata of one accessible lead group by ID. Use this after list_lead_groups " +
135
- "or discover_leadify_context to confirm its type and operational state. Returns only navigation " +
136
- "metadata (no lead records, schema, company profile, or persona payload), with its shared sidebar " +
137
- "folder (folderId, folder), and reports inaccessible " +
138
- "groups as not found, without revealing their organization.", {
154
+ "or discover_leadify_context to confirm its type and operational state, and to see which other groups " +
155
+ "this group is linked to: relations lists each link as { field, leadGroupId, canonicalType, " +
156
+ "canonicalEndpointRole, inverseField when present } (for example a person group linked to the company " +
157
+ "group of their employers with field parent_lead and canonicalType EMPLOYED_BY). An empty relations " +
158
+ "array means the group is linked to no other group; link it with configure_lead_group_relations, after " +
159
+ "reading these relations, since that tool replaces the whole list. Returns no lead records, schema, " +
160
+ "company profile, or persona payload; includes its shared sidebar folder (folderId, folder), and reports " +
161
+ "inaccessible groups as not found, without revealing their organization.", {
139
162
  lead_group_id: z.string().describe("Lead group ID returned by Leadify discovery or another authorized Leadify response."),
140
163
  }, async ({ lead_group_id }) => {
141
164
  try {
@@ -51,7 +51,8 @@ export function registerProspectionTrackingTools(server, client = getClient()) {
51
51
  "organization, in the same order as the Suivi page: most overdue first. Use it to pick who to follow up today; " +
52
52
  "in the DUE feed, people handled by a campaign or by Louise (AUTOMATED) and excluded people never appear. Each item: personId, " +
53
53
  "leadId, groupId, person (name, jobTitle), company, groups, due (channel, step FIRST_MESSAGE or FOLLOW_UP with its " +
54
- "index and total, dueAt, daysLate, acceptedAt, alsoDue) or reply (at, channel, daysWaiting), lastMessage, recentSignal, reason " +
54
+ "index and total, dueAt, daysLate, acceptedAt, alsoDue) or reply (at, channel, daysWaiting), lastMessage (a 500-character " +
55
+ "excerpt; include_text=true adds lastMessage.text, the whole message), recentSignal, reason " +
55
56
  "(EXCLUSION_ENDED or INVITATION_NOT_ACCEPTED) and href. The page also gives total (same filters) and nextUpcoming " +
56
57
  `when empty. Pages of ${FOLLOW_UP_FEED_PAGE_SIZE.default} by default (max ${FOLLOW_UP_FEED_PAGE_SIZE.max}): pass ` +
57
58
  "nextCursor unchanged as cursor, with the same filters, until it is null. Read get_contact_state before writing.", {
@@ -62,8 +63,10 @@ export function registerProspectionTrackingTools(server, client = getClient()) {
62
63
  group_id: id.optional().describe("Only people present in this lead group."),
63
64
  cursor: z.string().min(1).max(500).optional().describe("nextCursor from the previous page, unchanged. Omit for the first page."),
64
65
  limit: z.number().int().min(1).max(FOLLOW_UP_FEED_PAGE_SIZE.max).optional(),
65
- }, ({ organization_id, kind, member_user_id, channel, group_id, cursor, limit }) => client.get("/api/prospection-tracking/follow-up-feed", query({
66
+ include_text: z.boolean().optional().describe("Also return lastMessage.text, the whole message next to its 500-character excerpt. Off by default."),
67
+ }, ({ organization_id, kind, member_user_id, channel, group_id, cursor, limit, include_text }) => client.get("/api/prospection-tracking/follow-up-feed", query({
66
68
  organizationId: organization_id, kind, memberUserId: member_user_id, channel, groupId: group_id, cursor, limit,
69
+ includeText: include_text === undefined ? undefined : String(include_text),
67
70
  })));
68
71
  read("get_prospection_activity", "Counters and day-by-day series of the Leadify Suivi page (tab Activité) for a period: messagesSent (emails, " +
69
72
  "LinkedIn messages), invitations (sent, accepted, acceptanceRate), peopleContacted (total, new), replies (people, " +
@@ -5,6 +5,8 @@ export function registerQualificationRunTools(server, client = getClient()) {
5
5
  server.tool("start_qualification_run", "Validate or start qualification of an explicit list of 1 to 20 companies in one organization and group, " +
6
6
  "then their linked people. dry_run defaults to true: read preflight.snapshot.expectedSchemaHash, then " +
7
7
  "send it as expected_schema_hash with dry_run=false and the same request_id to launch. " +
8
+ "The company group must be linked to a person group (configure_lead_group_relations on the person group, " +
9
+ "checked with get_lead_group): without that link the server refuses with QUALIFICATION_EMPLOYEE_REQUIRED. " +
8
10
  "The server refuses companies without a verified employee and more than two distinct pilot companies " +
9
11
  "until a human approves the pilot for this group in Leadify. No outreach or campaign activation. " +
10
12
  "Keep request_id unchanged when retrying the same selection. Read the returned runId with get_qualification_run.", { organization_id: z.string().trim().min(1), lead_group_id: z.string().trim().min(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "9.10.0",
3
+ "version": "9.12.0",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",