@agifyai/leadify-mcp 8.9.0 → 9.1.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 +3 -5
- package/dist/server.js +0 -2
- package/dist/tools/campaign_wizard.js +1 -1
- package/dist/tools/personas.js +65 -47
- package/dist/tools/pipeline.js +2 -34
- package/package.json +1 -1
- package/dist/tools/fine_tuning.d.ts +0 -5
- package/dist/tools/fine_tuning.js +0 -124
package/README.md
CHANGED
|
@@ -312,9 +312,9 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
|
|
|
312
312
|
| `apply_canonical_identity_backfill` | Appliquer exactement un backfill d'identité relu par digest, sans outreach ni activation. |
|
|
313
313
|
| `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. |
|
|
314
314
|
| `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. |
|
|
315
|
-
| `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`, `
|
|
316
|
-
| `create_persona_contract` | Créer un Persona tenant-scoped et lié à une Verticale depuis un contrat canonique v2 complet et strict. `tool_pack_id
|
|
317
|
-
| `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
|
|
315
|
+
| `list_persona_contracts` / `get_persona_contract` | Lister ou lire les contrats Persona canoniques v2 d’une organisation explicitement sélectionnée. Le contrat suit celui de Leadify : `intelligence` ne contient que `painPoints`, `outreach` que `outreachTemplates`, et les blocs retirés par PRD-1816 (`agentDomain`, `signalTierRules`, `enableSignals`, `icpStrategy`, `cardAnalysisSections`, `outreachSignalRouting`, `toneInstructions`, `outreachFields`) sont refusés à l’écriture et retirés à la lecture des anciens contrats ; `activeTools` n’appartient pas au contrat. |
|
|
316
|
+
| `create_persona_contract` | Créer un Persona tenant-scoped et lié à une Verticale depuis un contrat canonique v2 complet et strict. Aucun pack d’outils n’est demandé : `tool_pack_id`, s’il est encore envoyé, est ignoré et n’est jamais transmis à Leadify. Toute clé inconnue du contrat est refusée. |
|
|
317
|
+
| `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 ignoré, comme à la création. Une Persona ACTIVE mutée repasse en DRAFT. |
|
|
318
318
|
| `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. |
|
|
319
319
|
| `list_data_sources` | Lister toutes les sources de données configurées (par pays puis nom). |
|
|
320
320
|
| `create_data_source` | Créer une nouvelle source de données (admin uniquement). |
|
|
@@ -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
|
}
|
|
@@ -26,7 +26,7 @@ export function registerCampaignWizardTools(server, injectedClient) {
|
|
|
26
26
|
// ── Messages types (playbooks) ─────────────────────────────────────────
|
|
27
27
|
server.tool("list_playbooks", "List the organization's 'Messages types' (outreach playbooks) used by the outreach writer: id, name, kind " +
|
|
28
28
|
"(EVENT or GENERIC), latest version, validity window, linked event, channels with steps, campaign usage, and " +
|
|
29
|
-
"languages {allowedLanguages, fallbackLanguage} (null:
|
|
29
|
+
"languages {allowedLanguages, fallbackLanguage} (null: no languages declared on this message type).", {
|
|
30
30
|
organization_id: organizationId,
|
|
31
31
|
include_archived: z.boolean().optional().default(false).describe("Include archived playbooks."),
|
|
32
32
|
}, async ({ organization_id, include_archived }) => {
|
package/dist/tools/personas.js
CHANGED
|
@@ -6,15 +6,28 @@ import { LeadifyApiError, handleToolError, toolResult } from "../types.js";
|
|
|
6
6
|
* versioned `personaContract`. These tools deliberately do not expose any
|
|
7
7
|
* historical Persona field or field-specific patch operation.
|
|
8
8
|
*
|
|
9
|
-
* The published MCP contract must match the server `personaContractSchema
|
|
10
|
-
* `intelligence.activeTools`
|
|
11
|
-
*
|
|
9
|
+
* The published MCP contract must match the server `personaContractSchema`
|
|
10
|
+
* (Leadify `apps/server/src/lib/persona-contract.ts`). `intelligence.activeTools`
|
|
11
|
+
* is not part of that document, and the blocks removed by PRD-1816 are refused
|
|
12
|
+
* on write and stripped from older stored contracts on read.
|
|
13
|
+
*
|
|
14
|
+
* PRD-2295: since PRD-2238 Leadify no longer requires or reads a tool pack.
|
|
15
|
+
* `tool_pack_id` stays accepted on writes so older callers keep working, but it
|
|
16
|
+
* is ignored and never sent to the API.
|
|
12
17
|
*/
|
|
13
18
|
const tierSchema = z.object({
|
|
14
19
|
rule: z.string(),
|
|
15
20
|
expectedEvidence: z.string(),
|
|
16
21
|
examples: z.array(z.string()).max(20),
|
|
17
22
|
}).strict();
|
|
23
|
+
// PRD-1816: blocks Leadify removed from the contract.
|
|
24
|
+
const REMOVED_CONTRACT_KEYS = {
|
|
25
|
+
targeting: ["agentDomain"],
|
|
26
|
+
qualification: ["signalTierRules", "enableSignals"],
|
|
27
|
+
intelligence: ["icpStrategy", "cardAnalysisSections"],
|
|
28
|
+
outreach: ["outreachSignalRouting", "toneInstructions", "outreachFields"],
|
|
29
|
+
};
|
|
30
|
+
const REMOVED_TARGET_PROFILE_KEYS = ["buyerTypes", "coreDefinition", "productFamilies", "notTheDefinition"];
|
|
18
31
|
const commonContractSchema = z.object({
|
|
19
32
|
schemaVersion: z.literal(2),
|
|
20
33
|
kind: z.enum(["PERSON", "COMPANY"]),
|
|
@@ -22,9 +35,21 @@ const commonContractSchema = z.object({
|
|
|
22
35
|
targetProfile: z.unknown().nullable(),
|
|
23
36
|
disqualificationCriteria: z.unknown().nullable(),
|
|
24
37
|
schemaPacks: z.array(z.string()).max(2),
|
|
25
|
-
agentDomain: z.string().nullable(),
|
|
26
38
|
outputLanguage: z.string().nullable(),
|
|
27
|
-
}).strict(),
|
|
39
|
+
}).strict().superRefine((targeting, ctx) => {
|
|
40
|
+
const profile = targeting.targetProfile;
|
|
41
|
+
if (!profile || typeof profile !== "object" || Array.isArray(profile))
|
|
42
|
+
return;
|
|
43
|
+
for (const key of REMOVED_TARGET_PROFILE_KEYS) {
|
|
44
|
+
if (profile[key] !== undefined) {
|
|
45
|
+
ctx.addIssue({
|
|
46
|
+
code: z.ZodIssueCode.custom,
|
|
47
|
+
path: ["targetProfile", key],
|
|
48
|
+
message: `targeting.targetProfile.${key} was removed (PRD-1816)`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}),
|
|
28
53
|
qualification: z.object({
|
|
29
54
|
tiers: z.object({
|
|
30
55
|
hot: tierSchema,
|
|
@@ -38,19 +63,12 @@ const commonContractSchema = z.object({
|
|
|
38
63
|
level: z.enum(["GOLDEN", "CRITICAL", "STRONG", "INFO"]),
|
|
39
64
|
justification: z.string().min(1),
|
|
40
65
|
}).strict()).max(100),
|
|
41
|
-
signalTierRules: z.unknown().nullable(),
|
|
42
|
-
enableSignals: z.boolean(),
|
|
43
66
|
}).strict(),
|
|
44
67
|
intelligence: z.object({
|
|
45
68
|
painPoints: z.unknown().nullable(),
|
|
46
|
-
icpStrategy: z.unknown().nullable(),
|
|
47
|
-
cardAnalysisSections: z.unknown().nullable(),
|
|
48
69
|
}).strict(),
|
|
49
70
|
outreach: z.object({
|
|
50
|
-
outreachSignalRouting: z.unknown().nullable(),
|
|
51
71
|
outreachTemplates: z.unknown().nullable(),
|
|
52
|
-
toneInstructions: z.string().nullable(),
|
|
53
|
-
outreachFields: z.unknown().nullable(),
|
|
54
72
|
}).strict(),
|
|
55
73
|
}).strict();
|
|
56
74
|
const personExtensionSchema = z.object({
|
|
@@ -81,13 +99,38 @@ function withoutOutOfContractKeys(value) {
|
|
|
81
99
|
}
|
|
82
100
|
return contract;
|
|
83
101
|
}
|
|
102
|
+
/** Older stored contracts may still carry the PRD-1816 blocks: read them without those blocks. */
|
|
103
|
+
function withoutRemovedContractKeys(value) {
|
|
104
|
+
const contract = withoutOutOfContractKeys(value);
|
|
105
|
+
if (!contract || typeof contract !== "object" || Array.isArray(contract))
|
|
106
|
+
return contract;
|
|
107
|
+
const record = contract;
|
|
108
|
+
for (const [section, keys] of Object.entries(REMOVED_CONTRACT_KEYS)) {
|
|
109
|
+
const block = record[section];
|
|
110
|
+
if (!block || typeof block !== "object" || Array.isArray(block))
|
|
111
|
+
continue;
|
|
112
|
+
for (const key of keys)
|
|
113
|
+
delete block[key];
|
|
114
|
+
}
|
|
115
|
+
const targeting = record.targeting;
|
|
116
|
+
const profile = targeting?.targetProfile;
|
|
117
|
+
if (profile && typeof profile === "object" && !Array.isArray(profile)) {
|
|
118
|
+
for (const key of REMOVED_TARGET_PROFILE_KEYS)
|
|
119
|
+
delete profile[key];
|
|
120
|
+
}
|
|
121
|
+
return record;
|
|
122
|
+
}
|
|
84
123
|
function parsePersistedContract(value) {
|
|
85
|
-
return contractSchema.parse(
|
|
124
|
+
return contractSchema.parse(withoutRemovedContractKeys(value));
|
|
86
125
|
}
|
|
87
126
|
function parseWrittenContract(value) {
|
|
88
|
-
return
|
|
127
|
+
return contractSchema.parse(withoutOutOfContractKeys(value));
|
|
89
128
|
}
|
|
90
129
|
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.");
|
|
130
|
+
const ignoredToolPackId = z
|
|
131
|
+
.string()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe("Deprecated and ignored: Leadify no longer uses tool packs. Never sent to the API; omit it.");
|
|
91
134
|
const tenantScope = {
|
|
92
135
|
organization_id: z
|
|
93
136
|
.string()
|
|
@@ -102,12 +145,6 @@ const optimisticScope = {
|
|
|
102
145
|
.datetime({ offset: true })
|
|
103
146
|
.describe("Exact updatedAt obtained from a preceding read."),
|
|
104
147
|
};
|
|
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
|
-
}
|
|
111
148
|
function canonicalPersona(persona) {
|
|
112
149
|
if (!persona.personaContract || typeof persona.personaContract !== "object") {
|
|
113
150
|
throw new Error("PERSONA_CONTRACT_REQUIRED: run the audited Persona migration before using this MCP.");
|
|
@@ -118,7 +155,6 @@ function canonicalPersona(persona) {
|
|
|
118
155
|
name: persona.name,
|
|
119
156
|
description: persona.description ?? null,
|
|
120
157
|
verticalId: persona.verticalId,
|
|
121
|
-
toolPackId: persona.toolPackId ?? null,
|
|
122
158
|
updatedAt: persona.updatedAt,
|
|
123
159
|
revision: persona.revision ?? 0,
|
|
124
160
|
status: persona.status,
|
|
@@ -171,7 +207,6 @@ async function replaceContract(input) {
|
|
|
171
207
|
organizationId: input.organizationId,
|
|
172
208
|
verticalId: persona.verticalId,
|
|
173
209
|
expectedUpdatedAt: input.expectedUpdatedAt,
|
|
174
|
-
toolPackId: requireToolPackId(persona, input.toolPackId),
|
|
175
210
|
...(input.description !== undefined ? { description: input.description } : {}),
|
|
176
211
|
personaContract: parseWrittenContract(input.contract),
|
|
177
212
|
});
|
|
@@ -198,24 +233,19 @@ export function registerPersonaTools(server, injectedClient) {
|
|
|
198
233
|
return handleToolError(error);
|
|
199
234
|
}
|
|
200
235
|
});
|
|
201
|
-
server.tool("create_persona_contract", "Create a Persona from one complete canonical contract v2. intelligence.activeTools is not part of the contract.
|
|
236
|
+
server.tool("create_persona_contract", "Create a Persona from one complete canonical contract v2. intelligence.activeTools is not part of the contract. No tool pack is needed.", {
|
|
202
237
|
...tenantScope,
|
|
203
238
|
vertical_id: z.string().trim().min(1),
|
|
204
239
|
name: z.string().trim().min(1),
|
|
205
240
|
description: z.string().optional(),
|
|
206
|
-
tool_pack_id:
|
|
207
|
-
.string()
|
|
208
|
-
.trim()
|
|
209
|
-
.min(1)
|
|
210
|
-
.describe("Tenant-scoped tool pack. Not part of personaContract."),
|
|
241
|
+
tool_pack_id: ignoredToolPackId,
|
|
211
242
|
contract: contractInputSchema,
|
|
212
|
-
}, async ({ organization_id, vertical_id, name, description,
|
|
243
|
+
}, async ({ organization_id, vertical_id, name, description, contract }) => {
|
|
213
244
|
try {
|
|
214
245
|
return toolResult(await client.post("/api/persona", {
|
|
215
246
|
organizationId: organization_id,
|
|
216
247
|
verticalId: vertical_id,
|
|
217
248
|
name,
|
|
218
|
-
toolPackId: tool_pack_id,
|
|
219
249
|
...(description !== undefined ? { description } : {}),
|
|
220
250
|
personaContract: parseWrittenContract(contract),
|
|
221
251
|
}));
|
|
@@ -224,19 +254,14 @@ export function registerPersonaTools(server, injectedClient) {
|
|
|
224
254
|
return handleToolError(error);
|
|
225
255
|
}
|
|
226
256
|
});
|
|
227
|
-
server.tool("replace_persona_contract", "Replace the complete canonical contract using optimistic concurrency.
|
|
257
|
+
server.tool("replace_persona_contract", "Replace the complete canonical contract using optimistic concurrency. Mutating an ACTIVE Persona returns it to DRAFT; call change_persona_status to reactivate.", {
|
|
228
258
|
...optimisticScope,
|
|
229
259
|
id: z.string().min(1),
|
|
230
260
|
name: z.string().trim().min(1).optional(),
|
|
231
261
|
description: z.string().optional(),
|
|
232
|
-
tool_pack_id:
|
|
233
|
-
.string()
|
|
234
|
-
.trim()
|
|
235
|
-
.min(1)
|
|
236
|
-
.optional()
|
|
237
|
-
.describe("Optional pack change. When omitted, the persisted toolPackId is reused."),
|
|
262
|
+
tool_pack_id: ignoredToolPackId,
|
|
238
263
|
contract: contractInputSchema,
|
|
239
|
-
}, async ({ organization_id, id, expected_updated_at, name, description,
|
|
264
|
+
}, async ({ organization_id, id, expected_updated_at, name, description, contract }) => {
|
|
240
265
|
try {
|
|
241
266
|
return toolResult(await replaceContract({
|
|
242
267
|
organizationId: organization_id,
|
|
@@ -244,7 +269,6 @@ export function registerPersonaTools(server, injectedClient) {
|
|
|
244
269
|
expectedUpdatedAt: expected_updated_at,
|
|
245
270
|
name,
|
|
246
271
|
description,
|
|
247
|
-
toolPackId: tool_pack_id,
|
|
248
272
|
contract: parseWrittenContract(contract),
|
|
249
273
|
client,
|
|
250
274
|
}));
|
|
@@ -258,13 +282,8 @@ export function registerPersonaTools(server, injectedClient) {
|
|
|
258
282
|
id: z.string().min(1),
|
|
259
283
|
path: z.string().trim().min(1),
|
|
260
284
|
value: z.unknown(),
|
|
261
|
-
tool_pack_id:
|
|
262
|
-
|
|
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 }) => {
|
|
285
|
+
tool_pack_id: ignoredToolPackId,
|
|
286
|
+
}, async ({ organization_id, id, expected_updated_at, path, value }) => {
|
|
268
287
|
try {
|
|
269
288
|
const persona = await fetchPersona(id, organization_id, client);
|
|
270
289
|
const contract = canonicalPersona(persona).contract;
|
|
@@ -272,7 +291,6 @@ export function registerPersonaTools(server, injectedClient) {
|
|
|
272
291
|
organizationId: organization_id,
|
|
273
292
|
id,
|
|
274
293
|
expectedUpdatedAt: expected_updated_at,
|
|
275
|
-
toolPackId: tool_pack_id,
|
|
276
294
|
contract: contractSchema.parse(setContractPath(contract, path, value)),
|
|
277
295
|
client,
|
|
278
296
|
}));
|
package/dist/tools/pipeline.js
CHANGED
|
@@ -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
|
|
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,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
|
-
}
|