@mentoringo/vantage-ops-mcp 1.3.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 +268 -0
- package/dist/dynamo.d.ts +70 -0
- package/dist/dynamo.js +238 -0
- package/dist/dynamo.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +163 -0
- package/dist/mcp-client.js +230 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/prompts.d.ts +14 -0
- package/dist/prompts.js +75 -0
- package/dist/prompts.js.map +1 -0
- package/dist/schemas.d.ts +397 -0
- package/dist/schemas.js +233 -0
- package/dist/schemas.js.map +1 -0
- package/dist/tools/admin-tools.d.ts +2 -0
- package/dist/tools/admin-tools.js +214 -0
- package/dist/tools/admin-tools.js.map +1 -0
- package/dist/tools/campaign-tools.d.ts +2 -0
- package/dist/tools/campaign-tools.js +219 -0
- package/dist/tools/campaign-tools.js.map +1 -0
- package/dist/tools/company-tools.d.ts +2 -0
- package/dist/tools/company-tools.js +194 -0
- package/dist/tools/company-tools.js.map +1 -0
- package/dist/tools/contact-tools.d.ts +2 -0
- package/dist/tools/contact-tools.js +162 -0
- package/dist/tools/contact-tools.js.map +1 -0
- package/dist/tools/mcp-write-tools.d.ts +16 -0
- package/dist/tools/mcp-write-tools.js +1062 -0
- package/dist/tools/mcp-write-tools.js.map +1 -0
- package/dist/tools/pipeline-tools.d.ts +2 -0
- package/dist/tools/pipeline-tools.js +199 -0
- package/dist/tools/pipeline-tools.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP write tools — thin wrappers over Vantage's /mcp/* HTTP endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Each tool validates inputs with zod, delegates to the typed HTTP
|
|
5
|
+
* client in ../mcp-client, and returns the server's JSON response as
|
|
6
|
+
* both a stringified content block (for human readability) and a
|
|
7
|
+
* structured payload (typed under the writeOutput schema). Errors
|
|
8
|
+
* surface with the Vantage API's own error message so scope-check
|
|
9
|
+
* failures (403) and validation failures (400) are self-explanatory
|
|
10
|
+
* in the Claude transcript.
|
|
11
|
+
*
|
|
12
|
+
* Scopes required per tool are documented inline; the API-key has to
|
|
13
|
+
* carry the matching scope or the request is rejected at the authorizer.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { addCampaignRecipients, addCampaignTargets, removeCampaignTarget, sourceCampaignContacts, apolloSearchContacts, apolloSearchCompanies, apolloUnlockEmail, apolloImportContacts, authConfigured, apiUrlForDiagnostics, createCampaign, createCompany, createContact, createOpportunity, createOutreach, createTemplate, deleteCampaign, deleteCompany, enrichCompany, deleteContact, deleteOpportunity, deleteTemplate, logActivity, pauseCampaign, startCampaign, updateCampaign, updateCampaignRecipient, updateCompany, updateContact, updateOpportunity, updateOutreach, updateTemplate, VantageApiError, } from "../mcp-client.js";
|
|
17
|
+
import { writeOutput, healthOutput } from "../schemas.js";
|
|
18
|
+
function asContent(data) {
|
|
19
|
+
const summary = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
20
|
+
const record = data !== null && typeof data === "object"
|
|
21
|
+
? data
|
|
22
|
+
: undefined;
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: summary }],
|
|
25
|
+
structuredContent: {
|
|
26
|
+
success: true,
|
|
27
|
+
record,
|
|
28
|
+
summary,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function asError(err) {
|
|
33
|
+
if (err instanceof VantageApiError) {
|
|
34
|
+
const summary = `Vantage API error (${err.status}): ${err.message}\n\n${JSON.stringify(err.body, null, 2)}`;
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: summary }],
|
|
37
|
+
structuredContent: {
|
|
38
|
+
success: false,
|
|
39
|
+
message: err.message,
|
|
40
|
+
record: err.body !== null && typeof err.body === "object"
|
|
41
|
+
? err.body
|
|
42
|
+
: undefined,
|
|
43
|
+
summary,
|
|
44
|
+
},
|
|
45
|
+
isError: true,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
49
|
+
const summary = `Error: ${msg}`;
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text", text: summary }],
|
|
52
|
+
structuredContent: {
|
|
53
|
+
success: false,
|
|
54
|
+
message: msg,
|
|
55
|
+
summary,
|
|
56
|
+
},
|
|
57
|
+
isError: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function registerMcpWriteTools(server) {
|
|
61
|
+
// ── Opportunities ──────────────────────────────────────────────
|
|
62
|
+
server.registerTool("vantage_opportunity_create", {
|
|
63
|
+
description: "Create a new pipeline opportunity in Vantage. Writes through the authenticated /mcp path " +
|
|
64
|
+
"so slug generation, stage history, weighted value, and audit trails follow the normal Lambda " +
|
|
65
|
+
"semantics. Idempotent when an explicit UUID id is provided — replaying the same call returns " +
|
|
66
|
+
"the existing record instead of creating a duplicate. Requires API-key scope: opportunities:write.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
id: z
|
|
69
|
+
.string()
|
|
70
|
+
.uuid()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Optional UUID for idempotency. If provided and a record with this id already exists, " +
|
|
73
|
+
"it is returned unchanged; otherwise the new record is created with this id."),
|
|
74
|
+
name: z.string().min(1).describe("Human-readable opportunity name (required)"),
|
|
75
|
+
company_id: z
|
|
76
|
+
.string()
|
|
77
|
+
.min(1)
|
|
78
|
+
.describe("UUID of the company this opportunity is linked to (required)"),
|
|
79
|
+
stage: z
|
|
80
|
+
.enum([
|
|
81
|
+
"lead",
|
|
82
|
+
"qualified",
|
|
83
|
+
"discovery",
|
|
84
|
+
"demo",
|
|
85
|
+
"proposal",
|
|
86
|
+
"negotiation",
|
|
87
|
+
"closed_won",
|
|
88
|
+
"closed_lost",
|
|
89
|
+
])
|
|
90
|
+
.optional()
|
|
91
|
+
.describe("Initial pipeline stage (default: qualified)"),
|
|
92
|
+
value: z.number().optional().describe("Opportunity value in the currency unit below"),
|
|
93
|
+
currency: z.string().optional().default("USD").describe("ISO currency code"),
|
|
94
|
+
probability: z
|
|
95
|
+
.number()
|
|
96
|
+
.min(0)
|
|
97
|
+
.max(100)
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("Win probability 0-100. Inferred from stage if omitted."),
|
|
100
|
+
expected_close_date: z
|
|
101
|
+
.string()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("ISO 8601 date, e.g. 2026-06-30"),
|
|
104
|
+
primary_contact_id: z
|
|
105
|
+
.string()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe("Primary contact UUID at the company"),
|
|
108
|
+
campaign_id: z
|
|
109
|
+
.string()
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Campaign UUID that sourced this opportunity"),
|
|
112
|
+
description: z.string().optional(),
|
|
113
|
+
notes: z.string().optional(),
|
|
114
|
+
tags: z.array(z.string()).optional(),
|
|
115
|
+
owner_id: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("Owning user UUID (defaults to the API-key's user_id)"),
|
|
119
|
+
},
|
|
120
|
+
outputSchema: writeOutput,
|
|
121
|
+
}, async (args) => {
|
|
122
|
+
try {
|
|
123
|
+
return asContent(await createOpportunity(args));
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
return asError(err);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
server.registerTool("vantage_opportunity_update_stage", {
|
|
130
|
+
description: "Advance or change an opportunity's stage. Appends a stage-history entry and recomputes " +
|
|
131
|
+
"probability + weighted_value via the API. Requires scope: opportunities:write.",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
id: z.string().min(1).describe("Opportunity UUID"),
|
|
134
|
+
stage: z
|
|
135
|
+
.enum([
|
|
136
|
+
"lead",
|
|
137
|
+
"qualified",
|
|
138
|
+
"discovery",
|
|
139
|
+
"demo",
|
|
140
|
+
"proposal",
|
|
141
|
+
"negotiation",
|
|
142
|
+
"closed_won",
|
|
143
|
+
"closed_lost",
|
|
144
|
+
])
|
|
145
|
+
.describe("Target stage"),
|
|
146
|
+
notes: z
|
|
147
|
+
.string()
|
|
148
|
+
.optional()
|
|
149
|
+
.describe("Optional note to attach to the stage change"),
|
|
150
|
+
},
|
|
151
|
+
outputSchema: writeOutput,
|
|
152
|
+
}, async ({ id, stage, notes }) => {
|
|
153
|
+
try {
|
|
154
|
+
return asContent(await updateOpportunity(id, { stage, ...(notes ? { notes } : {}) }));
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
return asError(err);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// ── Activities ─────────────────────────────────────────────────
|
|
161
|
+
server.registerTool("vantage_activity_log", {
|
|
162
|
+
description: "Log an activity / touchpoint against a company, contact, or opportunity. " +
|
|
163
|
+
"Use for deck sends, call notes, meeting summaries, follow-up reminders. " +
|
|
164
|
+
"Requires scope: activities:write.",
|
|
165
|
+
inputSchema: {
|
|
166
|
+
entity_type: z
|
|
167
|
+
.enum(["company", "contact", "opportunity"])
|
|
168
|
+
.describe("Type of entity this activity relates to"),
|
|
169
|
+
entity_id: z.string().min(1).describe("UUID of the linked entity"),
|
|
170
|
+
activity_type: z
|
|
171
|
+
.string()
|
|
172
|
+
.describe("Activity classifier: 'email_sent', 'call_logged', 'meeting', 'deck_sent', 'note', etc."),
|
|
173
|
+
summary: z.string().min(1).describe("One-line summary shown in activity lists"),
|
|
174
|
+
detail: z.string().optional().describe("Longer body / notes"),
|
|
175
|
+
occurred_at: z
|
|
176
|
+
.string()
|
|
177
|
+
.optional()
|
|
178
|
+
.describe("ISO 8601 timestamp when the activity happened (default: now)"),
|
|
179
|
+
},
|
|
180
|
+
outputSchema: writeOutput,
|
|
181
|
+
}, async (args) => {
|
|
182
|
+
try {
|
|
183
|
+
return asContent(await logActivity(args));
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
return asError(err);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// ── Contacts ───────────────────────────────────────────────────
|
|
190
|
+
server.registerTool("vantage_contact_create", {
|
|
191
|
+
description: "Create a new contact in Vantage. Links to a company by UUID. " +
|
|
192
|
+
"Idempotent when an explicit UUID id is provided — replaying the same call returns the " +
|
|
193
|
+
"existing record instead of creating a duplicate. Requires scope: contacts:write.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
id: z
|
|
196
|
+
.string()
|
|
197
|
+
.uuid()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe("Optional UUID for idempotency. If provided and a record with this id already exists, " +
|
|
200
|
+
"it is returned unchanged; otherwise the new record is created with this id."),
|
|
201
|
+
first_name: z.string().min(1).describe("First name (required)"),
|
|
202
|
+
last_name: z.string().optional(),
|
|
203
|
+
email: z.string().email().optional(),
|
|
204
|
+
company_id: z
|
|
205
|
+
.string()
|
|
206
|
+
.uuid()
|
|
207
|
+
.describe("Company UUID the contact is linked to. Required — v2 contacts " +
|
|
208
|
+
"must belong to a company (the company's client_id powers the " +
|
|
209
|
+
"by-client GSI). If unknown, look up or create the company first."),
|
|
210
|
+
title: z.string().optional().describe("Job title"),
|
|
211
|
+
role: z
|
|
212
|
+
.string()
|
|
213
|
+
.optional()
|
|
214
|
+
.describe("BD role: decision_maker, champion, evaluator, etc."),
|
|
215
|
+
phone: z.string().optional(),
|
|
216
|
+
linkedin_url: z.string().optional(),
|
|
217
|
+
notes: z.string().optional(),
|
|
218
|
+
},
|
|
219
|
+
outputSchema: writeOutput,
|
|
220
|
+
}, async (args) => {
|
|
221
|
+
try {
|
|
222
|
+
return asContent(await createContact(args));
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
return asError(err);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
server.registerTool("vantage_contact_update", {
|
|
229
|
+
description: "Update an existing contact's fields. Merge-patch semantics — only the fields " +
|
|
230
|
+
"provided are changed. Requires scope: contacts:write.",
|
|
231
|
+
inputSchema: {
|
|
232
|
+
id: z.string().min(1).describe("Contact UUID"),
|
|
233
|
+
first_name: z.string().optional(),
|
|
234
|
+
last_name: z.string().optional(),
|
|
235
|
+
email: z.string().email().optional(),
|
|
236
|
+
title: z.string().optional(),
|
|
237
|
+
role: z.string().optional(),
|
|
238
|
+
phone: z.string().optional(),
|
|
239
|
+
linkedin_url: z.string().optional(),
|
|
240
|
+
notes: z.string().optional(),
|
|
241
|
+
},
|
|
242
|
+
outputSchema: writeOutput,
|
|
243
|
+
}, async ({ id, ...updates }) => {
|
|
244
|
+
try {
|
|
245
|
+
return asContent(await updateContact(id, updates));
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
return asError(err);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// ── Companies ──────────────────────────────────────────────────
|
|
252
|
+
server.registerTool("vantage_company_create", {
|
|
253
|
+
description: "Create a new company (account) in Vantage. Links to a client tenant. " +
|
|
254
|
+
"Apollo enrichment runs automatically when domain is provided and the " +
|
|
255
|
+
"structured fields it returns (annual_revenue → estimated_revenue, " +
|
|
256
|
+
"employee_count → estimated_employees, linkedin_url) are auto-promoted " +
|
|
257
|
+
"to top-level fields when the caller doesn't supply them. " +
|
|
258
|
+
"Requires scope: companies:write.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
name: z.string().min(1).describe("Company name (required)"),
|
|
261
|
+
client_id: z
|
|
262
|
+
.string()
|
|
263
|
+
.min(1)
|
|
264
|
+
.describe("Client tenant UUID the company belongs to. Required — companies are always client-scoped."),
|
|
265
|
+
vertical: z
|
|
266
|
+
.string()
|
|
267
|
+
.optional()
|
|
268
|
+
.describe("Vertical (sort key). Defaults to 'general'."),
|
|
269
|
+
tier: z
|
|
270
|
+
.enum(["enterprise", "large", "mid-market", "smb", "emerging", "unclassified"])
|
|
271
|
+
.optional(),
|
|
272
|
+
status: z
|
|
273
|
+
.enum([
|
|
274
|
+
"new",
|
|
275
|
+
"prospect",
|
|
276
|
+
"target",
|
|
277
|
+
"active",
|
|
278
|
+
"qualified",
|
|
279
|
+
"engaged",
|
|
280
|
+
"opportunity",
|
|
281
|
+
"customer",
|
|
282
|
+
"churned",
|
|
283
|
+
"disqualified",
|
|
284
|
+
])
|
|
285
|
+
.optional(),
|
|
286
|
+
domain: z.string().optional().describe("Primary domain; triggers Apollo enrichment"),
|
|
287
|
+
industry: z.string().optional(),
|
|
288
|
+
description: z.string().optional(),
|
|
289
|
+
website: z.string().optional(),
|
|
290
|
+
linkedin_url: z.string().optional(),
|
|
291
|
+
headquarters: z.string().optional().describe("Free-form HQ string, e.g. 'Denver, CO'"),
|
|
292
|
+
city: z.string().optional(),
|
|
293
|
+
state: z.string().optional(),
|
|
294
|
+
country: z.string().optional(),
|
|
295
|
+
estimated_revenue: z
|
|
296
|
+
.number()
|
|
297
|
+
.optional()
|
|
298
|
+
.describe("Annual revenue in USD (numeric). Apollo can fill this when domain is provided."),
|
|
299
|
+
estimated_employees: z
|
|
300
|
+
.number()
|
|
301
|
+
.optional()
|
|
302
|
+
.describe("Total employee count. Apollo can fill this when domain is provided."),
|
|
303
|
+
key_services: z.array(z.string()).optional(),
|
|
304
|
+
specializations: z.array(z.string()).optional(),
|
|
305
|
+
partner_tier: z.string().optional(),
|
|
306
|
+
verticals: z.array(z.string()).optional().describe("Multi-vertical tagging (distinct from primary `vertical` sort key)."),
|
|
307
|
+
notes: z.string().optional(),
|
|
308
|
+
tags: z.array(z.string()).optional(),
|
|
309
|
+
},
|
|
310
|
+
outputSchema: writeOutput,
|
|
311
|
+
}, async (args) => {
|
|
312
|
+
try {
|
|
313
|
+
return asContent(await createCompany(args));
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
return asError(err);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
server.registerTool("vantage_company_update", {
|
|
320
|
+
description: "Update an existing company's fields (merge-patch). " +
|
|
321
|
+
"Lookup accepts UUID or slug. Requires scope: companies:write.",
|
|
322
|
+
inputSchema: {
|
|
323
|
+
id: z.string().min(1).describe("Company UUID or slug"),
|
|
324
|
+
name: z.string().optional(),
|
|
325
|
+
domain: z.string().optional(),
|
|
326
|
+
industry: z.string().optional(),
|
|
327
|
+
tier: z
|
|
328
|
+
.enum(["enterprise", "large", "mid-market", "smb", "emerging", "unclassified"])
|
|
329
|
+
.optional(),
|
|
330
|
+
status: z
|
|
331
|
+
.enum([
|
|
332
|
+
"new",
|
|
333
|
+
"prospect",
|
|
334
|
+
"target",
|
|
335
|
+
"active",
|
|
336
|
+
"qualified",
|
|
337
|
+
"engaged",
|
|
338
|
+
"opportunity",
|
|
339
|
+
"customer",
|
|
340
|
+
"churned",
|
|
341
|
+
"disqualified",
|
|
342
|
+
])
|
|
343
|
+
.optional(),
|
|
344
|
+
headquarters: z.string().optional().describe("Free-form HQ string, e.g. 'Denver, CO'"),
|
|
345
|
+
estimated_revenue: z.number().optional().describe("Annual revenue in USD"),
|
|
346
|
+
estimated_employees: z.number().optional().describe("Total employee count"),
|
|
347
|
+
key_services: z.array(z.string()).optional(),
|
|
348
|
+
specializations: z.array(z.string()).optional(),
|
|
349
|
+
partner_tier: z.string().optional(),
|
|
350
|
+
verticals: z.array(z.string()).optional(),
|
|
351
|
+
lead_score: z.number().optional(),
|
|
352
|
+
fit_score: z.number().optional(),
|
|
353
|
+
tags: z.array(z.string()).optional(),
|
|
354
|
+
notes: z.string().optional(),
|
|
355
|
+
logo_url: z.string().optional(),
|
|
356
|
+
},
|
|
357
|
+
outputSchema: writeOutput,
|
|
358
|
+
}, async ({ id, ...updates }) => {
|
|
359
|
+
try {
|
|
360
|
+
return asContent(await updateCompany(id, updates));
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
return asError(err);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
server.registerTool("vantage_company_delete", {
|
|
367
|
+
description: "Delete a company by UUID or slug. Destructive — intended for test cleanup " +
|
|
368
|
+
"or disambiguating duplicates. Requires scope: companies:write.",
|
|
369
|
+
inputSchema: {
|
|
370
|
+
id: z.string().min(1).describe("Company UUID or slug"),
|
|
371
|
+
},
|
|
372
|
+
outputSchema: writeOutput,
|
|
373
|
+
}, async ({ id }) => {
|
|
374
|
+
try {
|
|
375
|
+
return asContent(await deleteCompany(id));
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
return asError(err);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
server.registerTool("vantage_company_enrich", {
|
|
382
|
+
description: "Re-run Apollo enrichment on an existing company and promote the result " +
|
|
383
|
+
"to top-level fields (estimated_revenue, estimated_employees, linkedin_url). " +
|
|
384
|
+
"Use when the company was created without a domain (Apollo never ran), " +
|
|
385
|
+
"was created before the auto-promotion fix landed (data sits in custom_fields " +
|
|
386
|
+
"but top-level fields are blank), or when Apollo data has gone stale. " +
|
|
387
|
+
"Falls back to the cached apollo_enrichment block when Apollo returns nothing, " +
|
|
388
|
+
"so backfill works without burning credits. Caller-set values are never " +
|
|
389
|
+
"clobbered. Requires scope: companies:write.",
|
|
390
|
+
inputSchema: {
|
|
391
|
+
id: z.string().min(1).describe("Company UUID or slug"),
|
|
392
|
+
},
|
|
393
|
+
outputSchema: writeOutput,
|
|
394
|
+
}, async ({ id }) => {
|
|
395
|
+
try {
|
|
396
|
+
return asContent(await enrichCompany(id));
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
return asError(err);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
// ── Contact / Opportunity deletes ──────────────────────────────
|
|
403
|
+
server.registerTool("vantage_contact_delete", {
|
|
404
|
+
description: "Delete a contact by UUID. Destructive. Requires scope: contacts:write.",
|
|
405
|
+
inputSchema: {
|
|
406
|
+
id: z.string().min(1).describe("Contact UUID"),
|
|
407
|
+
},
|
|
408
|
+
outputSchema: writeOutput,
|
|
409
|
+
}, async ({ id }) => {
|
|
410
|
+
try {
|
|
411
|
+
return asContent(await deleteContact(id));
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
return asError(err);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
server.registerTool("vantage_opportunity_update", {
|
|
418
|
+
description: "Update fields on an opportunity (merge-patch) beyond the stage. For stage " +
|
|
419
|
+
"changes use vantage_opportunity_update_stage (appends stage history). " +
|
|
420
|
+
"Requires scope: opportunities:write.",
|
|
421
|
+
inputSchema: {
|
|
422
|
+
id: z.string().min(1).describe("Opportunity UUID"),
|
|
423
|
+
name: z.string().optional(),
|
|
424
|
+
description: z.string().optional(),
|
|
425
|
+
value: z.number().optional(),
|
|
426
|
+
probability: z.number().min(0).max(100).optional(),
|
|
427
|
+
currency: z.string().optional(),
|
|
428
|
+
expected_close_date: z.string().optional().describe("ISO 8601 date"),
|
|
429
|
+
next_step: z.string().optional(),
|
|
430
|
+
next_step_date: z.string().optional().describe("ISO 8601 date"),
|
|
431
|
+
primary_contact_id: z.string().uuid().optional(),
|
|
432
|
+
owner_id: z.string().uuid().optional(),
|
|
433
|
+
notes: z.string().optional(),
|
|
434
|
+
tags: z.array(z.string()).optional(),
|
|
435
|
+
},
|
|
436
|
+
outputSchema: writeOutput,
|
|
437
|
+
}, async ({ id, ...updates }) => {
|
|
438
|
+
try {
|
|
439
|
+
return asContent(await updateOpportunity(id, updates));
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
return asError(err);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
server.registerTool("vantage_opportunity_delete", {
|
|
446
|
+
description: "Delete an opportunity by UUID. Destructive. Requires scope: opportunities:write.",
|
|
447
|
+
inputSchema: {
|
|
448
|
+
id: z.string().min(1).describe("Opportunity UUID"),
|
|
449
|
+
},
|
|
450
|
+
outputSchema: writeOutput,
|
|
451
|
+
}, async ({ id }) => {
|
|
452
|
+
try {
|
|
453
|
+
return asContent(await deleteOpportunity(id));
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
return asError(err);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
// ── Campaigns ──────────────────────────────────────────────────
|
|
460
|
+
server.registerTool("vantage_campaign_create", {
|
|
461
|
+
description: "Create a new outreach campaign. Starts in 'draft' status; use " +
|
|
462
|
+
"vantage_campaign_start to activate. Pass `id` for idempotent " +
|
|
463
|
+
"re-runs — calling twice with the same UUID returns the existing " +
|
|
464
|
+
"record (HTTP 200) instead of creating a duplicate. " +
|
|
465
|
+
"Requires scope: campaigns:write.",
|
|
466
|
+
inputSchema: {
|
|
467
|
+
id: z
|
|
468
|
+
.string()
|
|
469
|
+
.uuid()
|
|
470
|
+
.optional()
|
|
471
|
+
.describe("Optional caller-supplied UUID. If a campaign with this id " +
|
|
472
|
+
"exists, the call is a no-op replay and returns the existing " +
|
|
473
|
+
"record. Omit to let the server generate a fresh UUID."),
|
|
474
|
+
name: z.string().min(1).describe("Campaign name"),
|
|
475
|
+
client_id: z.string().min(1).describe("Client tenant UUID"),
|
|
476
|
+
campaign_type: z
|
|
477
|
+
.enum(["email", "linkedin", "multi_channel", "multi"])
|
|
478
|
+
.optional()
|
|
479
|
+
.default("multi_channel"),
|
|
480
|
+
vertical: z.string().optional().describe("Target vertical"),
|
|
481
|
+
description: z.string().optional(),
|
|
482
|
+
goal_type: z
|
|
483
|
+
.enum(["meetings", "replies", "opens", "conversions"])
|
|
484
|
+
.optional()
|
|
485
|
+
.describe("What the campaign is optimizing for"),
|
|
486
|
+
goal_target: z
|
|
487
|
+
.number()
|
|
488
|
+
.optional()
|
|
489
|
+
.describe("Numeric target for the goal (e.g. 5 meetings)"),
|
|
490
|
+
},
|
|
491
|
+
outputSchema: writeOutput,
|
|
492
|
+
}, async (args) => {
|
|
493
|
+
try {
|
|
494
|
+
return asContent(await createCampaign(args));
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
return asError(err);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
server.registerTool("vantage_campaign_update", {
|
|
501
|
+
description: "Update campaign fields (name, description, goal, schedule, send rate, " +
|
|
502
|
+
"templates, AI flags, tags, etc). Merge-patch semantics. " +
|
|
503
|
+
"For status transitions prefer vantage_campaign_start / vantage_campaign_pause. " +
|
|
504
|
+
"For target/recipient list ops prefer vantage_campaign_add_targets / " +
|
|
505
|
+
"vantage_campaign_add_recipients. Requires scope: campaigns:write.",
|
|
506
|
+
inputSchema: {
|
|
507
|
+
id: z.string().min(1).describe("Campaign UUID or slug"),
|
|
508
|
+
name: z.string().optional(),
|
|
509
|
+
description: z.string().optional(),
|
|
510
|
+
campaign_type: z
|
|
511
|
+
.enum(["email", "linkedin", "multi_channel", "multi"])
|
|
512
|
+
.optional(),
|
|
513
|
+
vertical: z.string().optional(),
|
|
514
|
+
goal_type: z
|
|
515
|
+
.enum(["meetings", "replies", "opens", "conversions"])
|
|
516
|
+
.optional(),
|
|
517
|
+
goal_target: z.number().optional(),
|
|
518
|
+
daily_send_limit: z
|
|
519
|
+
.number()
|
|
520
|
+
.optional()
|
|
521
|
+
.describe("Max touches per day across the campaign"),
|
|
522
|
+
desired_outcome: z.string().optional(),
|
|
523
|
+
scheduled_start: z
|
|
524
|
+
.string()
|
|
525
|
+
.optional()
|
|
526
|
+
.describe("ISO 8601 timestamp when the campaign should activate"),
|
|
527
|
+
scheduled_end: z
|
|
528
|
+
.string()
|
|
529
|
+
.optional()
|
|
530
|
+
.describe("ISO 8601 timestamp when the campaign should auto-pause"),
|
|
531
|
+
ai_suggestions_enabled: z.boolean().optional(),
|
|
532
|
+
ai_auto_optimize: z.boolean().optional(),
|
|
533
|
+
tags: z.array(z.string()).optional(),
|
|
534
|
+
target_tiers: z
|
|
535
|
+
.array(z.string())
|
|
536
|
+
.optional()
|
|
537
|
+
.describe("Eligible company tiers (enterprise / large / mid-market / smb / emerging)"),
|
|
538
|
+
target_company_ids: z.array(z.string().uuid()).optional(),
|
|
539
|
+
target_contact_ids: z.array(z.string().uuid()).optional(),
|
|
540
|
+
target_criteria: z
|
|
541
|
+
.record(z.unknown())
|
|
542
|
+
.optional()
|
|
543
|
+
.describe("Free-form ICP filter criteria"),
|
|
544
|
+
primary_template_id: z
|
|
545
|
+
.string()
|
|
546
|
+
.uuid()
|
|
547
|
+
.optional()
|
|
548
|
+
.describe("Primary outreach template"),
|
|
549
|
+
template_ids: z
|
|
550
|
+
.array(z.string().uuid())
|
|
551
|
+
.optional()
|
|
552
|
+
.describe("All eligible outreach templates"),
|
|
553
|
+
follow_up_template_ids: z
|
|
554
|
+
.array(z.string().uuid())
|
|
555
|
+
.optional(),
|
|
556
|
+
sequence_steps: z
|
|
557
|
+
.array(z.record(z.unknown()))
|
|
558
|
+
.optional()
|
|
559
|
+
.describe("Multi-step outreach cadence definition"),
|
|
560
|
+
template_id: z
|
|
561
|
+
.string()
|
|
562
|
+
.uuid()
|
|
563
|
+
.optional()
|
|
564
|
+
.describe("Deprecated alias of primary_template_id; prefer primary_template_id"),
|
|
565
|
+
ab_test_template_ids: z
|
|
566
|
+
.array(z.string().uuid())
|
|
567
|
+
.optional()
|
|
568
|
+
.describe("A/B test variant template UUIDs"),
|
|
569
|
+
},
|
|
570
|
+
outputSchema: writeOutput,
|
|
571
|
+
}, async ({ id, ...updates }) => {
|
|
572
|
+
try {
|
|
573
|
+
return asContent(await updateCampaign(id, updates));
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
return asError(err);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
server.registerTool("vantage_campaign_delete", {
|
|
580
|
+
description: "Delete a campaign. Active campaigns cannot be deleted — pause first. " +
|
|
581
|
+
"Requires scope: campaigns:write.",
|
|
582
|
+
inputSchema: {
|
|
583
|
+
id: z.string().min(1).describe("Campaign UUID or slug"),
|
|
584
|
+
},
|
|
585
|
+
outputSchema: writeOutput,
|
|
586
|
+
}, async ({ id }) => {
|
|
587
|
+
try {
|
|
588
|
+
return asContent(await deleteCampaign(id));
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
return asError(err);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
server.registerTool("vantage_campaign_start", {
|
|
595
|
+
description: "Flip a draft or paused campaign to active status. Does not itself send " +
|
|
596
|
+
"messages — Vantage is a tracker, not a sender. Requires scope: campaigns:write.",
|
|
597
|
+
inputSchema: {
|
|
598
|
+
id: z.string().min(1).describe("Campaign UUID or slug"),
|
|
599
|
+
},
|
|
600
|
+
outputSchema: writeOutput,
|
|
601
|
+
}, async ({ id }) => {
|
|
602
|
+
try {
|
|
603
|
+
return asContent(await startCampaign(id));
|
|
604
|
+
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
return asError(err);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
server.registerTool("vantage_campaign_pause", {
|
|
610
|
+
description: "Pause an active campaign. Requires scope: campaigns:write.",
|
|
611
|
+
inputSchema: {
|
|
612
|
+
id: z.string().min(1).describe("Campaign UUID or slug"),
|
|
613
|
+
},
|
|
614
|
+
outputSchema: writeOutput,
|
|
615
|
+
}, async ({ id }) => {
|
|
616
|
+
try {
|
|
617
|
+
return asContent(await pauseCampaign(id));
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
return asError(err);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
server.registerTool("vantage_campaign_add_targets", {
|
|
624
|
+
description: "Attach one or more companies to a campaign's target list (junction table). " +
|
|
625
|
+
"Targets are the universe the campaign can draw recipients from. " +
|
|
626
|
+
"Requires scope: campaigns:write.",
|
|
627
|
+
inputSchema: {
|
|
628
|
+
id: z.string().min(1).describe("Campaign UUID or slug"),
|
|
629
|
+
company_ids: z
|
|
630
|
+
.array(z.string().uuid())
|
|
631
|
+
.min(1)
|
|
632
|
+
.describe("Company UUIDs to attach"),
|
|
633
|
+
priority: z.enum(["high", "normal", "low"]).optional().default("normal"),
|
|
634
|
+
},
|
|
635
|
+
outputSchema: writeOutput,
|
|
636
|
+
}, async ({ id, ...body }) => {
|
|
637
|
+
try {
|
|
638
|
+
return asContent(await addCampaignTargets(id, body));
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
return asError(err);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
server.registerTool("vantage_campaign_remove_target", {
|
|
645
|
+
description: "Remove a single company from a campaign's target list (junction table). " +
|
|
646
|
+
"Inverse of vantage_campaign_add_targets. Use this to clean up dangling " +
|
|
647
|
+
"junction entries after a target company is deleted, or to drop a target " +
|
|
648
|
+
"from a draft campaign before launch. Returns 404 if the (campaign, company) " +
|
|
649
|
+
"pair has no junction row. Requires scope: campaigns:write.",
|
|
650
|
+
inputSchema: {
|
|
651
|
+
id: z.string().min(1).describe("Campaign UUID or slug"),
|
|
652
|
+
company_id: z
|
|
653
|
+
.string()
|
|
654
|
+
.uuid()
|
|
655
|
+
.describe("Company UUID to detach from the campaign"),
|
|
656
|
+
},
|
|
657
|
+
outputSchema: writeOutput,
|
|
658
|
+
}, async ({ id, company_id }) => {
|
|
659
|
+
try {
|
|
660
|
+
return asContent(await removeCampaignTarget(id, company_id));
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
return asError(err);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
server.registerTool("vantage_campaign_add_recipients", {
|
|
667
|
+
description: "Add contacts to a campaign's active recipient list (engagement_status=pending). " +
|
|
668
|
+
"Recipients are the actual people messages would be addressed to. " +
|
|
669
|
+
"Requires scope: campaigns:write.",
|
|
670
|
+
inputSchema: {
|
|
671
|
+
id: z.string().min(1).describe("Campaign UUID or slug"),
|
|
672
|
+
contact_ids: z
|
|
673
|
+
.array(z.string().uuid())
|
|
674
|
+
.min(1)
|
|
675
|
+
.describe("Contact UUIDs to add as recipients"),
|
|
676
|
+
},
|
|
677
|
+
outputSchema: writeOutput,
|
|
678
|
+
}, async ({ id, ...body }) => {
|
|
679
|
+
try {
|
|
680
|
+
return asContent(await addCampaignRecipients(id, body));
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
return asError(err);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
server.registerTool("vantage_campaign_recipient_update", {
|
|
687
|
+
description: "Update a single recipient's engagement_status on a campaign. Use this to " +
|
|
688
|
+
"reflect events (invited / sent / opened / clicked / replied / bounced / " +
|
|
689
|
+
"unsubscribed) that don't auto-propagate from outreach records onto the " +
|
|
690
|
+
"campaign's embedded recipients map. Typical pattern: after " +
|
|
691
|
+
"vantage_outreach_update marks the outreach as 'replied', also call this " +
|
|
692
|
+
"tool so the campaign-recipient row in the Vantage UI reflects the same " +
|
|
693
|
+
"state. Stamps the matching timestamp (invited_at / sent_at / opened_at / " +
|
|
694
|
+
"clicked_at / replied_at / bounced_at / unsubscribed_at) automatically. " +
|
|
695
|
+
"Requires scope: campaigns:write.\n\n" +
|
|
696
|
+
"State guide:\n" +
|
|
697
|
+
" - pending: added to the campaign, no action yet\n" +
|
|
698
|
+
" - invited: connection attempt initiated (e.g. LinkedIn connect request) — no message sent yet\n" +
|
|
699
|
+
" - sent: outbound message sent\n" +
|
|
700
|
+
" - opened: target opened the message\n" +
|
|
701
|
+
" - clicked: target clicked a link in the message\n" +
|
|
702
|
+
" - replied: target responded\n" +
|
|
703
|
+
" - bounced: delivery failed (invalid address, etc.)\n" +
|
|
704
|
+
" - unsubscribed: target explicitly opted out (compliance-relevant — distinct from failed)\n" +
|
|
705
|
+
" - failed: technical send failure (provider error, retry exhausted, etc.)",
|
|
706
|
+
inputSchema: {
|
|
707
|
+
id: z.string().min(1).describe("Campaign UUID or slug"),
|
|
708
|
+
contact_id: z.string().uuid().describe("Recipient contact UUID"),
|
|
709
|
+
engagement_status: z
|
|
710
|
+
.enum([
|
|
711
|
+
"pending",
|
|
712
|
+
"invited",
|
|
713
|
+
"sent",
|
|
714
|
+
"opened",
|
|
715
|
+
"clicked",
|
|
716
|
+
"replied",
|
|
717
|
+
"bounced",
|
|
718
|
+
"unsubscribed",
|
|
719
|
+
"failed",
|
|
720
|
+
])
|
|
721
|
+
.describe("New engagement_status to set on the recipient row"),
|
|
722
|
+
occurred_at: z
|
|
723
|
+
.string()
|
|
724
|
+
.optional()
|
|
725
|
+
.describe("ISO 8601 timestamp for when the event occurred (default: now). " +
|
|
726
|
+
"Only stamped on the matching state transition (e.g., replied_at when " +
|
|
727
|
+
"engagement_status='replied')."),
|
|
728
|
+
},
|
|
729
|
+
outputSchema: writeOutput,
|
|
730
|
+
}, async ({ id, contact_id, ...body }) => {
|
|
731
|
+
try {
|
|
732
|
+
return asContent(await updateCampaignRecipient(id, contact_id, body));
|
|
733
|
+
}
|
|
734
|
+
catch (err) {
|
|
735
|
+
return asError(err);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
server.registerTool("vantage_campaign_source_contacts", {
|
|
739
|
+
description: "Search Apollo for contacts at each company targeted by a campaign, create them as Vantage " +
|
|
740
|
+
"contacts, and add them to the campaign's recipient list in one shot. " +
|
|
741
|
+
"Defaults to DC-ops personas (Directors/VPs of Data Center Operations, Critical Facilities, etc.) " +
|
|
742
|
+
"and manager/director/vp/c_suite seniority levels. " +
|
|
743
|
+
"Returns a per-company summary of contacts added. " +
|
|
744
|
+
"Requires scope: campaigns:write.",
|
|
745
|
+
inputSchema: {
|
|
746
|
+
id: z.string().min(1).describe("Campaign UUID or slug"),
|
|
747
|
+
titles: z
|
|
748
|
+
.array(z.string())
|
|
749
|
+
.optional()
|
|
750
|
+
.describe("Job title filters to search Apollo for (default: DC-ops personas). " +
|
|
751
|
+
"Pass an array to override the defaults."),
|
|
752
|
+
seniorities: z
|
|
753
|
+
.array(z.string())
|
|
754
|
+
.optional()
|
|
755
|
+
.describe("Seniority levels: manager, director, vp, c_suite, senior (default: all four)."),
|
|
756
|
+
limit_per_company: z
|
|
757
|
+
.number()
|
|
758
|
+
.int()
|
|
759
|
+
.min(1)
|
|
760
|
+
.max(10)
|
|
761
|
+
.optional()
|
|
762
|
+
.describe("Max contacts to add per target company (default: 3, max: 10)"),
|
|
763
|
+
},
|
|
764
|
+
outputSchema: writeOutput,
|
|
765
|
+
}, async ({ id, ...body }) => {
|
|
766
|
+
try {
|
|
767
|
+
return asContent(await sourceCampaignContacts(id, body));
|
|
768
|
+
}
|
|
769
|
+
catch (err) {
|
|
770
|
+
return asError(err);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
// ── Templates ──────────────────────────────────────────────────
|
|
774
|
+
server.registerTool("vantage_template_create", {
|
|
775
|
+
description: "Create a message template for campaigns. Templates can be attached to one " +
|
|
776
|
+
"or more campaigns via vantage_campaign_update (template_id field). " +
|
|
777
|
+
"Requires scope: templates:write.",
|
|
778
|
+
inputSchema: {
|
|
779
|
+
name: z.string().min(1).describe("Template name (internal reference)"),
|
|
780
|
+
body: z.string().min(1).describe("Message body (supports variables like {{first_name}})"),
|
|
781
|
+
subject: z.string().optional().describe("Subject line (for email templates)"),
|
|
782
|
+
description: z.string().optional(),
|
|
783
|
+
cta: z.string().optional().describe("Call-to-action text"),
|
|
784
|
+
campaign_type: z
|
|
785
|
+
.enum(["email", "linkedin", "multi_channel"])
|
|
786
|
+
.optional()
|
|
787
|
+
.default("email"),
|
|
788
|
+
target_vertical: z.string().optional(),
|
|
789
|
+
target_persona: z
|
|
790
|
+
.string()
|
|
791
|
+
.optional()
|
|
792
|
+
.describe("Target persona: decision_maker, champion, evaluator, etc."),
|
|
793
|
+
},
|
|
794
|
+
outputSchema: writeOutput,
|
|
795
|
+
}, async (args) => {
|
|
796
|
+
try {
|
|
797
|
+
return asContent(await createTemplate(args));
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
return asError(err);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
server.registerTool("vantage_template_update", {
|
|
804
|
+
description: "Update a template's fields. Merge-patch semantics. " +
|
|
805
|
+
"Requires scope: templates:write.",
|
|
806
|
+
inputSchema: {
|
|
807
|
+
id: z.string().min(1).describe("Template UUID"),
|
|
808
|
+
name: z.string().optional(),
|
|
809
|
+
body: z.string().optional(),
|
|
810
|
+
subject: z.string().optional(),
|
|
811
|
+
description: z.string().optional(),
|
|
812
|
+
cta: z.string().optional(),
|
|
813
|
+
target_vertical: z.string().optional(),
|
|
814
|
+
target_persona: z.string().optional(),
|
|
815
|
+
},
|
|
816
|
+
outputSchema: writeOutput,
|
|
817
|
+
}, async ({ id, ...updates }) => {
|
|
818
|
+
try {
|
|
819
|
+
return asContent(await updateTemplate(id, updates));
|
|
820
|
+
}
|
|
821
|
+
catch (err) {
|
|
822
|
+
return asError(err);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
server.registerTool("vantage_template_delete", {
|
|
826
|
+
description: "Delete a template by UUID. Requires scope: templates:write.",
|
|
827
|
+
inputSchema: {
|
|
828
|
+
id: z.string().min(1).describe("Template UUID"),
|
|
829
|
+
},
|
|
830
|
+
outputSchema: writeOutput,
|
|
831
|
+
}, async ({ id }) => {
|
|
832
|
+
try {
|
|
833
|
+
return asContent(await deleteTemplate(id));
|
|
834
|
+
}
|
|
835
|
+
catch (err) {
|
|
836
|
+
return asError(err);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
// ── Outreach records ───────────────────────────────────────────
|
|
840
|
+
// Vantage is an outreach TRACKER, not a sender. These tools record
|
|
841
|
+
// that a message was sent externally (via LinkedIn, email provider,
|
|
842
|
+
// extension, etc.) so engagement can be tracked + attributed.
|
|
843
|
+
server.registerTool("vantage_outreach_record", {
|
|
844
|
+
description: "Record an outbound message that was sent externally. Vantage does not itself " +
|
|
845
|
+
"transmit messages — it tracks them. Use this to log that a campaign touch " +
|
|
846
|
+
"happened (via LinkedIn DM, email, phone, etc.) so engagement can be " +
|
|
847
|
+
"attributed downstream. Requires scope: outreach:write.",
|
|
848
|
+
inputSchema: {
|
|
849
|
+
campaign_id: z.string().uuid().describe("Campaign UUID this touch belongs to"),
|
|
850
|
+
company_id: z.string().uuid().describe("Company UUID the recipient belongs to"),
|
|
851
|
+
contact_id: z
|
|
852
|
+
.string()
|
|
853
|
+
.uuid()
|
|
854
|
+
.optional()
|
|
855
|
+
.describe("Contact UUID (the specific recipient)"),
|
|
856
|
+
channel: z
|
|
857
|
+
.enum(["email", "linkedin", "phone", "in_person", "other"])
|
|
858
|
+
.optional()
|
|
859
|
+
.default("email"),
|
|
860
|
+
status: z
|
|
861
|
+
.enum(["sent", "opened", "replied", "bounced", "failed"])
|
|
862
|
+
.optional()
|
|
863
|
+
.default("sent"),
|
|
864
|
+
subject: z.string().optional(),
|
|
865
|
+
body: z.string().optional().describe("Message body as sent"),
|
|
866
|
+
template_id: z.string().uuid().optional().describe("Template used (if any)"),
|
|
867
|
+
sent_at: z.string().optional().describe("ISO 8601 timestamp (default: now)"),
|
|
868
|
+
},
|
|
869
|
+
outputSchema: writeOutput,
|
|
870
|
+
}, async (args) => {
|
|
871
|
+
try {
|
|
872
|
+
return asContent(await createOutreach(args));
|
|
873
|
+
}
|
|
874
|
+
catch (err) {
|
|
875
|
+
return asError(err);
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
server.registerTool("vantage_outreach_update", {
|
|
879
|
+
description: "Update an outreach record — typically to log engagement events (opened, replied) " +
|
|
880
|
+
"or attach reply text. Requires scope: outreach:write.",
|
|
881
|
+
inputSchema: {
|
|
882
|
+
id: z.string().min(1).describe("Outreach record UUID"),
|
|
883
|
+
status: z
|
|
884
|
+
.enum(["sent", "opened", "replied", "bounced", "failed"])
|
|
885
|
+
.optional(),
|
|
886
|
+
reply_text: z.string().optional(),
|
|
887
|
+
reply_sentiment: z.enum(["positive", "neutral", "negative"]).optional(),
|
|
888
|
+
notes: z.string().optional(),
|
|
889
|
+
},
|
|
890
|
+
outputSchema: writeOutput,
|
|
891
|
+
}, async ({ id, ...updates }) => {
|
|
892
|
+
try {
|
|
893
|
+
return asContent(await updateOutreach(id, updates));
|
|
894
|
+
}
|
|
895
|
+
catch (err) {
|
|
896
|
+
return asError(err);
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
// ── Apollo (people + company search, email reveal, bulk import) ──
|
|
900
|
+
server.registerTool("vantage_apollo_search_contacts", {
|
|
901
|
+
description: "Search Apollo for people matching the given criteria. Returns a list of " +
|
|
902
|
+
"person records (with masked emails) — pair with vantage_apollo_unlock_email " +
|
|
903
|
+
"to reveal a single email, or vantage_apollo_import_contact to bulk-import " +
|
|
904
|
+
"verified people into Vantage. Free of charge per search; only unlocks cost " +
|
|
905
|
+
"credits. Requires scope: apollo:write.",
|
|
906
|
+
inputSchema: {
|
|
907
|
+
domains: z
|
|
908
|
+
.union([z.array(z.string()), z.string()])
|
|
909
|
+
.optional()
|
|
910
|
+
.describe("Organization domain(s) to filter by, e.g. 'acme.com' or ['acme.com', 'beta.io']"),
|
|
911
|
+
q_organization_name: z
|
|
912
|
+
.string()
|
|
913
|
+
.optional()
|
|
914
|
+
.describe("Free-text organization-name match (use when domain unknown)"),
|
|
915
|
+
organization_ids: z
|
|
916
|
+
.array(z.string())
|
|
917
|
+
.optional()
|
|
918
|
+
.describe("Apollo organization IDs (from a prior search-companies call)"),
|
|
919
|
+
person_titles: z
|
|
920
|
+
.array(z.string())
|
|
921
|
+
.optional()
|
|
922
|
+
.describe("Title keywords, e.g. ['VP Supply Chain', 'Director Materials']"),
|
|
923
|
+
person_seniorities: z
|
|
924
|
+
.array(z.string())
|
|
925
|
+
.optional()
|
|
926
|
+
.describe("Seniority filters: manager, director, vp, c_suite, owner, partner"),
|
|
927
|
+
q_keywords: z.string().optional().describe("Free-text people search"),
|
|
928
|
+
page: z.number().int().min(1).optional().describe("Page (1-indexed)"),
|
|
929
|
+
limit: z
|
|
930
|
+
.number()
|
|
931
|
+
.int()
|
|
932
|
+
.min(1)
|
|
933
|
+
.max(100)
|
|
934
|
+
.optional()
|
|
935
|
+
.describe("Per-page (default 25, max 100)"),
|
|
936
|
+
},
|
|
937
|
+
outputSchema: writeOutput,
|
|
938
|
+
}, async (args) => {
|
|
939
|
+
try {
|
|
940
|
+
return asContent(await apolloSearchContacts(args));
|
|
941
|
+
}
|
|
942
|
+
catch (err) {
|
|
943
|
+
return asError(err);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
server.registerTool("vantage_apollo_search_companies", {
|
|
947
|
+
description: "Search Apollo for organizations matching the given criteria. Returns Apollo " +
|
|
948
|
+
"organization records (id, name, domain, employee count, industry, etc.) " +
|
|
949
|
+
"which can then feed into vantage_apollo_search_contacts (via organization_ids) " +
|
|
950
|
+
"or vantage_company_create. Requires scope: apollo:write.",
|
|
951
|
+
inputSchema: {
|
|
952
|
+
q_organization_name: z.string().optional().describe("Free-text org-name match"),
|
|
953
|
+
q_organization_domains: z
|
|
954
|
+
.union([z.array(z.string()), z.string()])
|
|
955
|
+
.optional()
|
|
956
|
+
.describe("Specific domain(s) to look up"),
|
|
957
|
+
organization_locations: z
|
|
958
|
+
.array(z.string())
|
|
959
|
+
.optional()
|
|
960
|
+
.describe("City/state/country filters, e.g. ['Atlanta, Georgia', 'Texas']"),
|
|
961
|
+
num_employees_ranges: z
|
|
962
|
+
.array(z.string())
|
|
963
|
+
.optional()
|
|
964
|
+
.describe("Employee-count buckets, e.g. ['1,10', '11,50', '51,200']"),
|
|
965
|
+
page: z.number().int().min(1).optional(),
|
|
966
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
967
|
+
},
|
|
968
|
+
outputSchema: writeOutput,
|
|
969
|
+
}, async (args) => {
|
|
970
|
+
try {
|
|
971
|
+
return asContent(await apolloSearchCompanies(args));
|
|
972
|
+
}
|
|
973
|
+
catch (err) {
|
|
974
|
+
return asError(err);
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
server.registerTool("vantage_apollo_unlock_email", {
|
|
978
|
+
description: "Reveal a verified email for a single person via Apollo's people/match endpoint. " +
|
|
979
|
+
"Requires either apollo_id (preferred — from a prior search) or linkedin_url, " +
|
|
980
|
+
"plus first_name + last_name + organization_name for higher match confidence. " +
|
|
981
|
+
"Each unlock consumes Apollo credits, so prefer to unlock only after confirming " +
|
|
982
|
+
"the person fits the campaign. Requires scope: apollo:write.",
|
|
983
|
+
inputSchema: {
|
|
984
|
+
apollo_id: z
|
|
985
|
+
.string()
|
|
986
|
+
.optional()
|
|
987
|
+
.describe("Apollo person ID (from search-contacts results) — strongest signal"),
|
|
988
|
+
first_name: z.string().optional(),
|
|
989
|
+
last_name: z.string().optional(),
|
|
990
|
+
linkedin_url: z
|
|
991
|
+
.string()
|
|
992
|
+
.optional()
|
|
993
|
+
.describe("LinkedIn profile URL — fallback when apollo_id is unknown"),
|
|
994
|
+
organization_name: z
|
|
995
|
+
.string()
|
|
996
|
+
.optional()
|
|
997
|
+
.describe("Current employer name — improves match confidence"),
|
|
998
|
+
},
|
|
999
|
+
outputSchema: writeOutput,
|
|
1000
|
+
}, async (args) => {
|
|
1001
|
+
try {
|
|
1002
|
+
return asContent(await apolloUnlockEmail(args));
|
|
1003
|
+
}
|
|
1004
|
+
catch (err) {
|
|
1005
|
+
return asError(err);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
server.registerTool("vantage_apollo_import_contact", {
|
|
1009
|
+
description: "Bulk-import a list of Apollo person records into Vantage. Each record is " +
|
|
1010
|
+
"matched to (or creates, if create_missing_companies is true) a Vantage " +
|
|
1011
|
+
"company by domain, then written to vantage-contacts-v2-prod with an " +
|
|
1012
|
+
"apollo_id link for future enrichment. Use after vantage_apollo_search_contacts " +
|
|
1013
|
+
"+ vantage_apollo_unlock_email when you have a verified shortlist. " +
|
|
1014
|
+
"Requires scope: apollo:write.",
|
|
1015
|
+
inputSchema: {
|
|
1016
|
+
client_id: z.string().min(1).describe("Vantage client tenant ID"),
|
|
1017
|
+
contacts: z
|
|
1018
|
+
.array(z.record(z.string(), z.unknown()))
|
|
1019
|
+
.min(1)
|
|
1020
|
+
.describe("Array of Apollo person records (typically from search-contacts results)"),
|
|
1021
|
+
create_missing_companies: z
|
|
1022
|
+
.boolean()
|
|
1023
|
+
.optional()
|
|
1024
|
+
.describe("Create companies for unknown domains (default: true)"),
|
|
1025
|
+
default_company_id: z
|
|
1026
|
+
.string()
|
|
1027
|
+
.optional()
|
|
1028
|
+
.describe("Fallback company UUID for contacts with no resolvable organization"),
|
|
1029
|
+
},
|
|
1030
|
+
outputSchema: writeOutput,
|
|
1031
|
+
}, async (args) => {
|
|
1032
|
+
try {
|
|
1033
|
+
return asContent(await apolloImportContacts(args));
|
|
1034
|
+
}
|
|
1035
|
+
catch (err) {
|
|
1036
|
+
return asError(err);
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
// ── Diagnostics ────────────────────────────────────────────────
|
|
1040
|
+
server.registerTool("vantage_mcp_health", {
|
|
1041
|
+
description: "Diagnostic for the MCP write surface. Reports whether VANTAGE_MCP_KEY is configured " +
|
|
1042
|
+
"and the API URL the tools will call. Does not make any API request.",
|
|
1043
|
+
inputSchema: {},
|
|
1044
|
+
outputSchema: healthOutput,
|
|
1045
|
+
}, async () => {
|
|
1046
|
+
const keyConfigured = authConfigured();
|
|
1047
|
+
const apiUrl = apiUrlForDiagnostics();
|
|
1048
|
+
const hint = keyConfigured
|
|
1049
|
+
? "Ready. Provision a key at <dashboard>/api-keys with the scopes you need."
|
|
1050
|
+
: "Set VANTAGE_MCP_KEY in ~/repo/.env or the mcp.json env block.";
|
|
1051
|
+
const summary = JSON.stringify({ api_url: apiUrl, key_configured: keyConfigured, hint }, null, 2);
|
|
1052
|
+
return {
|
|
1053
|
+
content: [{ type: "text", text: summary }],
|
|
1054
|
+
structuredContent: {
|
|
1055
|
+
healthy: keyConfigured,
|
|
1056
|
+
details: { api_url: apiUrl, key_configured: keyConfigured, hint },
|
|
1057
|
+
summary,
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
//# sourceMappingURL=mcp-write-tools.js.map
|