@agifyai/leadify-mcp 6.0.0 → 7.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 +4 -1
- package/dist/tools/dataroom.js +9 -8
- package/dist/tools/fine_tuning.js +3 -1
- package/dist/tools/pipeline.js +7 -1
- package/dist/tools/signals.js +231 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,7 +195,10 @@ Conséquences pratiques :
|
|
|
195
195
|
| `get_campaign` | Récupérer les détails d'une campagne et ses KPIs temps réel. |
|
|
196
196
|
| `update_campaign_status` | Changer le statut d'une campagne (DRAFT, ACTIVE, PAUSED, COMPLETED). |
|
|
197
197
|
| `export_campaign` | Exporter les statistiques complètes d'une campagne en CSV. |
|
|
198
|
-
| `
|
|
198
|
+
| `signal_upsert` | Créer ou mettre à jour un signal de business intelligence (INFO, CRITICAL, GOLDEN). Remet le state à `active`. |
|
|
199
|
+
| `signal_expire` | Expirer un signal (événement périmé). Flip de `state` uniquement. |
|
|
200
|
+
| `signal_disable` | Désactiver un signal (faux positif / écarté manuellement). Flip de `state` uniquement. |
|
|
201
|
+
| `signal_delete` | Supprimer définitivement un signal (cas rare : donnée erronée, doublon). |
|
|
199
202
|
| `add_activity` | Journaliser une interaction prospect (LinkedIn, email, call) dans le feed du lead. |
|
|
200
203
|
| `get_data_room` | Récupérer la data room complète : infos société (v2), documents, personas. |
|
|
201
204
|
| `describe_company_info_schema` | Lire le schéma companyInfo v2 mirroré côté MCP (enums, max-lengths, sections, patch tool par section). À appeler avant la première update. |
|
package/dist/tools/dataroom.js
CHANGED
|
@@ -276,16 +276,17 @@ export function registerDataRoomTools(server) {
|
|
|
276
276
|
}
|
|
277
277
|
});
|
|
278
278
|
// ── get_outreach_context ──────────────────────────────────────────────
|
|
279
|
-
server.tool("get_outreach_context", "Get a
|
|
280
|
-
"
|
|
281
|
-
"
|
|
279
|
+
server.tool("get_outreach_context", "Get a lean outreach context for a lead group. Returns the shape " +
|
|
280
|
+
"`{ leadGroupId, leadGroupName, organizationId, company }` where `company` is the " +
|
|
281
|
+
"outreach-relevant slice of companyInfo (identity, pitch, constraints, curated " +
|
|
282
|
+
"products, economics triggers/baseline, product ICP roles, proof wording). " +
|
|
282
283
|
"Takes a lead_group_id (NOT organization_id) — the org is resolved server-side, " +
|
|
283
284
|
"eliminating the cross-org footgun of get_data_room. " +
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
285
|
+
"Does NOT require a persona: the group needs no linked persona and an empty data " +
|
|
286
|
+
"room returns success (not an error). No persona or outreach block is returned. " +
|
|
287
|
+
"Outreach configuration (positioning, tone, rules, templates, etc.) is managed " +
|
|
288
|
+
"exclusively via the Fine Tuning tools (append_fine_tuning / set_fine_tuning / " +
|
|
289
|
+
"get_fine_tuning). " +
|
|
289
290
|
"Use this instead of get_data_room + get_lead_group_persona in the outreach " +
|
|
290
291
|
"pipeline Phase 0.", {
|
|
291
292
|
lead_group_id: z.string().describe("ID of the lead group to resolve context for."),
|
|
@@ -29,7 +29,9 @@ export function registerFineTuningTools(server) {
|
|
|
29
29
|
lead_group_id: z
|
|
30
30
|
.string()
|
|
31
31
|
.describe("ID of the lead group whose fine tuning configuration to fetch. " +
|
|
32
|
-
"
|
|
32
|
+
"Pass the lead_group_id directly; if you need to resolve it, use " +
|
|
33
|
+
"pipeline_next_lead (do NOT use get_leads — its payload is huge and is " +
|
|
34
|
+
"banned in the outreach pipeline)."),
|
|
33
35
|
}, async ({ lead_group_id }) => {
|
|
34
36
|
try {
|
|
35
37
|
const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/fine-tuning`);
|
package/dist/tools/pipeline.js
CHANGED
|
@@ -21,7 +21,13 @@ export function registerPipelineTools(server) {
|
|
|
21
21
|
"`get_lead` call); the caller only needs `lead_id`. " +
|
|
22
22
|
"The response is decorated with a Trigger.dev `traceUrl` deep-link when missing " +
|
|
23
23
|
"(https://cloud.trigger.dev/runs/{runId}), so you can always jump to the trace. " +
|
|
24
|
-
"
|
|
24
|
+
"WAIT BEHAVIOR: this is a single blocking HTTP call to the backend with no " +
|
|
25
|
+
"client-side timeout — the wait is entirely server-side. The backend may return " +
|
|
26
|
+
"BEFORE the agent finishes (observed ~120s) with `status: EXECUTING` and a runId; " +
|
|
27
|
+
"this is NOT a failure — the run keeps going server-side, follow it via the " +
|
|
28
|
+
"traceUrl. In dry-run (dryrun=true) the call is safe to retry, since nothing is " +
|
|
29
|
+
"persisted; for production writes (dryrun=false) prefer following the trace over " +
|
|
30
|
+
"blindly retrying to avoid duplicate generation.", {
|
|
25
31
|
lead_id: z.string().describe("ID of the lead to generate a message for."),
|
|
26
32
|
dryrun: z
|
|
27
33
|
.boolean()
|
package/dist/tools/signals.js
CHANGED
|
@@ -1,47 +1,242 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getClient } from "../client.js";
|
|
3
|
-
import { toolResult, handleToolError } from "../types.js";
|
|
3
|
+
import { toolResult, toolError, handleToolError } from "../types.js";
|
|
4
|
+
// Anti-loop guardrail, NOT a business limit: bounds how many signal mutations
|
|
5
|
+
// a single server run (= one agent session) can perform.
|
|
6
|
+
const MAX_SIGNAL_MUTATIONS_PER_RUN = 20;
|
|
7
|
+
let signalMutationCount = 0;
|
|
8
|
+
function capExceeded() {
|
|
9
|
+
if (signalMutationCount >= MAX_SIGNAL_MUTATIONS_PER_RUN) {
|
|
10
|
+
return toolError(`Signal mutation cap reached (${MAX_SIGNAL_MUTATIONS_PER_RUN} per run). ` +
|
|
11
|
+
"This is an anti-loop guardrail, not a business limit. If you genuinely " +
|
|
12
|
+
"need more mutations, ask the user to restart the MCP session.");
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
// Best-effort: a feed failure must never fail the signal mutation itself.
|
|
17
|
+
async function appendFeed(entry) {
|
|
18
|
+
try {
|
|
19
|
+
await getClient().post("/activity-feed", { mode: "agent", ...entry });
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return error instanceof Error ? error.message : String(error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function mutationResult(data, feedError) {
|
|
27
|
+
if (feedError === null)
|
|
28
|
+
return toolResult(data);
|
|
29
|
+
return toolResult({
|
|
30
|
+
result: data,
|
|
31
|
+
warning: `Signal mutation succeeded but the activity-feed entry failed: ${feedError}`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const reasonParam = z
|
|
35
|
+
.string()
|
|
36
|
+
.describe("One factual sentence explaining WHY you are performing this mutation " +
|
|
37
|
+
"(e.g. 'Tender closed on 2026-05-30, budget consumed.'). Recorded in the " +
|
|
38
|
+
"lead's append-only activity feed.");
|
|
4
39
|
export function registerSignalTools(server) {
|
|
5
|
-
// ──
|
|
6
|
-
server.
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
// ── signal_upsert ──────────────────────────────────────────────────────
|
|
41
|
+
server.registerTool("signal_upsert", {
|
|
42
|
+
title: "Upsert a business signal",
|
|
43
|
+
description: "Create a new business intelligence signal on a lead, or update an existing " +
|
|
44
|
+
"one's content by `id`. Signals report key events picked up by agents — budget " +
|
|
45
|
+
"approvals, position changes, tender publications, etc. — and are shown in the " +
|
|
46
|
+
"lead's feed.\n\n" +
|
|
47
|
+
"WITHOUT `id` → creates a signal (`lead_id`, `level`, `title`, `content` required).\n" +
|
|
48
|
+
"WITH `id` → PARTIAL update of the existing signal: only the fields you pass are " +
|
|
49
|
+
"modified. Ids are server-generated — never invent one; an unknown `id` returns 404. " +
|
|
50
|
+
"`lead_id` is immutable after creation.\n\n" +
|
|
51
|
+
"Every upsert (re)sets the signal's state to `active`: updating an expired or " +
|
|
52
|
+
"disabled signal reactivates it — this is also the way to reactivate a signal. To " +
|
|
53
|
+
"expire or disable a signal use `signal_expire` / `signal_disable` instead.\n\n" +
|
|
54
|
+
"Levels: GOLDEN = major actionable buying-intent (use sparingly), CRITICAL = " +
|
|
55
|
+
"important info needing attention, INFO = monitoring-grade info.\n" +
|
|
56
|
+
"Always provide `ai_summary` when `content` exceeds ~3 sentences, otherwise the UI " +
|
|
57
|
+
"truncates `content`.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
id: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("ID of an existing signal to update. OMIT to create a new signal. " +
|
|
63
|
+
"Unknown id → 404 (ids are server-generated, never invent one)."),
|
|
64
|
+
lead_id: z
|
|
65
|
+
.string()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("ID of the target lead. Required when creating; immutable afterwards."),
|
|
68
|
+
level: z
|
|
69
|
+
.enum(["INFO", "CRITICAL", "GOLDEN"])
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Signal priority level. Required when creating."),
|
|
72
|
+
title: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Short one-line title (e.g. 'Budget IRM approuvé'). Required when creating."),
|
|
76
|
+
content: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Long markdown version: full details, sources, context. Required when creating."),
|
|
80
|
+
ai_summary: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("3-4 sentence markdown summary, shown by default in the UI with a 'see more' " +
|
|
84
|
+
"toggle expanding `content`."),
|
|
85
|
+
date: z
|
|
86
|
+
.string()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Date the business event actually occurred (≠ insertion date). Partial or full " +
|
|
89
|
+
"ISO 8601: 'YYYY' / 'YYYY-MM' / 'YYYY-MM-DD' / datetime. Use the finest " +
|
|
90
|
+
"precision you can reliably determine."),
|
|
91
|
+
source: z
|
|
92
|
+
.string()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("Human-readable source name ('LinkedIn', 'BOAMP', 'Les Echos'). Main badge in UI."),
|
|
95
|
+
agent_source: z
|
|
96
|
+
.string()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Technical identifier of the producing agent, 3-4 words max, kebab-case or " +
|
|
99
|
+
"space-separated (e.g. 'leadify qualification', 'budget-monitor')."),
|
|
100
|
+
source_url: z.string().url().optional().describe("URL of the original source."),
|
|
101
|
+
reason: reasonParam,
|
|
102
|
+
},
|
|
103
|
+
annotations: { idempotentHint: true },
|
|
104
|
+
}, async ({ id, lead_id, level, title, content, ai_summary, date, source, agent_source, source_url, reason, }) => {
|
|
105
|
+
const capError = capExceeded();
|
|
106
|
+
if (capError)
|
|
107
|
+
return capError;
|
|
32
108
|
try {
|
|
33
|
-
const body = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
109
|
+
const body = { state: "active" };
|
|
110
|
+
if (id !== undefined)
|
|
111
|
+
body.id = id;
|
|
112
|
+
if (lead_id !== undefined)
|
|
113
|
+
body.leadId = lead_id;
|
|
114
|
+
if (level !== undefined)
|
|
115
|
+
body.level = level;
|
|
116
|
+
if (title !== undefined)
|
|
117
|
+
body.title = title;
|
|
118
|
+
if (content !== undefined)
|
|
119
|
+
body.content = content;
|
|
120
|
+
if (ai_summary !== undefined)
|
|
121
|
+
body.aiSummary = ai_summary;
|
|
122
|
+
if (date !== undefined)
|
|
123
|
+
body.date = date;
|
|
124
|
+
if (source !== undefined)
|
|
125
|
+
body.source = source;
|
|
39
126
|
if (agent_source !== undefined)
|
|
40
127
|
body.agentSource = agent_source;
|
|
41
128
|
if (source_url !== undefined)
|
|
42
129
|
body.sourceUrl = source_url;
|
|
43
|
-
const data = await getClient().post("/
|
|
44
|
-
|
|
130
|
+
const data = (await getClient().post("/signals", body));
|
|
131
|
+
signalMutationCount++;
|
|
132
|
+
const feedLeadId = data.signal?.leadId ?? lead_id;
|
|
133
|
+
const feedError = feedLeadId
|
|
134
|
+
? await appendFeed({
|
|
135
|
+
leadId: feedLeadId,
|
|
136
|
+
action: data.created ? "signal_added" : "signal_updated",
|
|
137
|
+
target: data.signal?.id ?? id,
|
|
138
|
+
after: data.signal?.title ?? title,
|
|
139
|
+
reason,
|
|
140
|
+
})
|
|
141
|
+
: "leadId missing from upsert response";
|
|
142
|
+
return mutationResult(data, feedError);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return handleToolError(error);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
// ── signal_expire / signal_disable ─────────────────────────────────────
|
|
149
|
+
const stateFlips = [
|
|
150
|
+
{
|
|
151
|
+
name: "signal_expire",
|
|
152
|
+
title: "Expire a business signal",
|
|
153
|
+
state: "expired",
|
|
154
|
+
action: "signal_expired",
|
|
155
|
+
description: "Mark an existing signal as EXPIRED: the underlying business event is no longer " +
|
|
156
|
+
"current (budget consumed, tender closed, contact left the position...). The signal " +
|
|
157
|
+
"is kept for history but stops counting as active intelligence. This only flips the " +
|
|
158
|
+
"signal's lifecycle state — content is untouched. Re-upserting the signal with " +
|
|
159
|
+
"`signal_upsert` reactivates it. Do NOT use this for wrong/duplicate data — that is " +
|
|
160
|
+
"`signal_delete`.",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "signal_disable",
|
|
164
|
+
title: "Disable a business signal",
|
|
165
|
+
state: "disabled",
|
|
166
|
+
action: "signal_disabled",
|
|
167
|
+
description: "Mark an existing signal as DISABLED: the signal is set aside as a false positive " +
|
|
168
|
+
"or manually discarded (wrong interpretation, irrelevant to this lead...). The " +
|
|
169
|
+
"signal is kept for traceability but ignored by agents and the UI. This only flips " +
|
|
170
|
+
"the signal's lifecycle state — content is untouched. Re-upserting the signal with " +
|
|
171
|
+
"`signal_upsert` reactivates it. For factually wrong or duplicate data, prefer " +
|
|
172
|
+
"`signal_delete`.",
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
for (const flip of stateFlips) {
|
|
176
|
+
server.registerTool(flip.name, {
|
|
177
|
+
title: flip.title,
|
|
178
|
+
description: flip.description,
|
|
179
|
+
inputSchema: {
|
|
180
|
+
id: z.string().describe("ID of the signal. Unknown id → 404."),
|
|
181
|
+
reason: reasonParam,
|
|
182
|
+
},
|
|
183
|
+
annotations: { idempotentHint: true },
|
|
184
|
+
}, async ({ id, reason }) => {
|
|
185
|
+
const capError = capExceeded();
|
|
186
|
+
if (capError)
|
|
187
|
+
return capError;
|
|
188
|
+
try {
|
|
189
|
+
const data = (await getClient().post("/signals", {
|
|
190
|
+
id,
|
|
191
|
+
state: flip.state,
|
|
192
|
+
}));
|
|
193
|
+
signalMutationCount++;
|
|
194
|
+
const feedLeadId = data.signal?.leadId;
|
|
195
|
+
const feedError = feedLeadId
|
|
196
|
+
? await appendFeed({
|
|
197
|
+
leadId: feedLeadId,
|
|
198
|
+
action: flip.action,
|
|
199
|
+
target: id,
|
|
200
|
+
after: flip.state.toUpperCase(),
|
|
201
|
+
reason,
|
|
202
|
+
})
|
|
203
|
+
: "leadId missing from upsert response";
|
|
204
|
+
return mutationResult(data, feedError);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return handleToolError(error);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// ── signal_delete ──────────────────────────────────────────────────────
|
|
212
|
+
server.registerTool("signal_delete", {
|
|
213
|
+
title: "Delete a business signal (rare)",
|
|
214
|
+
description: "PERMANENTLY delete a signal. RARE case reserved for factually wrong data or " +
|
|
215
|
+
"duplicates. The normal lifecycle never deletes: an outdated signal becomes " +
|
|
216
|
+
"`expired` (`signal_expire`) and a false positive becomes `disabled` " +
|
|
217
|
+
"(`signal_disable`), both keeping history. Deletion is irreversible.",
|
|
218
|
+
inputSchema: {
|
|
219
|
+
id: z.string().describe("ID of the signal to delete. Unknown id → 404."),
|
|
220
|
+
lead_id: z
|
|
221
|
+
.string()
|
|
222
|
+
.describe("ID of the lead the signal belongs to (used for the activity feed)."),
|
|
223
|
+
reason: reasonParam,
|
|
224
|
+
},
|
|
225
|
+
annotations: { destructiveHint: true, idempotentHint: false },
|
|
226
|
+
}, async ({ id, lead_id, reason }) => {
|
|
227
|
+
const capError = capExceeded();
|
|
228
|
+
if (capError)
|
|
229
|
+
return capError;
|
|
230
|
+
try {
|
|
231
|
+
const data = await getClient().delete(`/signals/${encodeURIComponent(id)}`);
|
|
232
|
+
signalMutationCount++;
|
|
233
|
+
const feedError = await appendFeed({
|
|
234
|
+
leadId: lead_id,
|
|
235
|
+
action: "signal_deleted",
|
|
236
|
+
target: id,
|
|
237
|
+
reason,
|
|
238
|
+
});
|
|
239
|
+
return mutationResult(data, feedError);
|
|
45
240
|
}
|
|
46
241
|
catch (error) {
|
|
47
242
|
return handleToolError(error);
|