@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.
- package/README.md +117 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +34 -0
- package/dist/tools/activities.d.ts +2 -0
- package/dist/tools/activities.js +103 -0
- package/dist/tools/auth.d.ts +2 -0
- package/dist/tools/auth.js +16 -0
- package/dist/tools/campaigns.d.ts +2 -0
- package/dist/tools/campaigns.js +144 -0
- package/dist/tools/dataroom.d.ts +2 -0
- package/dist/tools/dataroom.js +82 -0
- package/dist/tools/datasources.d.ts +2 -0
- package/dist/tools/datasources.js +144 -0
- package/dist/tools/lead_groups.d.ts +2 -0
- package/dist/tools/lead_groups.js +172 -0
- package/dist/tools/leads.d.ts +2 -0
- package/dist/tools/leads.js +188 -0
- package/dist/tools/logs.d.ts +2 -0
- package/dist/tools/logs.js +181 -0
- package/dist/tools/organizations.d.ts +2 -0
- package/dist/tools/organizations.js +17 -0
- package/dist/tools/outreach_settings.d.ts +2 -0
- package/dist/tools/outreach_settings.js +397 -0
- package/dist/tools/personas.d.ts +2 -0
- package/dist/tools/personas.js +1268 -0
- package/dist/tools/schema.d.ts +2 -0
- package/dist/tools/schema.js +130 -0
- package/dist/tools/signals.d.ts +2 -0
- package/dist/tools/signals.js +50 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.js +38 -0
- package/package.json +35 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getClient } from "../client.js";
|
|
3
|
+
import { toolResult, handleToolError } from "../types.js";
|
|
4
|
+
// ─── Shared log-input schema ───────────────────────────────────────────────
|
|
5
|
+
const logLevelEnum = z.enum(["INFO", "WARNING", "ERROR"]);
|
|
6
|
+
const campaignLogInput = z.object({
|
|
7
|
+
campaign_slug: z.string().describe("Campaign identifier slug (e.g. 'campaign-2024-q1')."),
|
|
8
|
+
campaign_name: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe("Human-readable campaign name (required by the API)."),
|
|
11
|
+
lead_id: z.string().describe("ID of the lead this log entry is about."),
|
|
12
|
+
action: z.string().describe("Action performed (e.g. 'Email sent', 'Call made')."),
|
|
13
|
+
message: z.string().describe("Human-readable description of what happened."),
|
|
14
|
+
execution_id: z.number().int().describe("n8n execution ID that triggered this action."),
|
|
15
|
+
workflow_id: z.number().int().describe("n8n workflow ID that triggered this action."),
|
|
16
|
+
workflow: z.string().describe("Name of the n8n workflow."),
|
|
17
|
+
workflow_url: z.string().describe("Full URL to the n8n workflow."),
|
|
18
|
+
execution_url: z.string().describe("Full URL to the n8n execution."),
|
|
19
|
+
level: logLevelEnum.describe("Log severity level."),
|
|
20
|
+
lemlist_activity_id: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Lemlist activity ID, if this log originates from a Lemlist event."),
|
|
24
|
+
error_code: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Error code if the action failed (e.g. 'SMTP_TIMEOUT', 'RATE_LIMITED')."),
|
|
28
|
+
retry_count: z.number().int().optional().describe("Number of retry attempts. Defaults to 0."),
|
|
29
|
+
});
|
|
30
|
+
function mapLog(log) {
|
|
31
|
+
const body = {
|
|
32
|
+
campaignSlug: log.campaign_slug,
|
|
33
|
+
campaignName: log.campaign_name,
|
|
34
|
+
leadId: log.lead_id,
|
|
35
|
+
action: log.action,
|
|
36
|
+
message: log.message,
|
|
37
|
+
executionId: log.execution_id,
|
|
38
|
+
workflowId: log.workflow_id,
|
|
39
|
+
workflow: log.workflow,
|
|
40
|
+
workflowUrl: log.workflow_url,
|
|
41
|
+
executionUrl: log.execution_url,
|
|
42
|
+
level: log.level,
|
|
43
|
+
};
|
|
44
|
+
if (log.lemlist_activity_id !== undefined)
|
|
45
|
+
body.lemlistActivityId = log.lemlist_activity_id;
|
|
46
|
+
if (log.error_code !== undefined)
|
|
47
|
+
body.errorCode = log.error_code;
|
|
48
|
+
if (log.retry_count !== undefined)
|
|
49
|
+
body.retryCount = log.retry_count;
|
|
50
|
+
return body;
|
|
51
|
+
}
|
|
52
|
+
// ─── Tool registrations ────────────────────────────────────────────────────
|
|
53
|
+
export function registerLogTools(server) {
|
|
54
|
+
// ── add_campaign_log ───────────────────────────────────────────────────
|
|
55
|
+
server.tool("add_campaign_log", "Record a single campaign activity log entry for a specific lead. Logs track actions " +
|
|
56
|
+
"like emails sent, calls made, or workflow executions, linking them to a campaign " +
|
|
57
|
+
"slug and n8n workflow/execution metadata. Each log has a severity level " +
|
|
58
|
+
"(INFO, WARNING, ERROR). Requires admin permissions. Use this to build an audit " +
|
|
59
|
+
"trail of campaign interactions with leads. For bulk log ingestion, use " +
|
|
60
|
+
"add_campaign_logs instead.", campaignLogInput.shape, async (params) => {
|
|
61
|
+
try {
|
|
62
|
+
const data = await getClient().post("/add-lead-log", mapLog(params));
|
|
63
|
+
return toolResult(data);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return handleToolError(error);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// ── add_campaign_logs ──────────────────────────────────────────────────
|
|
70
|
+
server.tool("add_campaign_logs", "Record multiple campaign log entries in a single call. Same schema as add_campaign_log, " +
|
|
71
|
+
"but accepts an array of logs for efficient bulk ingestion (e.g. when replaying " +
|
|
72
|
+
"a batch of n8n workflow events). Requires admin permissions.", {
|
|
73
|
+
logs: z
|
|
74
|
+
.array(campaignLogInput)
|
|
75
|
+
.min(1)
|
|
76
|
+
.describe("Array of campaign log entries to create (minimum 1)."),
|
|
77
|
+
}, async ({ logs }) => {
|
|
78
|
+
try {
|
|
79
|
+
const data = await getClient().post("/add-lead-log", logs.map(mapLog));
|
|
80
|
+
return toolResult(data);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return handleToolError(error);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
// ── get_campaign_logs ──────────────────────────────────────────────────
|
|
87
|
+
server.tool("get_campaign_logs", "Retrieve campaign activity logs with flexible filtering by campaign slug, lead ID, " +
|
|
88
|
+
"lead group, action type, severity level, and date range. Supports pagination and " +
|
|
89
|
+
"sorting by createdAt, level, action, or campaignSlug. Returns log entries enriched " +
|
|
90
|
+
"with basic lead data. Requires admin permissions. Use this to audit campaign " +
|
|
91
|
+
"activity, investigate errors, or review a specific lead's interaction history.", {
|
|
92
|
+
campaign_slug: z.string().optional().describe("Filter by campaign slug."),
|
|
93
|
+
lead_id: z
|
|
94
|
+
.string()
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("Filter by lead ID — returns all logs for that lead."),
|
|
97
|
+
leadgroup_id: z
|
|
98
|
+
.string()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Filter by lead group ID — returns logs for all leads in the group."),
|
|
101
|
+
action: z
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("Filter by action (partial match). E.g. 'Email' matches 'Email sent'."),
|
|
105
|
+
level: logLevelEnum.optional().describe("Filter by severity level."),
|
|
106
|
+
date_from: z
|
|
107
|
+
.string()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe("Start date filter in ISO 8601 format (e.g. '2024-01-01T00:00:00Z')."),
|
|
110
|
+
date_to: z
|
|
111
|
+
.string()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe("End date filter in ISO 8601 format."),
|
|
114
|
+
page: z
|
|
115
|
+
.number()
|
|
116
|
+
.int()
|
|
117
|
+
.positive()
|
|
118
|
+
.optional()
|
|
119
|
+
.describe("Page number (1-based). Defaults to 1."),
|
|
120
|
+
limit: z
|
|
121
|
+
.number()
|
|
122
|
+
.int()
|
|
123
|
+
.positive()
|
|
124
|
+
.max(100)
|
|
125
|
+
.optional()
|
|
126
|
+
.describe("Results per page. Defaults to 50, max 100."),
|
|
127
|
+
sort_by: z
|
|
128
|
+
.enum(["createdAt", "level", "action", "campaignSlug"])
|
|
129
|
+
.optional()
|
|
130
|
+
.describe("Field to sort by. Defaults to 'createdAt'."),
|
|
131
|
+
sort_order: z
|
|
132
|
+
.enum(["asc", "desc"])
|
|
133
|
+
.optional()
|
|
134
|
+
.describe("Sort direction. Defaults to 'desc'."),
|
|
135
|
+
}, async (params) => {
|
|
136
|
+
try {
|
|
137
|
+
const qp = new URLSearchParams();
|
|
138
|
+
if (params.campaign_slug)
|
|
139
|
+
qp.set("campaign_slug", params.campaign_slug);
|
|
140
|
+
if (params.lead_id)
|
|
141
|
+
qp.set("lead_id", params.lead_id);
|
|
142
|
+
if (params.leadgroup_id)
|
|
143
|
+
qp.set("leadgroup_id", params.leadgroup_id);
|
|
144
|
+
if (params.action)
|
|
145
|
+
qp.set("action", params.action);
|
|
146
|
+
if (params.level)
|
|
147
|
+
qp.set("level", params.level);
|
|
148
|
+
if (params.date_from)
|
|
149
|
+
qp.set("date_from", params.date_from);
|
|
150
|
+
if (params.date_to)
|
|
151
|
+
qp.set("date_to", params.date_to);
|
|
152
|
+
if (params.page !== undefined)
|
|
153
|
+
qp.set("page", String(params.page));
|
|
154
|
+
if (params.limit !== undefined)
|
|
155
|
+
qp.set("limit", String(params.limit));
|
|
156
|
+
if (params.sort_by)
|
|
157
|
+
qp.set("sort_by", params.sort_by);
|
|
158
|
+
if (params.sort_order)
|
|
159
|
+
qp.set("sort_order", params.sort_order);
|
|
160
|
+
const data = await getClient().get("/get-lead-logs", qp);
|
|
161
|
+
return toolResult(data);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return handleToolError(error);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
// ── delete_campaign_log ────────────────────────────────────────────────
|
|
168
|
+
server.tool("delete_campaign_log", "Delete a single campaign log entry by its ID. This action is irreversible. " +
|
|
169
|
+
"Requires admin permissions. Use get_campaign_logs first to find the log ID. " +
|
|
170
|
+
"Always confirm with the user before calling this tool.", {
|
|
171
|
+
id: z.string().describe("ID of the campaign log entry to delete."),
|
|
172
|
+
}, async ({ id }) => {
|
|
173
|
+
try {
|
|
174
|
+
const data = await getClient().post("/delete-lead-log", { id });
|
|
175
|
+
return toolResult(data);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
return handleToolError(error);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getClient } from "../client.js";
|
|
2
|
+
import { toolResult, handleToolError } from "../types.js";
|
|
3
|
+
export function registerOrganizationTools(server) {
|
|
4
|
+
// ── list_organizations ─────────────────────────────────────────────────
|
|
5
|
+
server.tool("list_organizations", "List every organization accessible to the caller, returning each with id, name, and " +
|
|
6
|
+
"optional slug. Use this as the starting point when you need an organization_id — " +
|
|
7
|
+
"e.g. before create_lead_group, clone_lead_group, upsert_persona (org-scoped), " +
|
|
8
|
+
"or move_persona.", {}, async () => {
|
|
9
|
+
try {
|
|
10
|
+
const data = await getClient().get("/api/organizations");
|
|
11
|
+
return toolResult(data);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return handleToolError(error);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getClient } from "../client.js";
|
|
3
|
+
import { toolResult, toolError, handleToolError } from "../types.js";
|
|
4
|
+
// ── Schemas mirror apps/server/src/lib/outreach-settings-schemas.ts ────────
|
|
5
|
+
const sequenceBlock = z
|
|
6
|
+
.object({
|
|
7
|
+
goal: z.string().optional(),
|
|
8
|
+
coreElements: z.array(z.string()).optional(),
|
|
9
|
+
})
|
|
10
|
+
.passthrough();
|
|
11
|
+
const positioning = z
|
|
12
|
+
.object({
|
|
13
|
+
dream: z.string().max(200).optional(),
|
|
14
|
+
fear: z.string().max(200).optional(),
|
|
15
|
+
whyNow: z.string().max(200).optional(),
|
|
16
|
+
})
|
|
17
|
+
.passthrough();
|
|
18
|
+
const sequenceDesign = z
|
|
19
|
+
.object({
|
|
20
|
+
connexion: sequenceBlock.optional(),
|
|
21
|
+
linkedin: z
|
|
22
|
+
.object({
|
|
23
|
+
one: sequenceBlock.optional(),
|
|
24
|
+
two: sequenceBlock.optional(),
|
|
25
|
+
three: sequenceBlock.optional(),
|
|
26
|
+
})
|
|
27
|
+
.partial()
|
|
28
|
+
.passthrough()
|
|
29
|
+
.optional(),
|
|
30
|
+
email: z
|
|
31
|
+
.object({
|
|
32
|
+
one: sequenceBlock.optional(),
|
|
33
|
+
two: sequenceBlock.optional(),
|
|
34
|
+
three: sequenceBlock.optional(),
|
|
35
|
+
})
|
|
36
|
+
.partial()
|
|
37
|
+
.passthrough()
|
|
38
|
+
.optional(),
|
|
39
|
+
})
|
|
40
|
+
.passthrough();
|
|
41
|
+
const rules = z
|
|
42
|
+
.object({
|
|
43
|
+
forbidden: z.array(z.string()).optional(),
|
|
44
|
+
priority: z.array(z.string()).optional(),
|
|
45
|
+
format: z
|
|
46
|
+
.object({
|
|
47
|
+
signatureEmail: z.string().optional(),
|
|
48
|
+
signatureLinkedin: z.string().optional(),
|
|
49
|
+
vouvoiement: z.boolean().optional(),
|
|
50
|
+
bannedChars: z.array(z.string()).optional(),
|
|
51
|
+
bannedStyles: z.array(z.string()).optional(),
|
|
52
|
+
maxWordsPerSentence: z.number().int().positive().optional(),
|
|
53
|
+
})
|
|
54
|
+
.passthrough()
|
|
55
|
+
.optional(),
|
|
56
|
+
})
|
|
57
|
+
.passthrough();
|
|
58
|
+
const caseStudy = z
|
|
59
|
+
.object({
|
|
60
|
+
title: z.string().min(1),
|
|
61
|
+
summary: z.string().max(200),
|
|
62
|
+
idealFor: z.string().optional(),
|
|
63
|
+
proofPoints: z.array(z.string()).optional(),
|
|
64
|
+
})
|
|
65
|
+
.passthrough();
|
|
66
|
+
const urlValue = z
|
|
67
|
+
.union([z.literal(""), z.string().url()])
|
|
68
|
+
.nullable()
|
|
69
|
+
.optional();
|
|
70
|
+
async function fetchCurrentSettings(leadGroupId) {
|
|
71
|
+
const data = await getClient().get(`/api/lead-group/${encodeURIComponent(leadGroupId)}/outreach-settings`);
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
async function putPartial(leadGroupId, body) {
|
|
75
|
+
return getClient().put(`/api/lead-group/${encodeURIComponent(leadGroupId)}/outreach-settings`, body);
|
|
76
|
+
}
|
|
77
|
+
export function registerOutreachSettingsTools(server) {
|
|
78
|
+
// ── get_outreach_settings ──────────────────────────────────────────────
|
|
79
|
+
server.tool("get_outreach_settings", "Get the outreach settings (positioning, sequence design, rules, URLs, case studies) " +
|
|
80
|
+
"for a specific lead group / campaign. The settings drive the [Leadify] Outreach agent " +
|
|
81
|
+
"preflight. GET auto-creates a row with defaults on first read.", {
|
|
82
|
+
lead_group_id: z.string().describe("Lead group ID."),
|
|
83
|
+
}, async ({ lead_group_id }) => {
|
|
84
|
+
try {
|
|
85
|
+
const data = await getClient().get(`/api/lead-group/${encodeURIComponent(lead_group_id)}/outreach-settings`);
|
|
86
|
+
return toolResult(data);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return handleToolError(error);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// ── update_outreach_settings ───────────────────────────────────────────
|
|
93
|
+
// Full-form escape hatch: set the whole block at once. Prefer the
|
|
94
|
+
// granular tools below when you only want to touch a single slot / field.
|
|
95
|
+
server.tool("update_outreach_settings", "Full-form update (escape hatch). Replaces JSON blocks wholesale when " +
|
|
96
|
+
"provided — if you only want to tweak a single slot or sub-field, prefer " +
|
|
97
|
+
"update_outreach_positioning / update_outreach_sequence_slot / " +
|
|
98
|
+
"update_outreach_rules / update_outreach_case_study / update_outreach_urls / " +
|
|
99
|
+
"set_outreach_connection_request, which do safe read-modify-write merges.", {
|
|
100
|
+
lead_group_id: z.string().describe("Lead group ID."),
|
|
101
|
+
connection_request_enabled: z
|
|
102
|
+
.boolean()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("If true, first touchpoint is a LinkedIn connection request; if false, " +
|
|
105
|
+
"card_linkedin_one is written as a cold message."),
|
|
106
|
+
positioning: positioning.optional().describe("Value Equation — 3 short phrases (200 chars max each): dream (what this lead " +
|
|
107
|
+
"group wants), fear (what they dread), whyNow (why act now rather than later)."),
|
|
108
|
+
sequence_design: sequenceDesign
|
|
109
|
+
.optional()
|
|
110
|
+
.describe("Per-message blueprints keyed by channel/slot (connexion, linkedin.one/two/three, " +
|
|
111
|
+
"email.one/two/three). Each block = {goal, coreElements[]}."),
|
|
112
|
+
rules: rules
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("Formatting/content rules: forbidden phrases, priority points, and format " +
|
|
115
|
+
"(signatures, vouvoiement, bannedChars, bannedStyles, maxWordsPerSentence)."),
|
|
116
|
+
booking_url: urlValue.describe("Booking URL (e.g. Cal.com). Empty string or null clears it."),
|
|
117
|
+
website_url: urlValue.describe("Website URL. Empty string or null clears it."),
|
|
118
|
+
case_studies: z
|
|
119
|
+
.array(caseStudy)
|
|
120
|
+
.max(5)
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Up to 5 case studies used as proof (title, summary ≤200 chars, idealFor, proofPoints[])."),
|
|
123
|
+
}, async ({ lead_group_id, connection_request_enabled, positioning: pos, sequence_design, rules: rls, booking_url, website_url, case_studies, }) => {
|
|
124
|
+
try {
|
|
125
|
+
const body = {};
|
|
126
|
+
if (connection_request_enabled !== undefined)
|
|
127
|
+
body.connectionRequestEnabled = connection_request_enabled;
|
|
128
|
+
if (pos !== undefined)
|
|
129
|
+
body.positioning = pos;
|
|
130
|
+
if (sequence_design !== undefined)
|
|
131
|
+
body.sequenceDesign = sequence_design;
|
|
132
|
+
if (rls !== undefined)
|
|
133
|
+
body.rules = rls;
|
|
134
|
+
if (booking_url !== undefined)
|
|
135
|
+
body.bookingUrl = booking_url;
|
|
136
|
+
if (website_url !== undefined)
|
|
137
|
+
body.websiteUrl = website_url;
|
|
138
|
+
if (case_studies !== undefined)
|
|
139
|
+
body.caseStudies = case_studies;
|
|
140
|
+
const data = await putPartial(lead_group_id, body);
|
|
141
|
+
return toolResult(data);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return handleToolError(error);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
// ── update_outreach_positioning ────────────────────────────────────────
|
|
148
|
+
server.tool("update_outreach_positioning", "Patch only the positioning block (dream/fear/whyNow). Reads the current " +
|
|
149
|
+
"positioning, merges the fields you pass, and writes it back. Pass any " +
|
|
150
|
+
"subset of dream/fear/whyNow — unspecified fields stay intact.", {
|
|
151
|
+
lead_group_id: z.string().describe("Lead group ID."),
|
|
152
|
+
dream: z
|
|
153
|
+
.string()
|
|
154
|
+
.max(200)
|
|
155
|
+
.optional()
|
|
156
|
+
.describe("What this lead group wants (≤200 chars)."),
|
|
157
|
+
fear: z
|
|
158
|
+
.string()
|
|
159
|
+
.max(200)
|
|
160
|
+
.optional()
|
|
161
|
+
.describe("What they dread (≤200 chars)."),
|
|
162
|
+
whyNow: z
|
|
163
|
+
.string()
|
|
164
|
+
.max(200)
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("Why act now rather than later (≤200 chars)."),
|
|
167
|
+
}, async ({ lead_group_id, dream, fear, whyNow }) => {
|
|
168
|
+
try {
|
|
169
|
+
if (dream === undefined && fear === undefined && whyNow === undefined) {
|
|
170
|
+
return toolError("Provide at least one of: dream, fear, whyNow.");
|
|
171
|
+
}
|
|
172
|
+
const current = await fetchCurrentSettings(lead_group_id);
|
|
173
|
+
const merged = {
|
|
174
|
+
...(current.positioning ?? {}),
|
|
175
|
+
...(dream !== undefined && { dream }),
|
|
176
|
+
...(fear !== undefined && { fear }),
|
|
177
|
+
...(whyNow !== undefined && { whyNow }),
|
|
178
|
+
};
|
|
179
|
+
const data = await putPartial(lead_group_id, { positioning: merged });
|
|
180
|
+
return toolResult(data);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return handleToolError(error);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
// ── update_outreach_sequence_slot ──────────────────────────────────────
|
|
187
|
+
server.tool("update_outreach_sequence_slot", "Patch a single sequence slot (connexion, linkedin.one/two/three, or " +
|
|
188
|
+
"email.one/two/three). Reads the current sequenceDesign, mutates just the " +
|
|
189
|
+
"target slot, and writes the whole block back so other slots are untouched. " +
|
|
190
|
+
"goal and coreElements within the slot are each replaced wholesale when passed.", {
|
|
191
|
+
lead_group_id: z.string().describe("Lead group ID."),
|
|
192
|
+
channel: z
|
|
193
|
+
.enum(["connexion", "linkedin", "email"])
|
|
194
|
+
.describe("Channel: 'connexion' (single slot, LinkedIn invite note), 'linkedin' " +
|
|
195
|
+
"(slots one/two/three), or 'email' (slots one/two/three)."),
|
|
196
|
+
slot: z
|
|
197
|
+
.enum(["one", "two", "three"])
|
|
198
|
+
.optional()
|
|
199
|
+
.describe("Slot index. Required when channel is 'linkedin' or 'email'. Ignored for 'connexion'."),
|
|
200
|
+
goal: z
|
|
201
|
+
.string()
|
|
202
|
+
.optional()
|
|
203
|
+
.describe("The message goal (one sentence)."),
|
|
204
|
+
core_elements: z
|
|
205
|
+
.array(z.string())
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("List of elements the message must include (short bullet points). Replaces the array wholesale."),
|
|
208
|
+
}, async ({ lead_group_id, channel, slot, goal, core_elements }) => {
|
|
209
|
+
try {
|
|
210
|
+
if (goal === undefined && core_elements === undefined) {
|
|
211
|
+
return toolError("Provide at least one of: goal, core_elements.");
|
|
212
|
+
}
|
|
213
|
+
if (channel !== "connexion" && !slot) {
|
|
214
|
+
return toolError(`slot is required when channel is '${channel}'.`);
|
|
215
|
+
}
|
|
216
|
+
const current = await fetchCurrentSettings(lead_group_id);
|
|
217
|
+
const next = { ...(current.sequenceDesign ?? {}) };
|
|
218
|
+
if (channel === "connexion") {
|
|
219
|
+
const existing = (next.connexion ?? {});
|
|
220
|
+
next.connexion = {
|
|
221
|
+
...existing,
|
|
222
|
+
...(goal !== undefined && { goal }),
|
|
223
|
+
...(core_elements !== undefined && { coreElements: core_elements }),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const channelExisting = {
|
|
228
|
+
...(next[channel] ?? {}),
|
|
229
|
+
};
|
|
230
|
+
const slotExisting = (channelExisting[slot] ??
|
|
231
|
+
{});
|
|
232
|
+
channelExisting[slot] = {
|
|
233
|
+
...slotExisting,
|
|
234
|
+
...(goal !== undefined && { goal }),
|
|
235
|
+
...(core_elements !== undefined && { coreElements: core_elements }),
|
|
236
|
+
};
|
|
237
|
+
next[channel] = channelExisting;
|
|
238
|
+
}
|
|
239
|
+
const data = await putPartial(lead_group_id, { sequenceDesign: next });
|
|
240
|
+
return toolResult(data);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
return handleToolError(error);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
// ── update_outreach_rules ──────────────────────────────────────────────
|
|
247
|
+
server.tool("update_outreach_rules", "Patch only the rules block. forbidden and priority arrays are replaced " +
|
|
248
|
+
"wholesale when passed. format.* sub-fields are deep-merged individually " +
|
|
249
|
+
"(signatureEmail/signatureLinkedin/vouvoiement/bannedChars/bannedStyles/" +
|
|
250
|
+
"maxWordsPerSentence) — pass only what you want to change.", {
|
|
251
|
+
lead_group_id: z.string().describe("Lead group ID."),
|
|
252
|
+
forbidden: z
|
|
253
|
+
.array(z.string())
|
|
254
|
+
.optional()
|
|
255
|
+
.describe("Forbidden phrases (replaces the array when passed)."),
|
|
256
|
+
priority: z
|
|
257
|
+
.array(z.string())
|
|
258
|
+
.optional()
|
|
259
|
+
.describe("Priority points (replaces the array when passed)."),
|
|
260
|
+
format: z
|
|
261
|
+
.object({
|
|
262
|
+
signatureEmail: z.string().optional(),
|
|
263
|
+
signatureLinkedin: z.string().optional(),
|
|
264
|
+
vouvoiement: z.boolean().optional(),
|
|
265
|
+
bannedChars: z.array(z.string()).optional(),
|
|
266
|
+
bannedStyles: z.array(z.string()).optional(),
|
|
267
|
+
maxWordsPerSentence: z.number().int().positive().optional(),
|
|
268
|
+
})
|
|
269
|
+
.passthrough()
|
|
270
|
+
.optional()
|
|
271
|
+
.describe("Format sub-fields: only fields passed are updated. Arrays (bannedChars/bannedStyles) are replaced wholesale."),
|
|
272
|
+
}, async ({ lead_group_id, forbidden, priority, format }) => {
|
|
273
|
+
try {
|
|
274
|
+
if (forbidden === undefined &&
|
|
275
|
+
priority === undefined &&
|
|
276
|
+
format === undefined) {
|
|
277
|
+
return toolError("Provide at least one of: forbidden, priority, format.");
|
|
278
|
+
}
|
|
279
|
+
const current = await fetchCurrentSettings(lead_group_id);
|
|
280
|
+
const currentRules = (current.rules ?? {});
|
|
281
|
+
const next = { ...currentRules };
|
|
282
|
+
if (forbidden !== undefined)
|
|
283
|
+
next.forbidden = forbidden;
|
|
284
|
+
if (priority !== undefined)
|
|
285
|
+
next.priority = priority;
|
|
286
|
+
if (format !== undefined) {
|
|
287
|
+
next.format = {
|
|
288
|
+
...(currentRules.format ?? {}),
|
|
289
|
+
...format,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const data = await putPartial(lead_group_id, { rules: next });
|
|
293
|
+
return toolResult(data);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
return handleToolError(error);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
// ── update_outreach_case_study ─────────────────────────────────────────
|
|
300
|
+
server.tool("update_outreach_case_study", "Add, replace, or remove a single case study without re-sending the whole " +
|
|
301
|
+
"list. Reads the current caseStudies array, mutates the target entry, " +
|
|
302
|
+
"and writes the full list back. Cap is 5 entries.", {
|
|
303
|
+
lead_group_id: z.string().describe("Lead group ID."),
|
|
304
|
+
action: z
|
|
305
|
+
.enum(["add", "replace", "remove"])
|
|
306
|
+
.describe("add = append (fails if list is at cap 5); replace = overwrite at index; remove = delete at index."),
|
|
307
|
+
index: z
|
|
308
|
+
.number()
|
|
309
|
+
.int()
|
|
310
|
+
.nonnegative()
|
|
311
|
+
.optional()
|
|
312
|
+
.describe("Zero-based index. Required for 'replace' and 'remove'. Ignored for 'add'."),
|
|
313
|
+
case_study: caseStudy
|
|
314
|
+
.optional()
|
|
315
|
+
.describe("Case study payload (title, summary ≤200 chars, idealFor, proofPoints[]). Required for 'add' and 'replace'."),
|
|
316
|
+
}, async ({ lead_group_id, action, index, case_study }) => {
|
|
317
|
+
try {
|
|
318
|
+
if ((action === "add" || action === "replace") && !case_study) {
|
|
319
|
+
return toolError(`case_study is required for action '${action}'.`);
|
|
320
|
+
}
|
|
321
|
+
if ((action === "replace" || action === "remove") &&
|
|
322
|
+
index === undefined) {
|
|
323
|
+
return toolError(`index is required for action '${action}'.`);
|
|
324
|
+
}
|
|
325
|
+
const current = await fetchCurrentSettings(lead_group_id);
|
|
326
|
+
const list = Array.isArray(current.caseStudies)
|
|
327
|
+
? [...current.caseStudies]
|
|
328
|
+
: [];
|
|
329
|
+
if (action === "add") {
|
|
330
|
+
if (list.length >= 5) {
|
|
331
|
+
return toolError("Cannot add — case studies list is at cap (5). Use 'replace' or 'remove' first.");
|
|
332
|
+
}
|
|
333
|
+
list.push(case_study);
|
|
334
|
+
}
|
|
335
|
+
else if (action === "replace") {
|
|
336
|
+
if (index < 0 || index >= list.length) {
|
|
337
|
+
return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
|
|
338
|
+
}
|
|
339
|
+
list[index] = case_study;
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
if (index < 0 || index >= list.length) {
|
|
343
|
+
return toolError(`Index ${index} out of bounds (list has ${list.length} entries).`);
|
|
344
|
+
}
|
|
345
|
+
list.splice(index, 1);
|
|
346
|
+
}
|
|
347
|
+
const data = await putPartial(lead_group_id, { caseStudies: list });
|
|
348
|
+
return toolResult(data);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
return handleToolError(error);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
// ── update_outreach_urls ───────────────────────────────────────────────
|
|
355
|
+
server.tool("update_outreach_urls", "Patch bookingUrl and/or websiteUrl only. Empty string or null clears a " +
|
|
356
|
+
"field. Other outreach settings are untouched.", {
|
|
357
|
+
lead_group_id: z.string().describe("Lead group ID."),
|
|
358
|
+
booking_url: urlValue.describe("Booking URL (e.g. Cal.com). Empty string or null clears it."),
|
|
359
|
+
website_url: urlValue.describe("Website URL. Empty string or null clears it."),
|
|
360
|
+
}, async ({ lead_group_id, booking_url, website_url }) => {
|
|
361
|
+
try {
|
|
362
|
+
if (booking_url === undefined && website_url === undefined) {
|
|
363
|
+
return toolError("Provide at least one of: booking_url, website_url.");
|
|
364
|
+
}
|
|
365
|
+
const body = {};
|
|
366
|
+
if (booking_url !== undefined)
|
|
367
|
+
body.bookingUrl = booking_url;
|
|
368
|
+
if (website_url !== undefined)
|
|
369
|
+
body.websiteUrl = website_url;
|
|
370
|
+
const data = await putPartial(lead_group_id, body);
|
|
371
|
+
return toolResult(data);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
return handleToolError(error);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
// ── set_outreach_connection_request ────────────────────────────────────
|
|
378
|
+
server.tool("set_outreach_connection_request", "Toggle whether the first LinkedIn touchpoint is a connection request " +
|
|
379
|
+
"(true) or a cold direct message (false). When false, card_linkedin_one " +
|
|
380
|
+
"is written as a cold first-contact message and the update_lead handler " +
|
|
381
|
+
"rejects any write to card_connexion.", {
|
|
382
|
+
lead_group_id: z.string().describe("Lead group ID."),
|
|
383
|
+
enabled: z
|
|
384
|
+
.boolean()
|
|
385
|
+
.describe("true = connection request first; false = cold DM."),
|
|
386
|
+
}, async ({ lead_group_id, enabled }) => {
|
|
387
|
+
try {
|
|
388
|
+
const data = await putPartial(lead_group_id, {
|
|
389
|
+
connectionRequestEnabled: enabled,
|
|
390
|
+
});
|
|
391
|
+
return toolResult(data);
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
return handleToolError(error);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|