@agifyai/leadify-mcp 9.5.0 → 9.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -228,8 +228,11 @@ Leadify et ne doit jamais être envoyée par un agent MCP.
|
|
|
228
228
|
| Tool | Description |
|
|
229
229
|
|------|-------------|
|
|
230
230
|
| `test_api_key` | Vérifier que la clé API configurée est valide (health check). |
|
|
231
|
-
| `list_lead_groups` | Lister les groupes accessibles d'une organisation explicitement sélectionnée, en vue compacte. |
|
|
232
|
-
| `get_lead_group` | Consulter les métadonnées compactes d'un groupe accessible par son ID. |
|
|
231
|
+
| `list_lead_groups` | Lister les groupes accessibles d'une organisation explicitement sélectionnée, en vue compacte, avec le sous-menu de chacun (`folderId`, `folder`). `folder_id` garde exactement les groupes d'un sous-menu, `none` ceux qui n'en ont aucun. |
|
|
232
|
+
| `get_lead_group` | Consulter les métadonnées compactes d'un groupe accessible par son ID, avec son sous-menu. |
|
|
233
|
+
| `list_lead_group_folders` | Lister les sous-menus partagés de la barre latérale d'une organisation (nom, emoji, description, groupes). Toute l'équipe voit les mêmes. |
|
|
234
|
+
| `create_lead_group_folder` / `update_lead_group_folder` / `delete_lead_group_folder` | Créer, renommer, changer l'emoji (caractère ou code) ou la description, ajouter ou retirer des groupes, supprimer un sous-menu. La suppression garde les groupes. Droit d'écriture requis. |
|
|
235
|
+
| `move_lead_groups_to_folder` | Ranger des groupes dans un sous-menu, ou les en sortir avec `folder_id: null` ; tous bougent ou aucun. Un groupe est dans un seul sous-menu. |
|
|
233
236
|
| `create_lead_group` | Créer un groupe, choisir son type canonique optionnel et rattacher atomiquement `persona_id` avec l’`offer_id` ACTIVE du même tenant. |
|
|
234
237
|
| `update_lead_group` | Modifier un groupe avec réconciliation du schéma ; lorsque le serveur exige un contexte complet, transmettre ensemble `persona_id` et `offer_id` sans inférer l’offre. |
|
|
235
238
|
| `add_leads` | Import plat générique vers un groupe : ne crée aucune relation d’emploi, aucun rattachement parent ni identité canonique. Ne jamais l’utiliser pour rattacher une Personne à une Company (voir `upsert_person_leads_with_employment`). |
|
package/dist/server.js
CHANGED
|
@@ -12,6 +12,7 @@ import { registerActivityTools } from "./tools/activities.js";
|
|
|
12
12
|
import { registerDataSourceTools } from "./tools/datasources.js";
|
|
13
13
|
import { registerPersonaTools } from "./tools/personas.js";
|
|
14
14
|
import { registerLeadGroupTools } from "./tools/lead_groups.js";
|
|
15
|
+
import { registerLeadGroupFolderTools } from "./tools/lead_group_folders.js";
|
|
15
16
|
import { registerOrganizationTools } from "./tools/organizations.js";
|
|
16
17
|
import { registerPipelineTools } from "./tools/pipeline.js";
|
|
17
18
|
import { registerContextWorkspaceTools } from "./tools/context_workspace.js";
|
|
@@ -34,6 +35,7 @@ export function createServer() {
|
|
|
34
35
|
registerAuthTools(server);
|
|
35
36
|
registerOrganizationTools(server);
|
|
36
37
|
registerLeadGroupTools(server);
|
|
38
|
+
registerLeadGroupFolderTools(server);
|
|
37
39
|
registerLeadViewTools(server);
|
|
38
40
|
registerLeadTools(server);
|
|
39
41
|
registerLeadMoveTools(server);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type LeadifyClient } from "../client.js";
|
|
3
|
+
/** Accepts "🇩🇪" or its code "1f1e9-1f1ea"; Leadify stores the code. */
|
|
4
|
+
export declare function emojiCode(value: string): string;
|
|
5
|
+
export declare function registerLeadGroupFolderTools(server: McpServer, client?: Pick<LeadifyClient, "get" | "post" | "put" | "delete">): void;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getClient } from "../client.js";
|
|
3
|
+
import { toolResult, handleToolError } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* PRD-2480: the shared lead group folders ("sous-menus" of the Leadify
|
|
6
|
+
* sidebar, PRD-2471). One organization shares them; a group sits in one
|
|
7
|
+
* folder at most. Every change here is what the whole team sees in Leadify.
|
|
8
|
+
*/
|
|
9
|
+
const UNIFIED = /^[a-f0-9]{2,6}(?:-[a-f0-9]{2,6}){0,15}$/i;
|
|
10
|
+
/** Accepts "🇩🇪" or its code "1f1e9-1f1ea"; Leadify stores the code. */
|
|
11
|
+
export function emojiCode(value) {
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (UNIFIED.test(trimmed))
|
|
14
|
+
return trimmed.toLowerCase();
|
|
15
|
+
return Array.from(trimmed).map((char) => char.codePointAt(0).toString(16)).join("-");
|
|
16
|
+
}
|
|
17
|
+
const emoji = z.string().trim().min(1).max(64)
|
|
18
|
+
.describe("Emoji shown before the folder name, as the character (🇩🇪) or its code (1f1e9-1f1ea).");
|
|
19
|
+
const groupIds = z.array(z.string().trim().min(1)).max(500);
|
|
20
|
+
const folderId = z.string().trim().min(1).describe("Folder ID from list_lead_group_folders.");
|
|
21
|
+
const path = (id) => `/api/lead-group-folders/${encodeURIComponent(id)}`;
|
|
22
|
+
export function registerLeadGroupFolderTools(server, client = getClient()) {
|
|
23
|
+
server.tool("list_lead_group_folders", "List the shared folders (sidebar sub-menus such as \"🇩🇪 DACH\") of one organization, each with its " +
|
|
24
|
+
"name, emoji code, description and the ids of the lead groups it holds. The whole team sees the same " +
|
|
25
|
+
"folders in Leadify. To list the groups of one folder with their details, use list_lead_groups with folder_id.", {
|
|
26
|
+
organization_id: z.string().trim().min(1).optional()
|
|
27
|
+
.describe("Organization ID from discover_leadify_context. Required when the key reaches several organizations."),
|
|
28
|
+
}, async ({ organization_id }) => {
|
|
29
|
+
try {
|
|
30
|
+
const params = organization_id ? new URLSearchParams({ organizationId: organization_id }) : undefined;
|
|
31
|
+
return toolResult(await client.get("/api/lead-group-folders", params));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return handleToolError(error);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
server.tool("create_lead_group_folder", "Create a shared folder in the Leadify sidebar of one organization, optionally with groups already in it. " +
|
|
38
|
+
"A group sits in one folder at most: listed groups leave their previous folder. Needs write permission. " +
|
|
39
|
+
"Visible at once to the whole team.", {
|
|
40
|
+
organization_id: z.string().trim().min(1).optional()
|
|
41
|
+
.describe("Organization ID. Required when the key reaches several organizations."),
|
|
42
|
+
name: z.string().trim().min(1).max(100).describe("Folder name, e.g. DACH."),
|
|
43
|
+
emoji: emoji.optional(),
|
|
44
|
+
description: z.string().trim().max(2000).optional(),
|
|
45
|
+
lead_group_ids: groupIds.optional().describe("Lead groups of this organization to put in the folder."),
|
|
46
|
+
}, async ({ organization_id, name, emoji: icon, description, lead_group_ids }) => {
|
|
47
|
+
try {
|
|
48
|
+
return toolResult(await client.post("/api/lead-group-folders", {
|
|
49
|
+
...(organization_id ? { organizationId: organization_id } : {}),
|
|
50
|
+
name,
|
|
51
|
+
...(icon ? { emoji: emojiCode(icon) } : {}),
|
|
52
|
+
...(description !== undefined ? { description } : {}),
|
|
53
|
+
...(lead_group_ids?.length ? { leadGroupIds: lead_group_ids } : {}),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return handleToolError(error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
server.tool("update_lead_group_folder", "Change a shared folder: rename it, set or clear its emoji and description, add groups to it (they leave " +
|
|
61
|
+
"their previous folder) or take groups out of it. Only the fields given change, so a colleague's other " +
|
|
62
|
+
"edits are kept. Needs write permission.", {
|
|
63
|
+
folder_id: folderId,
|
|
64
|
+
name: z.string().trim().min(1).max(100).optional(),
|
|
65
|
+
emoji: emoji.nullable().optional().describe("New emoji, or null to remove it."),
|
|
66
|
+
description: z.string().trim().max(2000).nullable().optional().describe("New description, or null to remove it."),
|
|
67
|
+
add_lead_group_ids: groupIds.optional().describe("Groups moved into this folder."),
|
|
68
|
+
remove_lead_group_ids: groupIds.optional().describe("Groups taken out of this folder; they stay outside any folder."),
|
|
69
|
+
}, async ({ folder_id, name, emoji: icon, description, add_lead_group_ids, remove_lead_group_ids }) => {
|
|
70
|
+
try {
|
|
71
|
+
return toolResult(await client.put(path(folder_id), {
|
|
72
|
+
...(name !== undefined ? { name } : {}),
|
|
73
|
+
...(icon !== undefined ? { emoji: icon === null ? null : emojiCode(icon) } : {}),
|
|
74
|
+
...(description !== undefined ? { description } : {}),
|
|
75
|
+
...(add_lead_group_ids?.length ? { addLeadGroupIds: add_lead_group_ids } : {}),
|
|
76
|
+
...(remove_lead_group_ids?.length ? { removeLeadGroupIds: remove_lead_group_ids } : {}),
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return handleToolError(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.tool("delete_lead_group_folder", "Delete a shared folder. Its groups are kept and simply leave the folder; no lead or group is deleted. " +
|
|
84
|
+
"Needs write permission.", { folder_id: folderId }, async ({ folder_id }) => {
|
|
85
|
+
try {
|
|
86
|
+
return toolResult(await client.delete(path(folder_id)));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return handleToolError(error);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
server.tool("move_lead_groups_to_folder", "Put lead groups in a shared folder, or take them out of any folder with folder_id null. All groups move " +
|
|
93
|
+
"or none does. Needs write permission.", {
|
|
94
|
+
lead_group_ids: groupIds.min(1).describe("Lead groups to file, all from the same organization."),
|
|
95
|
+
folder_id: folderId.nullable().describe("Target folder ID, or null to take the groups out of any folder."),
|
|
96
|
+
}, async ({ lead_group_ids, folder_id }) => {
|
|
97
|
+
try {
|
|
98
|
+
return toolResult(await client.put("/api/lead-group-folders/assignments", { leadGroupIds: lead_group_ids, folderId: folder_id }));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return handleToolError(error);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { type LeadifyClient } from "../client.js";
|
|
3
3
|
export declare function assertLeadGroupsBelongToOrganization(data: unknown, organizationId: string): Array<Record<string, unknown>>;
|
|
4
|
+
/** PRD-2480: the shared sidebar folder of a group, or null when it sits outside any folder. */
|
|
5
|
+
export declare function leadGroupFolder(group: Record<string, unknown>): {
|
|
6
|
+
folderId: string | null;
|
|
7
|
+
folder: {
|
|
8
|
+
id: unknown;
|
|
9
|
+
name: unknown;
|
|
10
|
+
emoji: unknown;
|
|
11
|
+
} | null;
|
|
12
|
+
};
|
|
4
13
|
/** Keep group reads navigable: never return schema, profiles, or lead data. */
|
|
5
14
|
export declare function compactLeadGroup(group: unknown): Record<string, unknown>;
|
|
6
15
|
export declare function registerOrganizationTools(server: McpServer, client?: Pick<LeadifyClient, "get" | "post" | "put">): void;
|
|
@@ -22,6 +22,26 @@ export function assertLeadGroupsBelongToOrganization(data, organizationId) {
|
|
|
22
22
|
}
|
|
23
23
|
return leadGroups;
|
|
24
24
|
}
|
|
25
|
+
/** PRD-2480: the shared sidebar folder of a group, or null when it sits outside any folder. */
|
|
26
|
+
export function leadGroupFolder(group) {
|
|
27
|
+
const folder = group.folder && typeof group.folder === "object" ? group.folder : null;
|
|
28
|
+
return {
|
|
29
|
+
folderId: typeof group.folderId === "string" ? group.folderId : null,
|
|
30
|
+
folder: folder ? { id: folder.id, name: folder.name, emoji: folder.emoji ?? null } : null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** One line of a group list: navigation fields and its folder, never schema or leads. */
|
|
34
|
+
function listedLeadGroup(group) {
|
|
35
|
+
return {
|
|
36
|
+
id: group.id,
|
|
37
|
+
name: group.name,
|
|
38
|
+
organizationId: group.organizationId,
|
|
39
|
+
leadCount: group._count?.leads ?? 0,
|
|
40
|
+
published: group.published ?? false,
|
|
41
|
+
refreshEnabled: group.refreshEnabled ?? false,
|
|
42
|
+
...leadGroupFolder(group),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
25
45
|
/** Keep group reads navigable: never return schema, profiles, or lead data. */
|
|
26
46
|
export function compactLeadGroup(group) {
|
|
27
47
|
const source = group && typeof group === "object" ? group : {};
|
|
@@ -42,6 +62,7 @@ export function compactLeadGroup(group) {
|
|
|
42
62
|
refreshEnabled: source.refreshEnabled ?? false,
|
|
43
63
|
refreshFrequencyDays: source.refreshFrequencyDays ?? null,
|
|
44
64
|
createdAt: source.createdAt ?? null,
|
|
65
|
+
...leadGroupFolder(source),
|
|
45
66
|
};
|
|
46
67
|
}
|
|
47
68
|
export function registerOrganizationTools(server, client = getClient()) {
|
|
@@ -86,25 +107,22 @@ export function registerOrganizationTools(server, client = getClient()) {
|
|
|
86
107
|
}
|
|
87
108
|
});
|
|
88
109
|
server.tool("list_lead_groups", "List lead groups visible in one explicitly selected organization. Returns a compact " +
|
|
89
|
-
"navigation view only: id, name, organizationId, lead count, published and refresh state
|
|
110
|
+
"navigation view only: id, name, organizationId, lead count, published and refresh state, and the " +
|
|
111
|
+
"shared sidebar folder (folderId, folder { id, name, emoji }). Pass folder_id to keep exactly the " +
|
|
112
|
+
"groups of one folder (list_lead_group_folders), or \"none\" for the groups outside any folder. " +
|
|
90
113
|
"Start with discover_leadify_context when no organization_id is known.", {
|
|
91
114
|
organization_id: z.string().describe("Organization ID explicitly selected from list_organizations or discover_leadify_context."),
|
|
115
|
+
folder_id: z.string().trim().min(1).optional().describe("Keep only the groups of this folder, or \"none\" for groups outside any folder."),
|
|
92
116
|
limit: z.number().int().min(1).max(100).optional().default(50).describe("Maximum groups returned, bounded to 100."),
|
|
93
|
-
}, async ({ organization_id, limit }) => {
|
|
117
|
+
}, async ({ organization_id, folder_id, limit }) => {
|
|
94
118
|
try {
|
|
95
|
-
const params = new URLSearchParams({ organizationId: organization_id });
|
|
96
|
-
const data = await
|
|
119
|
+
const params = new URLSearchParams({ organizationId: organization_id, ...(folder_id ? { folderId: folder_id } : {}) });
|
|
120
|
+
const data = await client.get("/api/lead-groups", params);
|
|
97
121
|
const groups = assertLeadGroupsBelongToOrganization(data, organization_id);
|
|
98
122
|
return toolResult({
|
|
99
123
|
organizationId: organization_id,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
name: group.name,
|
|
103
|
-
organizationId: group.organizationId,
|
|
104
|
-
leadCount: group._count?.leads ?? 0,
|
|
105
|
-
published: group.published ?? false,
|
|
106
|
-
refreshEnabled: group.refreshEnabled ?? false,
|
|
107
|
-
})),
|
|
124
|
+
...(folder_id ? { folderId: folder_id } : {}),
|
|
125
|
+
leadGroups: groups.slice(0, limit).map(listedLeadGroup),
|
|
108
126
|
total: groups.length,
|
|
109
127
|
truncated: groups.length > limit,
|
|
110
128
|
});
|
|
@@ -115,12 +133,13 @@ export function registerOrganizationTools(server, client = getClient()) {
|
|
|
115
133
|
});
|
|
116
134
|
server.tool("get_lead_group", "Read the compact metadata of one accessible lead group by ID. Use this after list_lead_groups " +
|
|
117
135
|
"or discover_leadify_context to confirm its type and operational state. Returns only navigation " +
|
|
118
|
-
"metadata (no lead records, schema, company profile, or persona payload)
|
|
136
|
+
"metadata (no lead records, schema, company profile, or persona payload), with its shared sidebar " +
|
|
137
|
+
"folder (folderId, folder), and reports inaccessible " +
|
|
119
138
|
"groups as not found, without revealing their organization.", {
|
|
120
139
|
lead_group_id: z.string().describe("Lead group ID returned by Leadify discovery or another authorized Leadify response."),
|
|
121
140
|
}, async ({ lead_group_id }) => {
|
|
122
141
|
try {
|
|
123
|
-
const data = await
|
|
142
|
+
const data = await client.get(`/api/lead-group/${encodeURIComponent(lead_group_id)}`);
|
|
124
143
|
const source = data && typeof data === "object" ? data : {};
|
|
125
144
|
return toolResult({ leadGroup: compactLeadGroup(source.leadGroup) });
|
|
126
145
|
}
|
|
@@ -130,13 +149,14 @@ export function registerOrganizationTools(server, client = getClient()) {
|
|
|
130
149
|
});
|
|
131
150
|
server.tool("discover_leadify_context", "Safe zero-ID entry point for Leadify. With no organization_id, lists accessible organizations " +
|
|
132
151
|
"and never chooses one when several are available. With an explicit organization_id, returns " +
|
|
133
|
-
"a compact list of its lead groups
|
|
152
|
+
"a compact list of its lead groups with their folder, and the organization's shared sidebar folders. " +
|
|
153
|
+
"Use the returned lead_group_id with lead, campaign, persona " +
|
|
134
154
|
"and Context Workspace tools.", {
|
|
135
155
|
organization_id: z.string().optional().describe("Organization selected explicitly after discovery. Omit only for the first step."),
|
|
136
156
|
limit: z.number().int().min(1).max(100).optional().default(50).describe("Maximum lead groups returned after selection."),
|
|
137
157
|
}, async ({ organization_id, limit }) => {
|
|
138
158
|
try {
|
|
139
|
-
const orgData = await
|
|
159
|
+
const orgData = await client.get("/api/organizations");
|
|
140
160
|
const organizations = Array.isArray(orgData.organizations)
|
|
141
161
|
? orgData.organizations.map((org) => ({ id: org.id, name: org.name ?? null, slug: org.slug ?? null }))
|
|
142
162
|
: [];
|
|
@@ -149,11 +169,28 @@ export function registerOrganizationTools(server, client = getClient()) {
|
|
|
149
169
|
if (!organizations.some((org) => org.id === organization_id)) {
|
|
150
170
|
return toolResult({ organizations, selectionRequired: true, message: "organization_id is not accessible to this key." });
|
|
151
171
|
}
|
|
152
|
-
const groupsData = await
|
|
172
|
+
const groupsData = await client.get("/api/lead-groups", new URLSearchParams({ organizationId: organization_id }));
|
|
153
173
|
const groups = assertLeadGroupsBelongToOrganization(groupsData, organization_id);
|
|
174
|
+
// The folders complete the map; a failure to read them never hides the groups.
|
|
175
|
+
let folders = null;
|
|
176
|
+
let foldersError;
|
|
177
|
+
try {
|
|
178
|
+
const folderData = await client.get("/api/lead-group-folders", new URLSearchParams({ organizationId: organization_id }));
|
|
179
|
+
folders = Array.isArray(folderData?.folders)
|
|
180
|
+
? folderData.folders.map((folder) => ({ id: folder.id, name: folder.name, emoji: folder.emoji ?? null, leadGroupIds: folder.leadGroupIds ?? [] }))
|
|
181
|
+
: [];
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
foldersError = error instanceof Error ? error.message : String(error);
|
|
185
|
+
}
|
|
154
186
|
return toolResult({
|
|
155
187
|
organization: organizations.find((org) => org.id === organization_id),
|
|
156
|
-
|
|
188
|
+
folders,
|
|
189
|
+
...(foldersError ? { foldersError } : {}),
|
|
190
|
+
leadGroups: groups.slice(0, limit).map((group) => {
|
|
191
|
+
const { refreshEnabled: _refreshEnabled, ...listed } = listedLeadGroup(group);
|
|
192
|
+
return listed;
|
|
193
|
+
}),
|
|
157
194
|
total: groups.length,
|
|
158
195
|
truncated: groups.length > limit,
|
|
159
196
|
next: "Choose a lead_group_id, then use get_leads, get_context_workspace, or list_persona_contracts.",
|