@agifyai/leadify-mcp 8.5.2 → 8.5.4
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 +34 -7
- package/dist/server.js +2 -0
- package/dist/tools/campaigns.d.ts +2 -1
- package/dist/tools/campaigns.js +37 -7
- package/dist/tools/events.d.ts +2 -0
- package/dist/tools/events.js +340 -0
- package/dist/tools/leads.js +33 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,6 +121,7 @@ src/
|
|
|
121
121
|
└── tools/
|
|
122
122
|
├── auth.ts
|
|
123
123
|
├── leads.ts
|
|
124
|
+
├── events.ts
|
|
124
125
|
├── campaigns.ts
|
|
125
126
|
├── dataroom.ts
|
|
126
127
|
├── personas.ts
|
|
@@ -190,6 +191,29 @@ Conséquences pratiques :
|
|
|
190
191
|
|
|
191
192
|
---
|
|
192
193
|
|
|
194
|
+
## Sémantique des événements
|
|
195
|
+
|
|
196
|
+
Une participation est rattachée à l’entreprise canonique (`Account`), même
|
|
197
|
+
lorsqu’un `lead_id` est fourni au tool. `get_leads.event_filter` et
|
|
198
|
+
`list_event_participations` ajoutent explicitement `VERIFIED` lorsque le statut
|
|
199
|
+
n’est pas fourni. Pour relire des faits `UNVERIFIED`, `DISPUTED` ou `REFUTED`,
|
|
200
|
+
l’agent doit demander ces statuts explicitement. Une donnée incertaine ou
|
|
201
|
+
l’absence d’un enregistrement ne constitue jamais une preuve de présence ou de
|
|
202
|
+
non-participation.
|
|
203
|
+
|
|
204
|
+
Les corrections ajoutent un fait immuable et mettent à jour le snapshot
|
|
205
|
+
courant dans la même opération ; elles exigent un motif. Il n’existe aucun tool
|
|
206
|
+
de suppression de participation ou de preuve. Archiver un événement le retire
|
|
207
|
+
des sélecteurs courants sans effacer son historique. Les champs legacy
|
|
208
|
+
`congress_presence`, `next_congress`, `next_congress_date` et `card_congress`
|
|
209
|
+
restent disponibles pour compatibilité/export, mais ne sont jamais synchronisés
|
|
210
|
+
depuis ce modèle.
|
|
211
|
+
|
|
212
|
+
Pour `add_leads` et `update_lead`, `location` utilise l’objet canonique
|
|
213
|
+
`{ city, region?, postalCode?, countryCode, street? }`. `city` et le code pays
|
|
214
|
+
ISO-2 `countryCode` sont obligatoires. La projection `geo` est calculée par
|
|
215
|
+
Leadify et ne doit jamais être envoyée par un agent MCP.
|
|
216
|
+
|
|
193
217
|
## Tools disponibles
|
|
194
218
|
|
|
195
219
|
| Tool | Description |
|
|
@@ -200,14 +224,16 @@ Conséquences pratiques :
|
|
|
200
224
|
| `create_lead_group` | Créer un groupe et choisir son type canonique optionnel (`GENERIC`, `COMPANY` ou `PERSON`) via `entity_kind`. |
|
|
201
225
|
| `update_lead_group` | Modifier un groupe, y compris son `entity_kind`, avec réconciliation du schéma côté Leadify. |
|
|
202
226
|
| `add_leads` | Ajouter un ou plusieurs leads à un groupe. |
|
|
203
|
-
| `get_leads` | Rechercher et lister des leads avec filtres, recherche et pagination
|
|
227
|
+
| `get_leads` | Rechercher et lister des leads avec filtres, recherche et pagination, dont le critère composé `event_filter`. |
|
|
204
228
|
| `get_lead` | Récupérer les détails complets d'un lead par son ID. |
|
|
205
229
|
| `update_lead` | Mettre à jour un ou plusieurs champs d'un lead existant. |
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
|
|
230
|
+
| `list_events` | Lister les événements tenant-scoped ; les événements archivés sont masqués par défaut. |
|
|
231
|
+
| `get_event` | Lire un événement partagé et son état d’archivage. |
|
|
232
|
+
| `create_event` | Créer un événement partagé avec une clé d’idempotence. |
|
|
233
|
+
| `update_event` | Modifier ou archiver sans suppression un événement partagé, avec version attendue. |
|
|
234
|
+
| `list_event_participations` | Lister les participations d’entreprise ; filtre explicitement `VERIFIED` par défaut. |
|
|
235
|
+
| `get_event_participation` | Lire un snapshot courant et son historique ; `VERIFIED` par défaut, statut incertain à demander explicitement. |
|
|
236
|
+
| `record_event_participations` | Valider à blanc ou enregistrer un lot idempotent de 1 à 100 observations/corrections. |
|
|
211
237
|
| `preview_reset_sequence_messages` / `execute_reset_sequence_messages` | Prévisualiser puis effacer irréversiblement les messages générés d'un prospect, d'une liste explicite ou d'un groupe explicite. L'exécution exige le digest de preview et une clé d'idempotence. |
|
|
212
238
|
| `preview_reset_ai_fields` / `execute_reset_ai_fields` | Prévisualiser puis effacer irréversiblement les champs IA d'un prospect, d'une liste explicite ou d'un groupe explicite, sans toucher aux contacts, flags manuels, activités ou campagnes. |
|
|
213
239
|
| `delete_leads` | Supprimer définitivement des leads par leurs IDs. |
|
|
@@ -219,7 +245,8 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
|
|
|
219
245
|
| `get_campaign_logs` | Récupérer les logs de campagne avec filtres et pagination. |
|
|
220
246
|
| `delete_campaign_log` | Supprimer une entrée de log de campagne. |
|
|
221
247
|
| `update_campaign_stats` | Mettre à jour les statistiques d'email d'une campagne pour un groupe de leads. |
|
|
222
|
-
| `create_campaign` | Créer une campagne DRAFT mono-canal rattachée à un groupe de leads. `channel` (`LINKEDIN` ou `EMAIL`) est obligatoire
|
|
248
|
+
| `create_campaign` | Créer une campagne DRAFT mono-canal rattachée à un groupe de leads. `channel` (`LINKEDIN` ou `EMAIL`) est obligatoire ; `start_at`, `end_at` et `timezone` configurent la fenêtre métier. À `end_at`, Leadify ferme la campagne idempotemment, annule uniquement les jobs prouvablement pré-fournisseur et préserve les autres pour réconciliation. |
|
|
249
|
+
| `update_campaign_configuration` | Corriger le canal ou la fenêtre d'une campagne DRAFT encore ouverte, sans activation ni enrôlement. |
|
|
223
250
|
| `list_campaigns` | Lister compactement les campagnes d'une organisation explicitement sélectionnée et leur statut courant. |
|
|
224
251
|
| `get_campaign` | Récupérer les détails d'une campagne et ses KPIs temps réel. |
|
|
225
252
|
| `update_campaign_status` | Changer le statut d'une campagne (DRAFT, ACTIVE, PAUSED, COMPLETED). |
|
package/dist/server.js
CHANGED
|
@@ -16,6 +16,7 @@ import { registerContextWorkspaceTools } from "./tools/context_workspace.js";
|
|
|
16
16
|
import { registerLeadViewTools } from "./tools/views.js";
|
|
17
17
|
import { registerRelationshipTools } from "./tools/relationships.js";
|
|
18
18
|
import { registerCommercialTruthTools } from "./tools/commercial_truth.js";
|
|
19
|
+
import { registerEventTools } from "./tools/events.js";
|
|
19
20
|
import { MCP_SERVER_NAME, MCP_VERSION } from "./version.js";
|
|
20
21
|
export function createServer() {
|
|
21
22
|
const server = new McpServer({
|
|
@@ -28,6 +29,7 @@ export function createServer() {
|
|
|
28
29
|
registerLeadViewTools(server);
|
|
29
30
|
registerLeadTools(server);
|
|
30
31
|
registerRelationshipTools(server);
|
|
32
|
+
registerEventTools(server);
|
|
31
33
|
registerCommercialTruthTools(server);
|
|
32
34
|
registerSchemaTools(server);
|
|
33
35
|
registerLogTools(server);
|
|
@@ -2,5 +2,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { type LeadifyClient } from "../client.js";
|
|
3
3
|
type CampaignReadClient = Pick<LeadifyClient, "get">;
|
|
4
4
|
type CampaignWriteClient = Pick<LeadifyClient, "post">;
|
|
5
|
-
|
|
5
|
+
type CampaignMutationClient = Pick<LeadifyClient, "put">;
|
|
6
|
+
export declare function registerCampaignTools(server: McpServer, readClient?: CampaignReadClient, writeClient?: CampaignWriteClient, mutationClient?: CampaignMutationClient): void;
|
|
6
7
|
export {};
|
package/dist/tools/campaigns.js
CHANGED
|
@@ -19,7 +19,7 @@ function assertCampaignBelongsToOrganization(data, organizationId) {
|
|
|
19
19
|
}
|
|
20
20
|
return campaign;
|
|
21
21
|
}
|
|
22
|
-
export function registerCampaignTools(server, readClient, writeClient) {
|
|
22
|
+
export function registerCampaignTools(server, readClient, writeClient, mutationClient) {
|
|
23
23
|
// ── update_campaign_stats ──────────────────────────────────────────────
|
|
24
24
|
server.tool("update_campaign_stats", "Update email campaign statistics for a lead group. If the given campaignSlug " +
|
|
25
25
|
"doesn't exist in the group's stats yet, it is created; otherwise provided fields " +
|
|
@@ -88,26 +88,33 @@ export function registerCampaignTools(server, readClient, writeClient) {
|
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
// ── create_campaign ────────────────────────────────────────────────────
|
|
91
|
-
server.tool("create_campaign", "Create a mono-channel DRAFT campaign
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"campaign until that API support is released. Returns the new campaign ID; use " +
|
|
95
|
-
"update_campaign_status only after the campaign is fully configured and explicitly approved.", {
|
|
91
|
+
server.tool("create_campaign", "Create a mono-channel DRAFT campaign with an explicit channel, optional ISO-8601 start/end " +
|
|
92
|
+
"window, and IANA timezone. At endAt Leadify closes the campaign idempotently, cancels only " +
|
|
93
|
+
"pre-provider jobs, and preserves any provider-barrier job for reconciliation.", {
|
|
96
94
|
name: z.string().describe("Campaign name."),
|
|
97
95
|
lead_group_id: z.string().describe("ID of the lead group to associate."),
|
|
98
96
|
channel: z.enum(["LINKEDIN", "EMAIL"]).describe("Required effective delivery channel. Create separate campaigns for LinkedIn and email."),
|
|
97
|
+
start_at: z.string().datetime().optional().describe("Optional inclusive campaign start timestamp in ISO-8601 UTC."),
|
|
98
|
+
end_at: z.string().datetime().optional().describe("Optional exclusive campaign end timestamp in ISO-8601 UTC; must be after start_at."),
|
|
99
|
+
timezone: z.string().min(1).optional().describe("IANA timezone used for the campaign business window, for example Europe/Paris."),
|
|
99
100
|
description: z.string().optional().describe("Optional description."),
|
|
100
101
|
persona_id: z
|
|
101
102
|
.string()
|
|
102
103
|
.optional()
|
|
103
104
|
.describe("ID of the persona to assign (optional)."),
|
|
104
|
-
}, async ({ name, lead_group_id, channel, description, persona_id }) => {
|
|
105
|
+
}, async ({ name, lead_group_id, channel, start_at, end_at, timezone, description, persona_id }) => {
|
|
105
106
|
try {
|
|
106
107
|
const body = {
|
|
107
108
|
name,
|
|
108
109
|
leadGroupId: lead_group_id,
|
|
109
110
|
channel,
|
|
110
111
|
};
|
|
112
|
+
if (start_at !== undefined)
|
|
113
|
+
body.startAt = start_at;
|
|
114
|
+
if (end_at !== undefined)
|
|
115
|
+
body.endAt = end_at;
|
|
116
|
+
if (timezone !== undefined)
|
|
117
|
+
body.timezone = timezone;
|
|
111
118
|
if (description !== undefined)
|
|
112
119
|
body.description = description;
|
|
113
120
|
if (persona_id !== undefined)
|
|
@@ -119,6 +126,29 @@ export function registerCampaignTools(server, readClient, writeClient) {
|
|
|
119
126
|
return handleToolError(error);
|
|
120
127
|
}
|
|
121
128
|
});
|
|
129
|
+
server.tool("update_campaign_configuration", "Correct an open DRAFT campaign's channel or lifecycle window before activation. This tool refuses ACTIVE, PAUSED, COMPLETED, or closed campaigns and never activates or enrolls prospects.", {
|
|
130
|
+
id: z.string().describe("Campaign ID."),
|
|
131
|
+
channel: z.enum(["LINKEDIN", "EMAIL"]).optional(),
|
|
132
|
+
start_at: z.string().datetime().nullable().optional(),
|
|
133
|
+
end_at: z.string().datetime().nullable().optional(),
|
|
134
|
+
timezone: z.string().min(1).optional(),
|
|
135
|
+
}, async ({ id, channel, start_at, end_at, timezone }) => {
|
|
136
|
+
try {
|
|
137
|
+
const body = {};
|
|
138
|
+
if (channel !== undefined)
|
|
139
|
+
body.channel = channel;
|
|
140
|
+
if (start_at !== undefined)
|
|
141
|
+
body.startAt = start_at;
|
|
142
|
+
if (end_at !== undefined)
|
|
143
|
+
body.endAt = end_at;
|
|
144
|
+
if (timezone !== undefined)
|
|
145
|
+
body.timezone = timezone;
|
|
146
|
+
return toolResult(await (mutationClient ?? getClient()).put(`/api/campaign/${encodeURIComponent(id)}/configuration`, body));
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return handleToolError(error);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
122
152
|
// ── get_campaign ───────────────────────────────────────────────────────
|
|
123
153
|
server.tool("get_campaign", "Retrieve a campaign chosen from list_campaigns, scoped to the explicitly selected " +
|
|
124
154
|
"organization. Includes its normalized current status (DRAFT, ACTIVE, PAUSED, or COMPLETED) " +
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getClient } from "../client.js";
|
|
3
|
+
import { handleToolError, toolError, toolResult } from "../types.js";
|
|
4
|
+
const dateOnly = z.string()
|
|
5
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, "Use an ISO date in YYYY-MM-DD format.")
|
|
6
|
+
.refine((value) => {
|
|
7
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
8
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
|
|
9
|
+
}, "Use a real calendar date in YYYY-MM-DD format.");
|
|
10
|
+
const role = z.enum([
|
|
11
|
+
"EXHIBITOR",
|
|
12
|
+
"SPONSOR",
|
|
13
|
+
"SPEAKER",
|
|
14
|
+
"PARTNER",
|
|
15
|
+
"ORGANIZER",
|
|
16
|
+
"CONTRIBUTOR",
|
|
17
|
+
"PARTICIPANT",
|
|
18
|
+
"UNKNOWN",
|
|
19
|
+
"OTHER",
|
|
20
|
+
]);
|
|
21
|
+
const status = z.enum(["UNVERIFIED", "VERIFIED", "DISPUTED", "REFUTED"]);
|
|
22
|
+
const confidence = z.enum(["LOW", "MEDIUM", "HIGH"]);
|
|
23
|
+
const nullableLabel = z.string().trim().min(1).max(500).nullable().optional();
|
|
24
|
+
const nullableUrl = z.string().trim().url().max(2048).nullable().optional();
|
|
25
|
+
const idempotencyKey = z.string().trim().min(1).max(255);
|
|
26
|
+
const eventFields = {
|
|
27
|
+
name: z.string().trim().min(1).max(300),
|
|
28
|
+
edition: z.string().trim().min(1).max(200),
|
|
29
|
+
starts_on: dateOnly,
|
|
30
|
+
ends_on: dateOnly,
|
|
31
|
+
city: nullableLabel,
|
|
32
|
+
country_code: z.string().trim().regex(/^[A-Za-z]{2}$/).transform((value) => value.toUpperCase()).nullable().optional(),
|
|
33
|
+
venue: nullableLabel,
|
|
34
|
+
official_url: nullableUrl,
|
|
35
|
+
last_verified_on: dateOnly.nullable().optional(),
|
|
36
|
+
};
|
|
37
|
+
const createEventShape = {
|
|
38
|
+
organization_id: z.string().trim().min(1),
|
|
39
|
+
slug: z.string().trim().min(1).max(160).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
|
|
40
|
+
...eventFields,
|
|
41
|
+
idempotency_key: idempotencyKey,
|
|
42
|
+
};
|
|
43
|
+
const createEventInput = z.object(createEventShape).strict().superRefine((value, context) => {
|
|
44
|
+
if (value.ends_on < value.starts_on) {
|
|
45
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["ends_on"], message: "ends_on must be on or after starts_on." });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const updateEventShape = {
|
|
49
|
+
organization_id: z.string().trim().min(1),
|
|
50
|
+
event_id: z.string().trim().min(1),
|
|
51
|
+
name: eventFields.name.optional(),
|
|
52
|
+
edition: eventFields.edition.optional(),
|
|
53
|
+
starts_on: eventFields.starts_on.optional(),
|
|
54
|
+
ends_on: eventFields.ends_on.optional(),
|
|
55
|
+
city: eventFields.city,
|
|
56
|
+
country_code: eventFields.country_code,
|
|
57
|
+
venue: eventFields.venue,
|
|
58
|
+
official_url: eventFields.official_url,
|
|
59
|
+
last_verified_on: eventFields.last_verified_on,
|
|
60
|
+
archive: z.boolean().optional().default(false),
|
|
61
|
+
expected_version: z.number().int().positive(),
|
|
62
|
+
idempotency_key: idempotencyKey,
|
|
63
|
+
};
|
|
64
|
+
const updateEventInput = z.object(updateEventShape).strict().superRefine((value, context) => {
|
|
65
|
+
const mutable = ["name", "edition", "starts_on", "ends_on", "city", "country_code", "venue", "official_url", "last_verified_on"];
|
|
66
|
+
const supplied = mutable.filter((key) => value[key] !== undefined);
|
|
67
|
+
if (value.archive && supplied.length > 0) {
|
|
68
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["archive"], message: "Archive and field updates must be separate idempotent operations." });
|
|
69
|
+
}
|
|
70
|
+
if (!value.archive && supplied.length === 0) {
|
|
71
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "Provide at least one field to update, or set archive=true." });
|
|
72
|
+
}
|
|
73
|
+
if (value.starts_on && value.ends_on && value.ends_on < value.starts_on) {
|
|
74
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["ends_on"], message: "ends_on must be on or after starts_on." });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const participationItem = z.object({
|
|
78
|
+
event_id: z.string().trim().min(1),
|
|
79
|
+
account_id: z.string().trim().min(1).optional(),
|
|
80
|
+
lead_id: z.string().trim().min(1).optional(),
|
|
81
|
+
roles: z.array(role).min(1).max(20),
|
|
82
|
+
role_details: z.array(z.string().trim().min(1).max(500)).max(50).optional().default([]),
|
|
83
|
+
stands: z.array(z.string().trim().min(1).max(200)).max(50).optional().default([]),
|
|
84
|
+
source_urls: z.array(z.string().trim().url().max(2048)).max(50).optional().default([]),
|
|
85
|
+
source_provider: nullableLabel,
|
|
86
|
+
proof_excerpt: z.string().trim().min(1).max(20_000),
|
|
87
|
+
observed_on: dateOnly,
|
|
88
|
+
verified_on: dateOnly.nullable().optional(),
|
|
89
|
+
confidence,
|
|
90
|
+
status,
|
|
91
|
+
correction_reason: z.string().trim().min(1).max(5_000).optional(),
|
|
92
|
+
idempotency_key: idempotencyKey,
|
|
93
|
+
}).strict().superRefine((value, context) => {
|
|
94
|
+
if (Number(Boolean(value.account_id)) + Number(Boolean(value.lead_id)) !== 1) {
|
|
95
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "Provide exactly one account_id or lead_id." });
|
|
96
|
+
}
|
|
97
|
+
if (value.status === "VERIFIED" && value.source_urls.length === 0) {
|
|
98
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["source_urls"], message: "VERIFIED requires at least one source URL." });
|
|
99
|
+
}
|
|
100
|
+
if (value.status === "VERIFIED" && !value.verified_on) {
|
|
101
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["verified_on"], message: "VERIFIED requires verified_on." });
|
|
102
|
+
}
|
|
103
|
+
if (value.verified_on && value.verified_on < value.observed_on) {
|
|
104
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["verified_on"], message: "verified_on must be on or after observed_on." });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
function compact(value) {
|
|
108
|
+
return Object.fromEntries(Object.entries(value).filter(([, child]) => child !== undefined));
|
|
109
|
+
}
|
|
110
|
+
function setCsv(params, key, values) {
|
|
111
|
+
if (values?.length)
|
|
112
|
+
params.set(key, values.join(","));
|
|
113
|
+
}
|
|
114
|
+
function participationPayload(item) {
|
|
115
|
+
return compact({
|
|
116
|
+
eventId: item.event_id,
|
|
117
|
+
accountId: item.account_id,
|
|
118
|
+
leadId: item.lead_id,
|
|
119
|
+
roles: item.roles,
|
|
120
|
+
roleDetails: item.role_details,
|
|
121
|
+
stands: item.stands,
|
|
122
|
+
sourceUrls: item.source_urls,
|
|
123
|
+
sourceProvider: item.source_provider,
|
|
124
|
+
proofExcerpt: item.proof_excerpt,
|
|
125
|
+
observedOn: item.observed_on,
|
|
126
|
+
verifiedOn: item.verified_on,
|
|
127
|
+
confidence: item.confidence,
|
|
128
|
+
status: item.status,
|
|
129
|
+
correctionReason: item.correction_reason,
|
|
130
|
+
idempotencyKey: item.idempotency_key,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function filterFactHistory(data, acceptedStatuses) {
|
|
134
|
+
const projectParticipation = (value) => {
|
|
135
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
136
|
+
return value;
|
|
137
|
+
const participation = value;
|
|
138
|
+
if (!Array.isArray(participation.facts))
|
|
139
|
+
return participation;
|
|
140
|
+
const facts = participation.facts.filter((fact) => (fact && typeof fact === "object" && !Array.isArray(fact)
|
|
141
|
+
&& typeof fact.status === "string"
|
|
142
|
+
&& acceptedStatuses.includes(fact.status)));
|
|
143
|
+
return {
|
|
144
|
+
...participation,
|
|
145
|
+
facts,
|
|
146
|
+
hiddenFactCount: participation.facts.length - facts.length,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
150
|
+
return data;
|
|
151
|
+
const source = data;
|
|
152
|
+
if (Array.isArray(source.items)) {
|
|
153
|
+
return { ...source, items: source.items.map(projectParticipation) };
|
|
154
|
+
}
|
|
155
|
+
if (source.participation) {
|
|
156
|
+
return { ...source, participation: projectParticipation(source.participation) };
|
|
157
|
+
}
|
|
158
|
+
return source;
|
|
159
|
+
}
|
|
160
|
+
export function registerEventTools(server, client = getClient()) {
|
|
161
|
+
server.tool("list_events", "List tenant-scoped events. Archived events are hidden unless include_archived=true. An event alone is not evidence that a company attended; cite participation only from a matching VERIFIED participation.", {
|
|
162
|
+
organization_id: z.string().trim().min(1),
|
|
163
|
+
include_archived: z.boolean().optional().default(false),
|
|
164
|
+
search: z.string().trim().min(1).max(200).optional(),
|
|
165
|
+
editions: z.array(z.string().trim().min(1).max(200)).max(100).optional(),
|
|
166
|
+
starts_on_from: dateOnly.optional(),
|
|
167
|
+
starts_on_to: dateOnly.optional(),
|
|
168
|
+
limit: z.number().int().min(1).max(100).optional().default(50),
|
|
169
|
+
cursor: z.string().trim().min(1).optional(),
|
|
170
|
+
}, async ({ organization_id, include_archived, search, editions, starts_on_from, starts_on_to, limit, cursor }) => {
|
|
171
|
+
try {
|
|
172
|
+
const params = new URLSearchParams({
|
|
173
|
+
organizationId: organization_id,
|
|
174
|
+
includeArchived: String(include_archived ?? false),
|
|
175
|
+
limit: String(limit ?? 50),
|
|
176
|
+
});
|
|
177
|
+
if (search)
|
|
178
|
+
params.set("search", search);
|
|
179
|
+
setCsv(params, "editions", editions);
|
|
180
|
+
if (starts_on_from)
|
|
181
|
+
params.set("startsOnFrom", starts_on_from);
|
|
182
|
+
if (starts_on_to)
|
|
183
|
+
params.set("startsOnTo", starts_on_to);
|
|
184
|
+
if (cursor)
|
|
185
|
+
params.set("cursor", cursor);
|
|
186
|
+
return toolResult(await client.get("/api/events", params));
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
return handleToolError(error);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
server.tool("get_event", "Get one tenant-scoped event, including its archive state and current participation count. This does not establish that any particular company attended.", {
|
|
193
|
+
organization_id: z.string().trim().min(1),
|
|
194
|
+
event_id: z.string().trim().min(1),
|
|
195
|
+
}, async ({ organization_id, event_id }) => {
|
|
196
|
+
try {
|
|
197
|
+
return toolResult(await client.get(`/api/events/${encodeURIComponent(event_id)}`, new URLSearchParams({ organizationId: organization_id })));
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return handleToolError(error);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
server.tool("create_event", "Create one reusable tenant-scoped event. The idempotency key makes an identical retry safe; changing the payload under the same key is rejected. Creating an event does not create company participation evidence.", createEventShape, async (params) => {
|
|
204
|
+
try {
|
|
205
|
+
const input = createEventInput.parse(params);
|
|
206
|
+
return toolResult(await client.post("/api/events", compact({
|
|
207
|
+
organizationId: input.organization_id,
|
|
208
|
+
slug: input.slug,
|
|
209
|
+
name: input.name,
|
|
210
|
+
edition: input.edition,
|
|
211
|
+
startsOn: input.starts_on,
|
|
212
|
+
endsOn: input.ends_on,
|
|
213
|
+
city: input.city,
|
|
214
|
+
countryCode: input.country_code,
|
|
215
|
+
venue: input.venue,
|
|
216
|
+
officialUrl: input.official_url,
|
|
217
|
+
lastVerifiedOn: input.last_verified_on,
|
|
218
|
+
idempotencyKey: input.idempotency_key,
|
|
219
|
+
})));
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
return handleToolError(error);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
server.tool("update_event", "Update or archive one shared event with optimistic versioning and idempotency. Set archive=true as a separate operation with no field changes. This affects every company view that references the event; archiving is non-destructive and keeps all participation evidence.", updateEventShape, async (params) => {
|
|
226
|
+
try {
|
|
227
|
+
const input = updateEventInput.parse(params);
|
|
228
|
+
const body = input.archive
|
|
229
|
+
? {
|
|
230
|
+
organizationId: input.organization_id,
|
|
231
|
+
archive: true,
|
|
232
|
+
expectedVersion: input.expected_version,
|
|
233
|
+
idempotencyKey: input.idempotency_key,
|
|
234
|
+
}
|
|
235
|
+
: compact({
|
|
236
|
+
organizationId: input.organization_id,
|
|
237
|
+
name: input.name,
|
|
238
|
+
edition: input.edition,
|
|
239
|
+
startsOn: input.starts_on,
|
|
240
|
+
endsOn: input.ends_on,
|
|
241
|
+
city: input.city,
|
|
242
|
+
countryCode: input.country_code,
|
|
243
|
+
venue: input.venue,
|
|
244
|
+
officialUrl: input.official_url,
|
|
245
|
+
lastVerifiedOn: input.last_verified_on,
|
|
246
|
+
expectedVersion: input.expected_version,
|
|
247
|
+
idempotencyKey: input.idempotency_key,
|
|
248
|
+
});
|
|
249
|
+
return toolResult(await client.patch(`/api/events/${encodeURIComponent(input.event_id)}`, body));
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
return handleToolError(error);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
server.tool("list_event_participations", "List current company-event participation snapshots in one tenant. Defaults explicitly to VERIFIED only. To review uncertain evidence or uncertain historical facts, pass statuses containing UNVERIFIED, DISPUTED or REFUTED; unrequested fact statuses are hidden and counted. Never cite uncertain data, or the absence of a record, as proof of attendance or non-attendance.", {
|
|
256
|
+
organization_id: z.string().trim().min(1),
|
|
257
|
+
event_ids: z.array(z.string().trim().min(1)).max(100).optional(),
|
|
258
|
+
account_ids: z.array(z.string().trim().min(1)).max(100).optional(),
|
|
259
|
+
lead_id: z.string().trim().min(1).optional(),
|
|
260
|
+
editions: z.array(z.string().trim().min(1).max(200)).max(100).optional(),
|
|
261
|
+
roles: z.array(role).max(20).optional(),
|
|
262
|
+
statuses: z.array(status).min(1).max(10).optional().default(["VERIFIED"]),
|
|
263
|
+
confidences: z.array(confidence).max(10).optional(),
|
|
264
|
+
starts_on_from: dateOnly.optional(),
|
|
265
|
+
starts_on_to: dateOnly.optional(),
|
|
266
|
+
include_facts: z.boolean().optional().default(false),
|
|
267
|
+
limit: z.number().int().min(1).max(100).optional().default(50),
|
|
268
|
+
cursor: z.string().trim().min(1).optional(),
|
|
269
|
+
}, async ({ organization_id, event_ids, account_ids, lead_id, editions, roles, statuses, confidences, starts_on_from, starts_on_to, include_facts, limit, cursor }) => {
|
|
270
|
+
try {
|
|
271
|
+
const params = new URLSearchParams({
|
|
272
|
+
organizationId: organization_id,
|
|
273
|
+
statuses: (statuses ?? ["VERIFIED"]).join(","),
|
|
274
|
+
includeFacts: String(include_facts ?? false),
|
|
275
|
+
limit: String(limit ?? 50),
|
|
276
|
+
});
|
|
277
|
+
setCsv(params, "eventIds", event_ids);
|
|
278
|
+
setCsv(params, "accountIds", account_ids);
|
|
279
|
+
if (lead_id)
|
|
280
|
+
params.set("leadId", lead_id);
|
|
281
|
+
setCsv(params, "editions", editions);
|
|
282
|
+
setCsv(params, "roles", roles);
|
|
283
|
+
setCsv(params, "confidences", confidences);
|
|
284
|
+
if (starts_on_from)
|
|
285
|
+
params.set("startsOnFrom", starts_on_from);
|
|
286
|
+
if (starts_on_to)
|
|
287
|
+
params.set("startsOnTo", starts_on_to);
|
|
288
|
+
if (cursor)
|
|
289
|
+
params.set("cursor", cursor);
|
|
290
|
+
const acceptedStatuses = statuses ?? ["VERIFIED"];
|
|
291
|
+
return toolResult(filterFactHistory(await client.get("/api/event-participations", params), acceptedStatuses));
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
return handleToolError(error);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
server.tool("get_event_participation", "Get one explicitly selected current participation and its immutable fact history. Access defaults to VERIFIED only; pass statuses explicitly to review UNVERIFIED, DISPUTED or REFUTED facts. Uncertain evidence is not verified attendance, and missing data never means non-participation.", {
|
|
298
|
+
organization_id: z.string().trim().min(1),
|
|
299
|
+
participation_id: z.string().trim().min(1),
|
|
300
|
+
statuses: z.array(status).min(1).max(10).optional().default(["VERIFIED"]),
|
|
301
|
+
}, async ({ organization_id, participation_id, statuses }) => {
|
|
302
|
+
try {
|
|
303
|
+
const data = await client.get(`/api/event-participations/${encodeURIComponent(participation_id)}`, new URLSearchParams({ organizationId: organization_id }));
|
|
304
|
+
const source = data && typeof data === "object" ? data : {};
|
|
305
|
+
const participation = source.participation && typeof source.participation === "object"
|
|
306
|
+
? source.participation
|
|
307
|
+
: null;
|
|
308
|
+
const accepted = statuses ?? ["VERIFIED"];
|
|
309
|
+
if (!participation || typeof participation.status !== "string") {
|
|
310
|
+
return toolError("Leadify returned an invalid event participation response.");
|
|
311
|
+
}
|
|
312
|
+
if (!accepted.includes(participation.status)) {
|
|
313
|
+
return toolError(`Participation status ${participation.status} was not explicitly requested. Pass statuses including ${participation.status} to review this uncertain fact.`);
|
|
314
|
+
}
|
|
315
|
+
return toolResult(filterFactHistory(data, accepted));
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
return handleToolError(error);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
server.tool("record_event_participations", "Validate or record 1-100 company-event participation observations. Set validate_only=true for a no-write dry-run. Each item targets exactly one account_id or resolvable lead_id and has its own idempotency key. Corrections append an immutable fact and require correction_reason; there is no delete operation. VERIFIED requires a source URL, proof excerpt and verification date. Uncertain or absent data cannot be cited as attendance.", {
|
|
322
|
+
organization_id: z.string().trim().min(1),
|
|
323
|
+
batch_id: z.string().trim().min(1).max(255),
|
|
324
|
+
validate_only: z.boolean().optional().default(false),
|
|
325
|
+
items: z.array(participationItem).min(1).max(100),
|
|
326
|
+
}, async ({ organization_id, batch_id, validate_only, items }) => {
|
|
327
|
+
try {
|
|
328
|
+
const parsedItems = z.array(participationItem).min(1).max(100).parse(items);
|
|
329
|
+
return toolResult(await client.post("/api/event-participations/batch", {
|
|
330
|
+
organizationId: organization_id,
|
|
331
|
+
batchId: batch_id,
|
|
332
|
+
validateOnly: validate_only ?? false,
|
|
333
|
+
items: parsedItems.map(participationPayload),
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
return handleToolError(error);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
package/dist/tools/leads.js
CHANGED
|
@@ -2,6 +2,22 @@ import { z } from "zod";
|
|
|
2
2
|
import { getClient } from "../client.js";
|
|
3
3
|
import { toolResult, handleToolError } from "../types.js";
|
|
4
4
|
const DEFAULT_LEAD_FIELDS = ["firstName", "lastName", "email", "company", "jobTitle", "linkedin"];
|
|
5
|
+
const eventFilterSchema = z.object({
|
|
6
|
+
event_ids: z.array(z.string().trim().min(1)).max(100).optional(),
|
|
7
|
+
editions: z.array(z.string().trim().min(1).max(200)).max(100).optional(),
|
|
8
|
+
roles: z.array(z.enum([
|
|
9
|
+
"EXHIBITOR", "SPONSOR", "SPEAKER", "PARTNER", "ORGANIZER",
|
|
10
|
+
"CONTRIBUTOR", "PARTICIPANT", "UNKNOWN", "OTHER",
|
|
11
|
+
])).max(20).optional(),
|
|
12
|
+
statuses: z.array(z.enum(["UNVERIFIED", "VERIFIED", "DISPUTED", "REFUTED"])).min(1).max(10).optional(),
|
|
13
|
+
confidences: z.array(z.enum(["LOW", "MEDIUM", "HIGH"])).max(10).optional(),
|
|
14
|
+
starts_on_from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
15
|
+
starts_on_to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
16
|
+
}).strict().superRefine((value, context) => {
|
|
17
|
+
if (value.starts_on_from && value.starts_on_to && value.starts_on_to < value.starts_on_from) {
|
|
18
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["starts_on_to"], message: "starts_on_to must be on or after starts_on_from." });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
5
21
|
const canonicalLocationSchema = z.object({
|
|
6
22
|
city: z.string().trim().min(1).max(200),
|
|
7
23
|
region: z.string().trim().min(1).max(500).optional(),
|
|
@@ -143,7 +159,9 @@ export function registerLeadTools(server, client = getClient()) {
|
|
|
143
159
|
server.tool("get_leads", "Search and list leads from a Leadify lead group with optional filtering, search, " +
|
|
144
160
|
"full-text search, saved views, and compact field projections. Returns lead ID plus " +
|
|
145
161
|
"common identity fields by default. Pass `fields` for a workflow-specific projection " +
|
|
146
|
-
"and `include_schema=true` only when the schema is explicitly needed."
|
|
162
|
+
"and `include_schema=true` only when the schema is explicitly needed. event_filter " +
|
|
163
|
+
"uses one correlated company participation and defaults explicitly to VERIFIED; " +
|
|
164
|
+
"uncertain or absent data must never be cited as attendance or non-attendance.", {
|
|
147
165
|
group_id: z.string().describe("ID of the lead group to query."),
|
|
148
166
|
view_id: z
|
|
149
167
|
.string()
|
|
@@ -175,7 +193,8 @@ export function registerLeadTools(server, client = getClient()) {
|
|
|
175
193
|
"operator to value. Operators: equals, not_equals, contains, not_contains, " +
|
|
176
194
|
"gte, lte, is_true, is_false, is_null, is_not_null. " +
|
|
177
195
|
'Example: {"industry": {"contains": "tech"}, "company": {"is_not_null": "true"}}'),
|
|
178
|
-
|
|
196
|
+
event_filter: eventFilterSchema.optional().describe("Structured company-event participation filter. Values within one list use OR; event, edition, role, status, confidence and date dimensions use AND against the same participation. statuses defaults to [VERIFIED]. Pass uncertain statuses explicitly for review. There is no negative or absence filter."),
|
|
197
|
+
}, async ({ group_id, view_id, page, limit, search, fields, include_schema, filters, event_filter }) => {
|
|
179
198
|
try {
|
|
180
199
|
const params = new URLSearchParams();
|
|
181
200
|
params.set("groupId", group_id);
|
|
@@ -194,6 +213,18 @@ export function registerLeadTools(server, client = getClient()) {
|
|
|
194
213
|
}
|
|
195
214
|
}
|
|
196
215
|
}
|
|
216
|
+
if (event_filter) {
|
|
217
|
+
const checked = eventFilterSchema.parse(event_filter);
|
|
218
|
+
params.set("eventFilter", JSON.stringify({
|
|
219
|
+
...(checked.event_ids ? { eventIds: checked.event_ids } : {}),
|
|
220
|
+
...(checked.editions ? { editions: checked.editions } : {}),
|
|
221
|
+
...(checked.roles ? { roles: checked.roles } : {}),
|
|
222
|
+
statuses: checked.statuses ?? ["VERIFIED"],
|
|
223
|
+
...(checked.confidences ? { confidences: checked.confidences } : {}),
|
|
224
|
+
...(checked.starts_on_from ? { startsOnFrom: checked.starts_on_from } : {}),
|
|
225
|
+
...(checked.starts_on_to ? { startsOnTo: checked.starts_on_to } : {}),
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
197
228
|
const data = await client.get("/get-leads", params);
|
|
198
229
|
return toolResult(compactLeadsResponse(data, fields ?? DEFAULT_LEAD_FIELDS, include_schema));
|
|
199
230
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED