@agifyai/leadify-mcp 6.0.1 → 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/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/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);
|