@agifyai/leadify-mcp 8.8.0 → 9.0.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
@@ -267,7 +267,7 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
267
267
  | `unenroll_campaign_audience` | Désenrôler uniquement la liste complète de `lead_ids` renvoyée par le preview courant. Relit et refuse toute sélection partielle, étendue ou périmée ; aucun envoi. |
268
268
  | `export_campaign` | Exporter les statistiques en CSV ; après clôture définitive, l'export provient du rapport final figé. |
269
269
  | `list_playbooks` / `get_playbook` | Lister et lire les « Messages types » (playbooks du rédacteur) d’une organisation, avec leur contenu complet à la dernière version. |
270
- | `publish_playbook` | Publier un message type : création, duplication ou nouvelle version (avec la version relue). Les refus du contrat du rédacteur sont rendus avec leur chemin. |
270
+ | `publish_playbook` | Publier un message type : création, duplication ou nouvelle version (avec la version relue), avec ses langues autorisées et sa langue de repli (`allowed_languages`, `fallback_language`, obligatoires à la création, gardées de la version précédente ou de l’original si absentes). Les refus du contrat du rédacteur sont rendus avec leur chemin. |
271
271
  | `list_campaign_senders` | Lister les comptes d’envoi (LinkedIn, boîtes mail) de la personne qui porte la clé : les messages partent à son nom. |
272
272
  | `get_campaign_audience_options` | Compter les personnes d’un groupe et de chacun de ses segments pour choisir l’audience d’une campagne. |
273
273
  | `get_contact_policy` | Lire la politique de contact effective de l’organisation ou d’une campagne, en lecture seule. |
@@ -329,8 +329,6 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
329
329
  | `update_outreach_urls` | Patch booking_url et/ou website_url uniquement. |
330
330
  | `set_outreach_connection_request` | Toggle du flag connectionRequestEnabled (LinkedIn invite vs cold DM). |
331
331
  | `trigger_outreach` | Générer un message via le runtime Write Outreach (dry-run par défaut). Le rédacteur libre peut fournir un `context_selection` complet Verticale–Offre–Persona ; aucun objet ou défaut n’est inféré. Avant une écriture, le runtime relit le digest et refuse toute persistance si le contexte a changé. |
332
- | `append_fine_tuning` | Appendre du contenu à une section du Fine Tuning (nonNegotiableRules, pitfalls, structure, examples). Toujours en mode append — garantie contractuelle. |
333
- | `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. |
334
332
  | `pipeline_next_lead` | Sélectionner le prochain lead à traiter (score descendant, sans message). Exclusion des IDs déjà vus, limit 1-5. |
335
333
 
336
334
  ### `source-icp-prospects` : sourcing Personne rattachée à une Company
package/dist/server.js CHANGED
@@ -12,7 +12,6 @@ import { registerPersonaTools } from "./tools/personas.js";
12
12
  import { registerLeadGroupTools } from "./tools/lead_groups.js";
13
13
  import { registerOrganizationTools } from "./tools/organizations.js";
14
14
  import { registerPipelineTools } from "./tools/pipeline.js";
15
- import { registerFineTuningTools } from "./tools/fine_tuning.js";
16
15
  import { registerContextWorkspaceTools } from "./tools/context_workspace.js";
17
16
  import { registerLeadViewTools } from "./tools/views.js";
18
17
  import { registerRelationshipTools } from "./tools/relationships.js";
@@ -52,6 +51,5 @@ export function createServer() {
52
51
  registerDataSourceTools(server);
53
52
  registerPersonaTools(server);
54
53
  registerPipelineTools(server);
55
- registerFineTuningTools(server);
56
54
  return server;
57
55
  }
@@ -19,11 +19,14 @@ function assertReviewMode(data) {
19
19
  }
20
20
  return data;
21
21
  }
22
+ /** Langues d’un message type, codes du produit Leadify (PRD-2231). */
23
+ const PLAYBOOK_LANGUAGES = ["fr", "en", "de", "es", "it"];
22
24
  export function registerCampaignWizardTools(server, injectedClient) {
23
25
  const client = () => injectedClient ?? getClient();
24
26
  // ── Messages types (playbooks) ─────────────────────────────────────────
25
27
  server.tool("list_playbooks", "List the organization's 'Messages types' (outreach playbooks) used by the outreach writer: id, name, kind " +
26
- "(EVENT or GENERIC), latest version, validity window, linked event, channels with steps, and campaign usage.", {
28
+ "(EVENT or GENERIC), latest version, validity window, linked event, channels with steps, campaign usage, and " +
29
+ "languages {allowedLanguages, fallbackLanguage} (null: no languages declared on this message type).", {
27
30
  organization_id: organizationId,
28
31
  include_archived: z.boolean().optional().default(false).describe("Include archived playbooks."),
29
32
  }, async ({ organization_id, include_archived }) => {
@@ -35,7 +38,7 @@ export function registerCampaignWizardTools(server, injectedClient) {
35
38
  }
36
39
  });
37
40
  server.tool("get_playbook", "Read one 'Message type' at its latest version, with its full editable content (intent, eligibility, " +
38
- "judgment guidance, promise, proofs, CTA, sequences and examples). Use it before publishing a new version.", {
41
+ "judgment guidance, promise, proofs, CTA, sequences and examples) and its languages. Use it before publishing a new version.", {
39
42
  organization_id: organizationId,
40
43
  playbook_id: z.string().min(1).describe("Playbook ID from list_playbooks."),
41
44
  }, async ({ organization_id, playbook_id }) => {
@@ -52,16 +55,39 @@ export function registerCampaignWizardTools(server, injectedClient) {
52
55
  "{startsAt?, endsAt?}, eligibility {all: [...]}, judgmentGuidance, promise, proofs [{id, statement, sourceUrl?}], " +
53
56
  "cta {description, url?}, sequences [{channel: EMAIL|LINKEDIN, steps: [{step, kind: EMAIL|LINKEDIN_INVITE|LINKEDIN_MESSAGE, delayDays, purpose}]}], " +
54
57
  "examples [{channel, step, subject?, body, commentary?}]. An EVENT playbook requires an eligibility condition " +
55
- "{type: 'event_participation', eventId, status: 'VERIFIED'}. Contract refusals are returned with their JSON path.", {
58
+ "{type: 'event_participation', eventId, status: 'VERIFIED'}. Languages: the message is written in the prospect's " +
59
+ "preferred language when it is in allowed_languages, otherwise in fallback_language. Both are required to create a message type; " +
60
+ "omitted on a new version or a duplicate, the previous or source languages are kept. Contract refusals are returned with their JSON path.", {
56
61
  organization_id: organizationId,
57
62
  kind: z.enum(["EVENT", "GENERIC"]).describe("EVENT requires a verified event participation; GENERIC does not. Fixed after creation."),
58
63
  content: z.record(z.unknown()).describe("Playbook content (see the tool description)."),
59
64
  playbook_id: z.string().min(1).optional().describe("Existing playbook to publish a new version of."),
60
65
  expected_version: z.number().int().min(1).optional().describe("Latest version read by get_playbook; required with playbook_id."),
61
66
  duplicated_from_id: z.string().min(1).optional().describe("Playbook duplicated into a new one."),
62
- }, async ({ organization_id, kind, content, playbook_id, expected_version, duplicated_from_id }) => {
67
+ allowed_languages: z
68
+ .array(z.enum(PLAYBOOK_LANGUAGES))
69
+ .min(1)
70
+ .optional()
71
+ .describe("Languages the message may be written in (fr, en, de, es, it). Requires fallback_language."),
72
+ fallback_language: z
73
+ .enum(PLAYBOOK_LANGUAGES)
74
+ .optional()
75
+ .describe("Language used when the prospect's preferred language is not allowed; one of allowed_languages."),
76
+ }, async ({ organization_id, kind, content, playbook_id, expected_version, duplicated_from_id, allowed_languages, fallback_language }) => {
63
77
  try {
78
+ if ((allowed_languages === undefined) !== (fallback_language === undefined)) {
79
+ return handleToolError(new Error("allowed_languages and fallback_language go together: set both, or neither to keep the previous languages."));
80
+ }
81
+ if (allowed_languages === undefined && playbook_id === undefined && duplicated_from_id === undefined) {
82
+ return handleToolError(new Error("A new message type needs its languages: set allowed_languages and fallback_language."));
83
+ }
84
+ if (allowed_languages !== undefined && fallback_language !== undefined && !allowed_languages.includes(fallback_language)) {
85
+ return handleToolError(new Error("fallback_language must be one of allowed_languages."));
86
+ }
64
87
  const body = { organizationId: organization_id, kind, content };
88
+ if (allowed_languages !== undefined && fallback_language !== undefined) {
89
+ body.languages = { allowedLanguages: allowed_languages, fallbackLanguage: fallback_language };
90
+ }
65
91
  if (playbook_id !== undefined)
66
92
  body.playbookId = playbook_id;
67
93
  if (expected_version !== undefined)
@@ -32,7 +32,8 @@ export function registerPipelineTools(server, client = getClient()) {
32
32
  "Vertical, Offer and Persona triplet. The backend requires all three objects to be " +
33
33
  "ACTIVE, tenant-scoped and linked; no default is inferred. Omit context_selection " +
34
34
  "to use the Lead Group-owned selection. The exact message-card set always comes " +
35
- "from Fine Tuning and cannot be selected through technical card names.", {
35
+ "from the campaign's message type (playbook), the only source of writing instructions, " +
36
+ "and cannot be selected through technical card names.", {
36
37
  lead_id: z.string().describe("ID of the lead to generate a message for."),
37
38
  dryrun: z
38
39
  .boolean()
@@ -103,39 +104,6 @@ export function registerPipelineTools(server, client = getClient()) {
103
104
  return handleToolError(error);
104
105
  }
105
106
  });
106
- // ── append_fine_tuning ───────────────────────────────────────────────────
107
- server.tool("append_fine_tuning", "Append content to a Fine Tuning section for a lead group. " +
108
- "ALWAYS in append mode — NEVER replaces existing content (contractual guarantee). " +
109
- "Use this to incrementally build up rules and examples as you iterate with the user. " +
110
- "Sections: nonNegotiableRules (imperative rules), pitfalls (traps to avoid), " +
111
- "structure (imposed message structure), examples (validated message samples). " +
112
- "For full rewrites, use the Leadify UI or the classic update endpoint.", {
113
- lead_group_id: z.string().describe("ID of the lead group."),
114
- section: z
115
- .enum(["nonNegotiableRules", "pitfalls", "structure", "examples"])
116
- .describe("Fine Tuning section to enrich."),
117
- content: z
118
- .string()
119
- .describe("Content to APPEND (not replace). Concatenated after existing content."),
120
- separator: z
121
- .string()
122
- .optional()
123
- .default("\n\n---\n\n")
124
- .describe("Separator between old and new content. Default: '\\n\\n---\\n\\n'."),
125
- }, async ({ lead_group_id, section, content, separator }) => {
126
- try {
127
- const data = await client.post("/api/pipeline/append-fine-tuning", {
128
- leadGroupId: lead_group_id,
129
- section,
130
- content,
131
- separator,
132
- });
133
- return toolResult(data);
134
- }
135
- catch (error) {
136
- return handleToolError(error);
137
- }
138
- });
139
107
  // ── pipeline_next_lead ───────────────────────────────────────────────────
140
108
  server.tool("pipeline_next_lead", "Select the next lead to process in the campaign pipeline. " +
141
109
  "Returns the hottest leads (highest score) that don't yet have a message " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agifyai/leadify-mcp",
3
- "version": "8.8.0",
3
+ "version": "9.0.0",
4
4
  "description": "MCP server for Leadify lead management API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,5 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { type LeadifyClient } from "../client.js";
3
- type FineTuningClient = Pick<LeadifyClient, "get" | "patch">;
4
- export declare function registerFineTuningTools(server: McpServer, client?: FineTuningClient): void;
5
- export {};
@@ -1,124 +0,0 @@
1
- import { z } from "zod";
2
- import { getClient } from "../client.js";
3
- import { toolResult, handleToolError } from "../types.js";
4
- const outputSettingSchema = z.discriminatedUnion("enabled", [
5
- z.object({ enabled: z.literal(true), count: z.number().int().min(1).max(5) }).strict(),
6
- z.object({ enabled: z.literal(false) }).strict(),
7
- ]);
8
- const outputConfigSchema = z.object({
9
- mode: z.literal("selected"),
10
- outputs: z.object({
11
- linkedinConnection: z.discriminatedUnion("enabled", [
12
- z.object({ enabled: z.literal(true), count: z.literal(1) }).strict(),
13
- z.object({ enabled: z.literal(false) }).strict(),
14
- ]),
15
- linkedinMessage: outputSettingSchema,
16
- email: outputSettingSchema,
17
- }).strict().refine((outputs) => Object.values(outputs).some((output) => output.enabled), "At least one output category must be enabled."),
18
- }).strict();
19
- // Dedicated tool for the Fine Tuning page (separate from OutreachSettings).
20
- // Both concepts live in the DB (FineTuning + OutreachSettings) and the API
21
- // keeps them strictly separated: this endpoint NEVER touches OutreachSettings.
22
- //
23
- // Two write tools coexist by design, with NON-OVERLAPPING contracts:
24
- // - append_fine_tuning → ALWAYS appends, never replaces. Contractual guarantee
25
- // for the iterative build-up pattern (skill operator
26
- // accumulates rules and examples as the user iterates).
27
- // - set_fine_tuning → WHOLESALE replace of ONE section. Use when you need
28
- // to wipe + rewrite a section cleanly (e.g. after a big
29
- // refactor, or to clear a section by passing empty
30
- // content).
31
- // The split exists so neither caller is ever confused about which behavior
32
- // they triggered.
33
- export function registerFineTuningTools(server, client) {
34
- // ── get_fine_tuning ───────────────────────────────────────────────────
35
- server.tool("get_fine_tuning", "Get the fine tuning configuration for a specific lead group. Returns exactly " +
36
- "the 5 levers of the Fine Tuning page and nothing else: " +
37
- "(1) nonNegotiableRules (Markdown), (2) pitfalls (Markdown), (3) structure (Markdown), " +
38
- "(4) supportedLanguages (string[]), (5) examples (Markdown), plus fallbackLanguage. " +
39
- "Read STRICTLY from the FineTuning model — zero OutreachSettings mixed in. " +
40
- "Use this when editing the Fine Tuning page or when an agent needs the copywriting " +
41
- "guardrails (rules + pitfalls + structure + examples + languages). " +
42
- "Upsert-on-read: if no FineTuning row exists for the lead group, one is created " +
43
- "with defaults, so the response shape is always stable.", {
44
- lead_group_id: z
45
- .string()
46
- .describe("ID of the lead group whose fine tuning configuration to fetch. " +
47
- "Pass the lead_group_id directly; if you need to resolve it, use " +
48
- "pipeline_next_lead (do NOT use get_leads — its payload is huge and is " +
49
- "banned in the outreach pipeline)."),
50
- }, async ({ lead_group_id }) => {
51
- try {
52
- const data = await (client ?? getClient()).get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/fine-tuning`);
53
- return toolResult(data);
54
- }
55
- catch (error) {
56
- return handleToolError(error);
57
- }
58
- });
59
- // ── set_fine_tuning ───────────────────────────────────────────────────
60
- // WHOLESALE replace of one Fine Tuning section. Distinct from
61
- // append_fine_tuning (which is append-only, contractual). Both tools
62
- // coexist intentionally — pick the right one for the intent.
63
- server.tool("set_fine_tuning", "WHOLESALE replace ONE section of the fine tuning configuration for a lead group. " +
64
- "The target section is fully overwritten with the new content. " +
65
- "This is the destructive counterpart of append_fine_tuning: use it when you need " +
66
- "to wipe and rewrite a section cleanly (big refactor, schema change, full clear). " +
67
- "SIBLING SECTIONS ARE NOT TOUCHED. Only the named section is replaced; the other 3 " +
68
- "Markdown sections, supportedLanguages, and fallbackLanguage stay intact. " +
69
- "To clear a section, pass content=\"\". " +
70
- "DO NOT use this for incremental edits — use append_fine_tuning instead so the " +
71
- "iterative build-up pattern is preserved (no accidental wipe of accumulated rules " +
72
- "or examples). " +
73
- "The 4 replaceable sections are: nonNegotiableRules, pitfalls, structure, examples. " +
74
- "Languages (supportedLanguages, fallbackLanguage) are managed separately via the " +
75
- "Fine Tuning UI or by a dedicated languages tool — this endpoint does not touch them. " +
76
- "Wired to PATCH /api/lead-group/{id}/fine-tuning/section/{section} per the Leadify API " +
77
- "contract: section is in the URL path (not the body), the request is a partial update " +
78
- "(not a wholesale row replace — for that, fall back to the Leadify UI or a follow-up " +
79
- "MCP tool, neither of which exists yet as of 3.6.1).", {
80
- lead_group_id: z
81
- .string()
82
- .describe("ID of the lead group whose fine tuning section to overwrite."),
83
- section: z
84
- .enum(["nonNegotiableRules", "pitfalls", "structure", "examples"])
85
- .describe("Which Fine Tuning section to replace. One of: " +
86
- "nonNegotiableRules | pitfalls | structure | examples. " +
87
- "Goes into the URL path as /section/{section} — not the request body."),
88
- content: z
89
- .string()
90
- .describe("New content for the section (Markdown). The section is replaced WHOLESALE — " +
91
- "any previous content is lost. Pass an empty string to clear the section."),
92
- }, async ({ lead_group_id, section, content }) => {
93
- try {
94
- const data = await (client ?? getClient()).patch(`/api/lead-group/${encodeURIComponent(lead_group_id)}/fine-tuning/section/${encodeURIComponent(section)}`, { content });
95
- return toolResult({
96
- ...(data && typeof data === "object" ? data : {}),
97
- _note: `Wholesale replace of section '${section}' for lead group ${lead_group_id}.`,
98
- });
99
- }
100
- catch (error) {
101
- return handleToolError(error);
102
- }
103
- });
104
- // ── set_fine_tuning_output_config ─────────────────────────────────────
105
- server.tool("set_fine_tuning_output_config", "Replace ONLY the business output configuration of a lead group's Fine Tuning. " +
106
- "Use this when its enabled output categories or quantities must change. " +
107
- "Accepts only the business categories linkedinConnection, linkedinMessage, and email; " +
108
- "it never accepts technical card or field names. A selected configuration must enable at " +
109
- "least one category; enabled linkedinConnection is exactly 1, while LinkedIn messages and " +
110
- "emails are 1–5. Markdown sections, supportedLanguages, and fallbackLanguage are preserved. " +
111
- "This only persists Fine Tuning configuration: it never generates outreach, schedules work, " +
112
- "activates campaigns, or sends messages. Returns the exact persisted Fine Tuning configuration.", {
113
- lead_group_id: z.string().describe("ID of the accessible lead group whose output configuration to update."),
114
- output_config: outputConfigSchema.describe("Selected business output configuration. Provide all three category keys and no technical card/field names."),
115
- }, async ({ lead_group_id, output_config }) => {
116
- try {
117
- const data = await (client ?? getClient()).patch(`/api/lead-group/${encodeURIComponent(lead_group_id)}/fine-tuning/output-config`, { outputConfig: output_config });
118
- return toolResult(data);
119
- }
120
- catch (error) {
121
- return handleToolError(error);
122
- }
123
- });
124
- }