@agifyai/leadify-mcp 1.4.1

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.
@@ -0,0 +1,144 @@
1
+ import { z } from "zod";
2
+ import { getClient } from "../client.js";
3
+ import { toolResult, handleToolError } from "../types.js";
4
+ const ICON_COLORS = [
5
+ "blue",
6
+ "red",
7
+ "amber",
8
+ "green",
9
+ "purple",
10
+ "orange",
11
+ "teal",
12
+ "indigo",
13
+ "pink",
14
+ "slate",
15
+ ];
16
+ const FREQUENCIES = [
17
+ "temps reel",
18
+ "quotidienne",
19
+ "hebdomadaire",
20
+ "mensuelle",
21
+ ];
22
+ export function registerDataSourceTools(server) {
23
+ // ── list_data_sources ──────────────────────────────────────────────────
24
+ server.tool("list_data_sources", "List every data source configured for the workspace, grouped by country then name. " +
25
+ "Open to all authenticated users. Use this to discover available sources before " +
26
+ "attaching them to a persona or campaign.", {}, async () => {
27
+ try {
28
+ const data = await getClient().get("/api/data-sources");
29
+ return toolResult(data);
30
+ }
31
+ catch (error) {
32
+ return handleToolError(error);
33
+ }
34
+ });
35
+ // ── create_data_source ─────────────────────────────────────────────────
36
+ server.tool("create_data_source", "Create a new data source entry. Admin-only. Requires a name and a 2-letter ISO " +
37
+ "country code. Additional metadata (icon, frequency, url) helps UI display and " +
38
+ "agent routing.", {
39
+ name: z.string().describe("Source name (e.g. 'LinkedIn FR')."),
40
+ country: z
41
+ .string()
42
+ .length(2)
43
+ .describe("ISO 2-letter country code (e.g. 'FR')."),
44
+ description: z.string().optional().describe("Source description."),
45
+ icon_label: z
46
+ .string()
47
+ .max(4)
48
+ .optional()
49
+ .describe("Icon abbreviation (2-4 chars, e.g. 'in')."),
50
+ icon_color: z
51
+ .enum(ICON_COLORS)
52
+ .optional()
53
+ .describe("Icon color."),
54
+ url: z.string().optional().describe("Source website URL."),
55
+ frequency: z
56
+ .enum(FREQUENCIES)
57
+ .optional()
58
+ .describe("Update frequency."),
59
+ }, async (params) => {
60
+ try {
61
+ const body = {
62
+ name: params.name,
63
+ country: params.country,
64
+ };
65
+ if (params.description !== undefined)
66
+ body.description = params.description;
67
+ if (params.icon_label !== undefined)
68
+ body.iconLabel = params.icon_label;
69
+ if (params.icon_color !== undefined)
70
+ body.iconColor = params.icon_color;
71
+ if (params.url !== undefined)
72
+ body.url = params.url;
73
+ if (params.frequency !== undefined)
74
+ body.frequency = params.frequency;
75
+ const data = await getClient().post("/api/data-sources", body);
76
+ return toolResult(data);
77
+ }
78
+ catch (error) {
79
+ return handleToolError(error);
80
+ }
81
+ });
82
+ // ── update_data_source ─────────────────────────────────────────────────
83
+ server.tool("update_data_source", "Update fields on an existing data source. Admin-only. Only the fields you pass are " +
84
+ "modified; omitted fields are left untouched.", {
85
+ id: z.string().describe("Source identifier."),
86
+ name: z.string().optional().describe("New source name."),
87
+ country: z
88
+ .string()
89
+ .length(2)
90
+ .optional()
91
+ .describe("ISO 2-letter country code."),
92
+ description: z.string().optional().describe("Source description."),
93
+ icon_label: z
94
+ .string()
95
+ .max(4)
96
+ .optional()
97
+ .describe("Icon abbreviation (2-4 chars)."),
98
+ icon_color: z
99
+ .enum(ICON_COLORS)
100
+ .optional()
101
+ .describe("Icon color."),
102
+ url: z.string().optional().describe("Source website URL."),
103
+ frequency: z
104
+ .enum(FREQUENCIES)
105
+ .optional()
106
+ .describe("Update frequency."),
107
+ }, async (params) => {
108
+ try {
109
+ const body = {};
110
+ if (params.name !== undefined)
111
+ body.name = params.name;
112
+ if (params.country !== undefined)
113
+ body.country = params.country;
114
+ if (params.description !== undefined)
115
+ body.description = params.description;
116
+ if (params.icon_label !== undefined)
117
+ body.iconLabel = params.icon_label;
118
+ if (params.icon_color !== undefined)
119
+ body.iconColor = params.icon_color;
120
+ if (params.url !== undefined)
121
+ body.url = params.url;
122
+ if (params.frequency !== undefined)
123
+ body.frequency = params.frequency;
124
+ const data = await getClient().put(`/api/data-sources/${encodeURIComponent(params.id)}`, body);
125
+ return toolResult(data);
126
+ }
127
+ catch (error) {
128
+ return handleToolError(error);
129
+ }
130
+ });
131
+ // ── delete_data_source ─────────────────────────────────────────────────
132
+ server.tool("delete_data_source", "Permanently remove a data source. Admin-only and irreversible. Always confirm with " +
133
+ "the user before calling this tool.", {
134
+ id: z.string().describe("Source identifier."),
135
+ }, async ({ id }) => {
136
+ try {
137
+ const data = await getClient().delete(`/api/data-sources/${encodeURIComponent(id)}`);
138
+ return toolResult(data);
139
+ }
140
+ catch (error) {
141
+ return handleToolError(error);
142
+ }
143
+ });
144
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerLeadGroupTools(server: McpServer): void;
@@ -0,0 +1,172 @@
1
+ import { z } from "zod";
2
+ import { getClient } from "../client.js";
3
+ import { toolResult, handleToolError } from "../types.js";
4
+ const DISABLABLE_TOOLS = [
5
+ "linkedin",
6
+ "search",
7
+ "jobboard",
8
+ "signals",
9
+ "linkedin_company",
10
+ "skool_member",
11
+ ];
12
+ export function registerLeadGroupTools(server) {
13
+ // ── create_lead_group ──────────────────────────────────────────────────
14
+ server.tool("create_lead_group", "Create a new lead group in the specified organization. A lead group holds leads, a " +
15
+ "schema, and an optional persona assignment. Use list_organizations first to find the " +
16
+ "organization_id if you don't have it. Assigning a persona at creation time scopes " +
17
+ "scoring/messaging immediately.", {
18
+ name: z.string().describe("Group name."),
19
+ organization_id: z
20
+ .string()
21
+ .describe("Target organization ID (Clerk org ID). Get one via list_organizations."),
22
+ description: z.string().nullable().optional().describe("Group description."),
23
+ persona_types: z
24
+ .array(z.string())
25
+ .optional()
26
+ .describe("CRM schema extension types (e.g. 'physician', 'skool_member'). Determines which " +
27
+ "custom fields are available on leads in this group."),
28
+ company_profile: z
29
+ .record(z.unknown())
30
+ .nullable()
31
+ .optional()
32
+ .describe("Sender/product overlay profile (freeform object). Overrides the org-level profile " +
33
+ "for messaging in this group only."),
34
+ disabled_tools: z
35
+ .array(z.enum(DISABLABLE_TOOLS))
36
+ .optional()
37
+ .describe("List of sourcing/monitoring tools to disable for this group. Allowed values: " +
38
+ "linkedin, search, jobboard, signals, linkedin_company, skool_member."),
39
+ persona_id: z
40
+ .string()
41
+ .nullable()
42
+ .optional()
43
+ .describe("ICP persona ID to assign. Must belong to the same org or be global. " +
44
+ "Omit to create the group without a persona."),
45
+ }, async (params) => {
46
+ try {
47
+ const body = {
48
+ name: params.name,
49
+ organizationId: params.organization_id,
50
+ };
51
+ if (params.description !== undefined)
52
+ body.description = params.description;
53
+ if (params.persona_types !== undefined)
54
+ body.personaTypes = params.persona_types;
55
+ if (params.company_profile !== undefined)
56
+ body.companyProfile = params.company_profile;
57
+ if (params.disabled_tools !== undefined)
58
+ body.disabledTools = params.disabled_tools;
59
+ if (params.persona_id !== undefined)
60
+ body.personaId = params.persona_id;
61
+ const data = await getClient().post("/api/lead-group", body);
62
+ return toolResult(data);
63
+ }
64
+ catch (error) {
65
+ return handleToolError(error);
66
+ }
67
+ });
68
+ // ── update_lead_group ──────────────────────────────────────────────────
69
+ server.tool("update_lead_group", "Update an existing lead group. All body fields are optional — only those passed are " +
70
+ "modified. Use this to rename a group, swap its persona, change which sourcing tools " +
71
+ "are enabled, or edit the company_profile overlay. To list leads or schema, use " +
72
+ "get_leads instead.", {
73
+ id: z.string().describe("Lead group ID to update."),
74
+ name: z.string().optional().describe("New group name."),
75
+ description: z.string().nullable().optional().describe("New group description."),
76
+ persona_id: z
77
+ .string()
78
+ .nullable()
79
+ .optional()
80
+ .describe("New ICP persona ID. Pass null to unassign. Must belong to the same org or be global."),
81
+ persona_types: z
82
+ .array(z.string())
83
+ .optional()
84
+ .describe("Updated CRM schema extension types."),
85
+ company_profile: z
86
+ .record(z.unknown())
87
+ .nullable()
88
+ .optional()
89
+ .describe("Updated sender/product overlay profile."),
90
+ disabled_tools: z
91
+ .array(z.enum(DISABLABLE_TOOLS))
92
+ .optional()
93
+ .describe("Updated list of disabled tools. Allowed values: linkedin, search, jobboard, " +
94
+ "signals, linkedin_company, skool_member."),
95
+ }, async (params) => {
96
+ try {
97
+ const body = {};
98
+ if (params.name !== undefined)
99
+ body.name = params.name;
100
+ if (params.description !== undefined)
101
+ body.description = params.description;
102
+ if (params.persona_id !== undefined)
103
+ body.personaId = params.persona_id;
104
+ if (params.persona_types !== undefined)
105
+ body.personaTypes = params.persona_types;
106
+ if (params.company_profile !== undefined)
107
+ body.companyProfile = params.company_profile;
108
+ if (params.disabled_tools !== undefined)
109
+ body.disabledTools = params.disabled_tools;
110
+ const data = await getClient().put(`/api/lead-group/${encodeURIComponent(params.id)}`, body);
111
+ return toolResult(data);
112
+ }
113
+ catch (error) {
114
+ return handleToolError(error);
115
+ }
116
+ });
117
+ // ── clone_lead_group ───────────────────────────────────────────────────
118
+ server.tool("clone_lead_group", "Clone a lead group (and all its leads) into another organization. Campaigns, views, " +
119
+ "and jobs are NOT cloned — only the group structure, schema, and lead data. Admin " +
120
+ "permission required on both orgs.", {
121
+ id: z.string().describe("Source lead group ID."),
122
+ target_organization_id: z
123
+ .string()
124
+ .describe("Destination organization ID. Caller must have admin access there."),
125
+ new_name: z
126
+ .string()
127
+ .nullable()
128
+ .optional()
129
+ .describe("Optional name for the cloned group. If omitted, uses the original name with ' (copy)' appended."),
130
+ }, async ({ id, target_organization_id, new_name }) => {
131
+ try {
132
+ const body = {
133
+ targetOrganizationId: target_organization_id,
134
+ };
135
+ if (new_name !== undefined)
136
+ body.newName = new_name;
137
+ const data = await getClient().post(`/api/lead-group/${encodeURIComponent(id)}/clone`, body);
138
+ return toolResult(data);
139
+ }
140
+ catch (error) {
141
+ return handleToolError(error);
142
+ }
143
+ });
144
+ // ── delete_lead_group ──────────────────────────────────────────────────
145
+ server.tool("delete_lead_group", "Permanently delete a lead group, including ALL its leads, schema, and associated data. " +
146
+ "Irreversible. Admin permission required. Always confirm with the user before calling " +
147
+ "this tool — this destroys every lead in the group.", {
148
+ id: z.string().describe("Lead group ID to delete."),
149
+ }, async ({ id }) => {
150
+ try {
151
+ const data = await getClient().delete(`/api/lead-group/${encodeURIComponent(id)}`);
152
+ return toolResult(data);
153
+ }
154
+ catch (error) {
155
+ return handleToolError(error);
156
+ }
157
+ });
158
+ // ── get_lead_group_persona ─────────────────────────────────────────────
159
+ server.tool("get_lead_group_persona", "Retrieve the persona assigned to a specific lead group, with group context. " +
160
+ "Returns null persona if none is assigned. Use this to check what targeting " +
161
+ "rules apply to a group before generating messages.", {
162
+ lead_group_id: z.string().describe("Lead group ID."),
163
+ }, async ({ lead_group_id }) => {
164
+ try {
165
+ const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/persona`);
166
+ return toolResult(data);
167
+ }
168
+ catch (error) {
169
+ return handleToolError(error);
170
+ }
171
+ });
172
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerLeadTools(server: McpServer): void;
@@ -0,0 +1,188 @@
1
+ import { z } from "zod";
2
+ import { getClient } from "../client.js";
3
+ import { toolResult, handleToolError } from "../types.js";
4
+ export function registerLeadTools(server) {
5
+ // ── add_leads ──────────────────────────────────────────────────────────
6
+ server.tool("add_leads", "Add one or more leads to a specific lead group in Leadify. Provide the group ID " +
7
+ "and an array of lead objects with any combination of fields (email, firstName, " +
8
+ "lastName, company, jobTitle, phone, custom fields, card_* fields, percent_* " +
9
+ "fields, relation fields, etc.). Optionally specify which fields should be " +
10
+ "treated as 'select' dropdowns in the UI. Returns the count of successfully " +
11
+ "imported leads and any errors. Use this for both bulk imports and single lead creation.", {
12
+ lead_group_id: z
13
+ .string()
14
+ .describe("ID of the lead group to add leads to."),
15
+ leads: z
16
+ .array(z.record(z.unknown()))
17
+ .describe("Array of lead objects. Each object is a key-value map of field names to values. " +
18
+ "Common fields: email, firstName, lastName, company, jobTitle, phone, linkedin, " +
19
+ "website, location, seniority, specialty, age. Also supports card_* fields, " +
20
+ "percent_* fields, boolean flags (decisionMaking, excluded), and relation fields."),
21
+ is_select_fields: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("List of field names that should be treated as 'select' (dropdown) fields in the UI. " +
25
+ "If omitted, no fields are marked as selects."),
26
+ }, async ({ lead_group_id, leads, is_select_fields }) => {
27
+ try {
28
+ const body = {
29
+ leadGroupId: lead_group_id,
30
+ leadData: { leads },
31
+ };
32
+ if (is_select_fields) {
33
+ body.leadData.isSelectFields =
34
+ is_select_fields;
35
+ }
36
+ const data = await getClient().post("/add-lead", body);
37
+ return toolResult(data);
38
+ }
39
+ catch (error) {
40
+ return handleToolError(error);
41
+ }
42
+ });
43
+ // ── get_leads ──────────────────────────────────────────────────────────
44
+ server.tool("get_leads", "Search and list leads from a Leadify lead group with optional filtering, search, " +
45
+ "and pagination. Supports field-level filters with operators (equals, not_equals, " +
46
+ "contains, not_contains, gte, lte, is_true, is_false, is_null, is_not_null), " +
47
+ "full-text search, and saved views. Returns paginated results with lead data and " +
48
+ "the group schema. Use this to browse, search, or filter leads. For retrieving " +
49
+ "a single lead by its ID, use get_lead instead.", {
50
+ group_id: z.string().describe("ID of the lead group to query."),
51
+ view_id: z
52
+ .string()
53
+ .optional()
54
+ .describe("ID of a saved view to apply. Views pre-configure filters, columns, and sorting."),
55
+ page: z
56
+ .number()
57
+ .int()
58
+ .positive()
59
+ .optional()
60
+ .describe("Page number (1-based). Defaults to 1."),
61
+ limit: z
62
+ .number()
63
+ .int()
64
+ .positive()
65
+ .optional()
66
+ .describe("Results per page. Defaults to 50."),
67
+ no_pagination: z
68
+ .boolean()
69
+ .optional()
70
+ .describe("If true, returns ALL leads in the group without pagination. Use with caution on large groups."),
71
+ search: z
72
+ .string()
73
+ .optional()
74
+ .describe("Full-text search term applied across all fields."),
75
+ filters: z
76
+ .record(z.record(z.string()))
77
+ .optional()
78
+ .describe("Filter leads by field values. Keys are field names, values are objects mapping " +
79
+ "operator to value. Operators: equals, not_equals, contains, not_contains, " +
80
+ "gte, lte, is_true, is_false, is_null, is_not_null. " +
81
+ 'Example: {"industry": {"contains": "tech"}, "company": {"is_not_null": "true"}}'),
82
+ }, async ({ group_id, view_id, page, limit, no_pagination, search, filters }) => {
83
+ try {
84
+ const params = new URLSearchParams();
85
+ params.set("groupId", group_id);
86
+ if (view_id)
87
+ params.set("viewId", view_id);
88
+ if (page !== undefined)
89
+ params.set("page", String(page));
90
+ if (limit !== undefined)
91
+ params.set("limit", String(limit));
92
+ if (no_pagination)
93
+ params.set("noPagination", "true");
94
+ if (search)
95
+ params.set("search", search);
96
+ if (filters) {
97
+ for (const [field, ops] of Object.entries(filters)) {
98
+ for (const [op, value] of Object.entries(ops)) {
99
+ params.set(`filter[${field}][${op}]`, value);
100
+ }
101
+ }
102
+ }
103
+ const data = await getClient().get("/get-leads", params);
104
+ return toolResult(data);
105
+ }
106
+ catch (error) {
107
+ return handleToolError(error);
108
+ }
109
+ });
110
+ // ── get_lead ───────────────────────────────────────────────────────────
111
+ server.tool("get_lead", "Retrieve full details for a single lead by its ID, including all data fields, " +
112
+ "group membership, creation/update timestamps, related leads (if relations are " +
113
+ "configured), and the group schema. Use this when you already have a lead ID " +
114
+ "and need its complete information. For searching or browsing multiple leads, " +
115
+ "use get_leads instead.", {
116
+ id: z.string().describe("The unique ID of the lead to retrieve."),
117
+ }, async ({ id }) => {
118
+ try {
119
+ const params = new URLSearchParams();
120
+ params.set("id", id);
121
+ const data = await getClient().get("/get-lead", params);
122
+ return toolResult(data);
123
+ }
124
+ catch (error) {
125
+ return handleToolError(error);
126
+ }
127
+ });
128
+ // ── update_lead ────────────────────────────────────────────────────────
129
+ server.tool("update_lead", "Update one or more fields on an existing lead. Each update specifies the field " +
130
+ "name, new value, and optionally whether the field is a 'select' dropdown type. " +
131
+ "Supports all field types including special fields (card_*, percent_*, boolean flags " +
132
+ "like decisionMaking/excluded), and relation fields. Returns the updated lead. " +
133
+ "For bulk updates across many leads, call this tool once per lead.", {
134
+ lead_id: z.string().describe("ID of the lead to update."),
135
+ updates: z
136
+ .array(z.object({
137
+ property_name: z
138
+ .string()
139
+ .describe("Name of the field to update (e.g. 'email', 'status', 'card_analysis')."),
140
+ value: z
141
+ .unknown()
142
+ .describe("New value for the field. Type depends on the field."),
143
+ is_select: z
144
+ .boolean()
145
+ .optional()
146
+ .describe("If true, the field is created/updated as a 'select' dropdown type. " +
147
+ "Use for categorical fields like status, source, etc."),
148
+ }))
149
+ .min(1)
150
+ .describe("Array of field updates to apply."),
151
+ }, async ({ lead_id, updates }) => {
152
+ try {
153
+ const body = {
154
+ leadId: lead_id,
155
+ updates: updates.map((u) => ({
156
+ propertyName: u.property_name,
157
+ value: u.value,
158
+ ...(u.is_select !== undefined ? { isSelect: u.is_select } : {}),
159
+ })),
160
+ };
161
+ const data = await getClient().put("/update-lead", body);
162
+ return toolResult(data);
163
+ }
164
+ catch (error) {
165
+ return handleToolError(error);
166
+ }
167
+ });
168
+ // ── delete_leads ───────────────────────────────────────────────────────
169
+ server.tool("delete_leads", "Permanently delete one or more leads by their IDs. This action is irreversible. " +
170
+ "Returns the count and IDs of deleted leads. If some leads are not found or " +
171
+ "access is denied, the response will list which IDs failed with details. " +
172
+ "Always confirm with the user before calling this tool.", {
173
+ lead_ids: z
174
+ .array(z.string())
175
+ .min(1)
176
+ .describe("Array of lead IDs to delete."),
177
+ }, async ({ lead_ids }) => {
178
+ try {
179
+ const data = await getClient().post("/delete-lead", {
180
+ leadIds: lead_ids,
181
+ });
182
+ return toolResult(data);
183
+ }
184
+ catch (error) {
185
+ return handleToolError(error);
186
+ }
187
+ });
188
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerLogTools(server: McpServer): void;