@adrata/adrata-mcp 1.0.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.
Files changed (41) hide show
  1. package/README.md +548 -0
  2. package/access/auth.js +289 -0
  3. package/access/oauth.js +1059 -0
  4. package/access/resource-metadata.js +167 -0
  5. package/access/tiers.js +422 -0
  6. package/analytics.js +634 -0
  7. package/api-bridge.js +499 -0
  8. package/governance/money.js +141 -0
  9. package/output-formatter.js +589 -0
  10. package/package.json +68 -0
  11. package/resources.js +246 -0
  12. package/security.js +690 -0
  13. package/server.js +2139 -0
  14. package/server.json +55 -0
  15. package/skills/backlog-triage/SKILL.md +115 -0
  16. package/skills/board-review/SKILL.md +96 -0
  17. package/skills/incident-to-card/SKILL.md +126 -0
  18. package/skills/log-outreach.md +62 -0
  19. package/skills/ship-the-card/SKILL.md +155 -0
  20. package/tool-annotations.js +269 -0
  21. package/tools/billing.js +149 -0
  22. package/tools/email-tools.js +652 -0
  23. package/tools/enterprise-tools.js +651 -0
  24. package/tools/free-search.js +160 -0
  25. package/tools/memory.js +440 -0
  26. package/tools/morning-brief.js +551 -0
  27. package/tools/paper-tools.js +563 -0
  28. package/tools/scheduling.js +322 -0
  29. package/tools/work-board-tools.js +758 -0
  30. package/toolsets/communications.js +276 -0
  31. package/toolsets/crm.js +495 -0
  32. package/toolsets/extensibility.js +1131 -0
  33. package/toolsets/infrastructure.js +757 -0
  34. package/toolsets/intelligence.js +232 -0
  35. package/toolsets/knowledge.js +154 -0
  36. package/toolsets/matrix.js +217 -0
  37. package/toolsets/outreach.js +432 -0
  38. package/toolsets/prospecting.js +314 -0
  39. package/toolsets/revenue/always-loaded.js +341 -0
  40. package/toolsets/revenue/sloan-tools.js +81 -0
  41. package/transport-http.js +505 -0
@@ -0,0 +1,652 @@
1
+ /**
2
+ * Enterprise-only email infrastructure tools for Adrata MCP Server.
3
+ *
4
+ * Provides domain management, email account provisioning, and sequence
5
+ * management tools. All tools require enterprise tier (OAuth workspace
6
+ * connection) and call existing Rust API endpoints.
7
+ *
8
+ * Domain tools:
9
+ * - search_domains: find available domains for purchase
10
+ * - purchase_domain: buy a domain through Adrata
11
+ * - setup_domain: configure DNS records (SPF, DKIM, DMARC)
12
+ * - verify_domain: check DNS propagation status
13
+ * - list_domains: all workspace domains with health status
14
+ *
15
+ * Email account tools:
16
+ * - create_email_account: provision a mailbox on an owned domain
17
+ * - list_email_accounts: list mailboxes, optionally filtered by domain
18
+ * - warmup_email: start or stop gradual sending warmup
19
+ * - get_email_health: deliverability score for an account
20
+ *
21
+ * Sequence tools:
22
+ * - create_sequence: build a multi-step email sequence
23
+ * - list_sequences_full: list sequences with status and metrics
24
+ * - add_sequence_step: add a step to an existing sequence
25
+ * - activate_sequence: start sending to enrolled contacts
26
+ * - pause_sequence: pause a running sequence
27
+ * - get_sequence_analytics: open/reply/bounce rates
28
+ * - enroll_contacts: add contacts to a sequence
29
+ */
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Tool registration
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Register all email infrastructure tools on the MCP server.
37
+ *
38
+ * @param {McpServer} server - The MCP server instance (already tier-gated)
39
+ * @param {object} deps - Dependencies: { z, api, AUTH, ok, API_BASE }
40
+ */
41
+ export function registerEmailTools(server, { z, api, AUTH, ok, API_BASE }) {
42
+ /**
43
+ * Resolve a domain name (or id) to the workspace email-domain record.
44
+ * The provisioning API keys verify/dns-records routes by domain ID, while
45
+ * MCP tools accept the human-friendly domain name.
46
+ */
47
+ async function findDomainRecord(domainOrId) {
48
+ const data = await api('GET', '/api/v1/email-provisioning/domains', {
49
+ params: { limit: 200, page: 1 },
50
+ });
51
+ const items = data?.data || [];
52
+ const needle = String(domainOrId || '')
53
+ .trim()
54
+ .toLowerCase();
55
+ return (
56
+ items.find(
57
+ (d) => (d.domain && String(d.domain).toLowerCase() === needle) || d.id === domainOrId
58
+ ) || null
59
+ );
60
+ }
61
+
62
+ // =========================================================================
63
+ // DOMAIN TOOLS
64
+ // =========================================================================
65
+
66
+ server.tool(
67
+ 'search_domains',
68
+ 'Search for available domains to purchase for email sending. Returns matching domains with pricing and availability. Enterprise only.',
69
+ {
70
+ query: z.string().describe('Domain search query (e.g. "acme", "outreach-co")'),
71
+ tlds: z
72
+ .array(z.string())
73
+ .optional()
74
+ .describe('TLDs to search (default: ["com", "io", "co"]). Example: ["com", "net", "org"]'),
75
+ },
76
+ async (args) => {
77
+ // The API checks one fully-qualified domain per request, so fan out
78
+ // across the requested TLDs (or the defaults) and aggregate.
79
+ const candidates = args.query.includes('.')
80
+ ? [args.query]
81
+ : (args.tlds && args.tlds.length > 0 ? args.tlds : ['com', 'io', 'co']).map(
82
+ (tld) => `${args.query}.${tld.replace(/^\./, '')}`
83
+ );
84
+
85
+ const results = await Promise.all(
86
+ candidates.map(async (domain) => {
87
+ try {
88
+ const data = await api('GET', '/api/v1/email-provisioning/domain-purchases/search', {
89
+ params: { domain },
90
+ });
91
+ return data?.data || data;
92
+ } catch (err) {
93
+ return { domain, available: false, message: err.message };
94
+ }
95
+ })
96
+ );
97
+
98
+ const available = results.filter((r) => r?.available);
99
+ return ok({
100
+ query: args.query,
101
+ domains: results,
102
+ message:
103
+ available.length === 0
104
+ ? 'No available domains found. Try a different query or different TLDs.'
105
+ : undefined,
106
+ });
107
+ }
108
+ );
109
+
110
+ server.tool(
111
+ 'purchase_domain',
112
+ 'Purchase a domain for email sending through Adrata. The domain will be added to your workspace and ready for DNS setup. Enterprise only.',
113
+ {
114
+ domain: z.string().describe('Full domain name to purchase (e.g. "outreach-acme.com")'),
115
+ autoSetup: z
116
+ .boolean()
117
+ .optional()
118
+ .describe('Automatically configure DNS records after purchase (default: true)'),
119
+ },
120
+ async (args) => {
121
+ // Domain purchases go through a Stripe checkout session; the API does
122
+ // not charge directly. Payment completion triggers provisioning.
123
+ const data = await api('POST', '/api/v1/email-provisioning/domain-purchases/checkout', {
124
+ body: {
125
+ domain: args.domain,
126
+ returnUrl: API_BASE,
127
+ mailboxCount: 0,
128
+ },
129
+ });
130
+ const details = data?.data || data;
131
+ return ok({
132
+ purchased: false,
133
+ checkoutRequired: true,
134
+ domain: args.domain,
135
+ orderId: details?.orderId,
136
+ checkoutUrl: details?.checkoutUrl || details?.checkout_url,
137
+ totalCents: details?.totalCents,
138
+ details,
139
+ nextSteps:
140
+ 'Open checkoutUrl to complete payment. After payment, DNS and email provisioning run automatically' +
141
+ (args.autoSetup !== false
142
+ ? '. Use verify_domain to check propagation status once provisioned.'
143
+ : '. Use setup_domain to review DNS records, then verify_domain to check propagation.'),
144
+ });
145
+ }
146
+ );
147
+
148
+ server.tool(
149
+ 'setup_domain',
150
+ 'Configure DNS records (SPF, DKIM, DMARC) for an email sending domain. Required before sending emails from the domain. Enterprise only.',
151
+ {
152
+ domain: z.string().describe('Domain name to configure (e.g. "outreach-acme.com")'),
153
+ },
154
+ async (args) => {
155
+ // If the domain is not registered in the workspace yet, add it (which
156
+ // returns the DNS records to configure). Otherwise fetch its records.
157
+ const existing = await findDomainRecord(args.domain);
158
+ if (!existing) {
159
+ const data = await api('POST', '/api/v1/email-provisioning/domains', {
160
+ body: { domain: args.domain },
161
+ });
162
+ const created = data?.data || data;
163
+ return ok({
164
+ domain: args.domain,
165
+ setup: created,
166
+ dnsRecords: created?.dnsRecords,
167
+ message:
168
+ 'Domain added. Add the returned SPF, DKIM, and DMARC records at your DNS host, then use verify_domain to check propagation.',
169
+ });
170
+ }
171
+ const data = await api(
172
+ 'GET',
173
+ `/api/v1/email-provisioning/domains/${encodeURIComponent(existing.id)}/dns-records`
174
+ );
175
+ return ok({
176
+ domain: args.domain,
177
+ setup: data?.data || data,
178
+ message:
179
+ 'These are the DNS records required for this domain. Add any missing records at your DNS host, then use verify_domain to check propagation.',
180
+ });
181
+ }
182
+ );
183
+
184
+ server.tool(
185
+ 'verify_domain',
186
+ 'Check DNS propagation status for an email domain. Returns verification status for SPF, DKIM, and DMARC records. Enterprise only.',
187
+ {
188
+ domain: z.string().describe('Domain name to verify (e.g. "outreach-acme.com")'),
189
+ },
190
+ async (args) => {
191
+ const record = await findDomainRecord(args.domain);
192
+ if (!record) {
193
+ return ok({
194
+ error: 'domain_not_found',
195
+ domain: args.domain,
196
+ message: `Domain ${args.domain} is not registered in this workspace. Use setup_domain to add it first.`,
197
+ });
198
+ }
199
+ const data = await api(
200
+ 'POST',
201
+ `/api/v1/email-provisioning/domains/${encodeURIComponent(record.id)}/verify`,
202
+ {
203
+ body: {},
204
+ }
205
+ );
206
+ return ok({
207
+ domain: args.domain,
208
+ verification: data?.data || data,
209
+ });
210
+ }
211
+ );
212
+
213
+ server.tool(
214
+ 'list_domains',
215
+ 'List all email sending domains in your workspace with health status. Shows verification state, deliverability scores, and active mailbox counts. Enterprise only.',
216
+ {
217
+ status: z.string().optional().describe('Filter by status: verified, pending, failed'),
218
+ },
219
+ async (args) => {
220
+ const params = {};
221
+ if (args.status) params.status = args.status;
222
+ const data = await api('GET', '/api/v1/email-provisioning/domains', { params });
223
+ return ok({
224
+ domains: data?.data || [],
225
+ total: data?.total || data?.meta?.total || (data?.data || []).length,
226
+ });
227
+ }
228
+ );
229
+
230
+ // =========================================================================
231
+ // EMAIL ACCOUNT TOOLS
232
+ // =========================================================================
233
+
234
+ server.tool(
235
+ 'create_email_account',
236
+ 'Provision a new email mailbox on an owned domain. Creates the account and optionally starts warmup. Enterprise only.',
237
+ {
238
+ domain: z
239
+ .string()
240
+ .describe('Domain to create the account on (must be a verified domain in your workspace)'),
241
+ username: z
242
+ .string()
243
+ .describe('Username/local part of the email (e.g. "sarah" for sarah@domain.com)'),
244
+ displayName: z.string().describe('Display name for the sender (e.g. "Sarah Johnson")'),
245
+ autoWarmup: z
246
+ .boolean()
247
+ .optional()
248
+ .describe('Start warmup immediately after creation (default: false)'),
249
+ },
250
+ async (args) => {
251
+ const email = `${args.username}@${args.domain}`;
252
+ // The mailbox API takes a full email address plus optional domainId.
253
+ const domainRecord = await findDomainRecord(args.domain).catch(() => null);
254
+ const body = {
255
+ email,
256
+ displayName: args.displayName,
257
+ };
258
+ if (domainRecord?.id) body.domainId = domainRecord.id;
259
+
260
+ const data = await api('POST', '/api/v1/email-provisioning/mailboxes', { body });
261
+
262
+ const account = data?.data || data;
263
+
264
+ // Optionally start warmup
265
+ if (args.autoWarmup && account?.id) {
266
+ try {
267
+ await api('POST', `/api/v1/email-provisioning/mailboxes/${account.id}/warmup`, {
268
+ body: { enable: true },
269
+ });
270
+ return ok({
271
+ created: true,
272
+ account,
273
+ email,
274
+ warmupStarted: true,
275
+ message: `Email account ${email} created and warmup started. Use get_email_health to monitor deliverability.`,
276
+ });
277
+ } catch (warmupErr) {
278
+ return ok({
279
+ created: true,
280
+ account,
281
+ email,
282
+ warmupStarted: false,
283
+ warmupError: warmupErr.message,
284
+ message: `Email account created but warmup could not be started. Use warmup_email to start manually.`,
285
+ });
286
+ }
287
+ }
288
+
289
+ return ok({
290
+ created: true,
291
+ account,
292
+ email: `${args.username}@${args.domain}`,
293
+ message: `Email account ${args.username}@${args.domain} created. Use warmup_email to begin gradual sending warmup before using in sequences.`,
294
+ });
295
+ }
296
+ );
297
+
298
+ server.tool(
299
+ 'list_email_accounts',
300
+ 'List email mailboxes in your workspace. Optionally filter by domain. Shows warmup status and health scores. Enterprise only.',
301
+ {
302
+ domain: z.string().optional().describe('Filter by domain name'),
303
+ status: z.string().optional().describe('Filter by status: active, warming, paused, disabled'),
304
+ limit: z.number().optional().describe('Results per page (default 25)'),
305
+ page: z.number().optional().describe('Page number'),
306
+ },
307
+ async (args) => {
308
+ const params = {
309
+ limit: args.limit || 25,
310
+ page: args.page || 1,
311
+ };
312
+ if (args.status) params.status = args.status;
313
+ const [provisionedResult, connectedResult] = await Promise.allSettled([
314
+ api('GET', '/api/v1/email-provisioning/mailboxes', { params }),
315
+ api('GET', '/api/v1/oauth/email/providers'),
316
+ ]);
317
+ const provisionedData =
318
+ provisionedResult.status === 'fulfilled' ? provisionedResult.value : null;
319
+ const connectedData = connectedResult.status === 'fulfilled' ? connectedResult.value : null;
320
+ // The connected-inbox read carries its own answerPolicy from the API
321
+ // (see /api/v1/oauth/email/providers): a caller with no per-user
322
+ // identity bound (e.g. a bare OAuth client-credentials token) gets
323
+ // abstain:true rather than a silent empty list. A 200 response is NOT
324
+ // by itself proof the caller could see connected inboxes — only
325
+ // answerPolicy.abstain === false is.
326
+ const connectedAnswerPolicy = connectedData?.answerPolicy || null;
327
+ const connectedVisibilityAbstained = connectedAnswerPolicy?.abstain === true;
328
+ let accounts = [
329
+ ...(provisionedData?.data || []).map((mailbox) => ({
330
+ ...mailbox,
331
+ source: 'provisioned',
332
+ syncStatus: null,
333
+ })),
334
+ ...(connectedData?.data || []).map((provider) => ({
335
+ id: provider.id,
336
+ email: provider.email,
337
+ provider: provider.provider,
338
+ status: provider.isExpired ? 'reauth_required' : provider.status,
339
+ source: 'connected',
340
+ syncStatus: provider.lastSyncStatus || (provider.lastSyncAt ? 'succeeded' : 'pending'),
341
+ lastSyncAt: provider.lastSyncAt,
342
+ lastSyncMessageCount: provider.lastSyncMessageCount,
343
+ warmupStatus: provider.warmupStatus || provider.warmup_status || 'unknown',
344
+ reputationScore: provider.reputationScore || provider.reputation_score || null,
345
+ })),
346
+ ];
347
+ if (args.status) {
348
+ accounts = accounts.filter((account) => account.status === args.status);
349
+ }
350
+ // Neither API has a domain filter; apply it client-side on the email suffix.
351
+ if (args.domain) {
352
+ const suffix = `@${String(args.domain).trim().toLowerCase()}`;
353
+ accounts = accounts.filter((m) =>
354
+ String(m.email || '')
355
+ .toLowerCase()
356
+ .endsWith(suffix)
357
+ );
358
+ }
359
+ return ok({
360
+ accounts,
361
+ total: accounts.length,
362
+ page: args.page || 1,
363
+ // Mirrors the answerPolicy.abstain contract used elsewhere (e.g.
364
+ // get_deal_close_probability, mutual_action_plan): abstain when this
365
+ // result cannot support the claim "these are all the accounts."
366
+ answerPolicy: {
367
+ abstain: connectedVisibilityAbstained,
368
+ reason: connectedVisibilityAbstained
369
+ ? connectedAnswerPolicy.reason
370
+ : 'Connected-inbox and provisioned-mailbox reads were both scoped to this caller\'s own identity; an empty or partial account list reflects what exists, not a blind spot in this caller\'s visibility.',
371
+ },
372
+ coverage: {
373
+ connectedInboxReadSucceeded: connectedResult.status === 'fulfilled',
374
+ // True only when the connected-inbox read both succeeded AND was
375
+ // not forced to abstain for lack of a per-user identity. A prior
376
+ // version of this field was `true` whenever the HTTP call
377
+ // completed, even when it was structurally blind (scoped to an
378
+ // OAuth client identity with zero possible matches) — that made an
379
+ // empty result read as "checked, none exist" when the true state
380
+ // was "could not see."
381
+ connectedInboxVisible: connectedResult.status === 'fulfilled' && !connectedVisibilityAbstained,
382
+ provisionedMailboxReadSucceeded: provisionedResult.status === 'fulfilled',
383
+ unifiedRepliesRequireConnectedInbox: true,
384
+ },
385
+ });
386
+ }
387
+ );
388
+
389
+ server.tool(
390
+ 'warmup_email',
391
+ 'Start or stop gradual sending warmup for an email account. Warmup builds sender reputation by slowly increasing send volume. Enterprise only.',
392
+ {
393
+ accountId: z.string().describe('Email account/mailbox ID'),
394
+ action: z.enum(['start', 'stop']).describe('Start or stop the warmup process'),
395
+ },
396
+ async (args) => {
397
+ const data = await api(
398
+ 'POST',
399
+ `/api/v1/email-provisioning/mailboxes/${args.accountId}/warmup`,
400
+ {
401
+ body: { enable: args.action === 'start' },
402
+ }
403
+ );
404
+ return ok({
405
+ accountId: args.accountId,
406
+ action: args.action,
407
+ warmup: data?.data || data,
408
+ message:
409
+ args.action === 'start'
410
+ ? 'Warmup started. Send volume will gradually increase over the next 2-4 weeks. Use get_email_health to monitor progress.'
411
+ : 'Warmup stopped. The account will no longer send warmup emails.',
412
+ });
413
+ }
414
+ );
415
+
416
+ server.tool(
417
+ 'get_email_health',
418
+ 'Get the deliverability health score for an email account. Returns sender reputation, bounce rate, spam complaints, and warmup progress. Enterprise only.',
419
+ {
420
+ accountId: z.string().describe('Email account/mailbox ID'),
421
+ },
422
+ async (args) => {
423
+ // There is no dedicated /health route; the mailbox record carries
424
+ // status, warmup state, and send-limit/health fields.
425
+ const data = await api('GET', `/api/v1/email-provisioning/mailboxes/${args.accountId}`);
426
+ return ok({
427
+ accountId: args.accountId,
428
+ health: data?.data || data,
429
+ });
430
+ }
431
+ );
432
+
433
+ // =========================================================================
434
+ // SEQUENCE TOOLS
435
+ // =========================================================================
436
+
437
+ server.tool(
438
+ 'create_sequence',
439
+ 'Create a new multi-step email sequence. Define the name and optionally include initial steps. Steps specify delay, subject, and body template. Enterprise only.',
440
+ {
441
+ name: z.string().describe('Sequence name (e.g. "Cold Outreach Q2 2026")'),
442
+ description: z.string().optional().describe('Sequence description'),
443
+ steps: z
444
+ .array(
445
+ z.object({
446
+ order: z.number().describe('Step order (1-based)'),
447
+ delayDays: z
448
+ .number()
449
+ .describe(
450
+ 'Days to wait before sending this step (0 for first step = send immediately)'
451
+ ),
452
+ subject: z
453
+ .string()
454
+ .describe('Email subject line (supports {{firstName}}, {{company}} variables)'),
455
+ body: z
456
+ .string()
457
+ .describe(
458
+ 'Email body template (supports {{firstName}}, {{company}}, {{title}} variables)'
459
+ ),
460
+ type: z
461
+ .string()
462
+ .optional()
463
+ .describe(
464
+ 'Step type: email (default), linkedin_view, linkedin_connect, call, manual'
465
+ ),
466
+ })
467
+ )
468
+ .optional()
469
+ .describe('Initial sequence steps. Can also add steps later with add_sequence_step.'),
470
+ mailboxId: z.string().optional().describe('Default mailbox ID to send from'),
471
+ },
472
+ async (args) => {
473
+ const createBody = {
474
+ name: args.name,
475
+ };
476
+ if (args.description) createBody.description = args.description;
477
+ if (args.steps) createBody.steps = args.steps;
478
+ if (args.mailboxId) createBody.mailboxId = args.mailboxId;
479
+
480
+ const data = await api('POST', '/api/v1/sequences', { body: createBody });
481
+ const sequence = data?.data || data;
482
+
483
+ return ok({
484
+ created: true,
485
+ sequence,
486
+ message:
487
+ args.steps && args.steps.length > 0
488
+ ? `Sequence "${args.name}" created with ${args.steps.length} steps. Use activate_sequence to start sending, or enroll_contacts to add recipients.`
489
+ : `Sequence "${args.name}" created. Use add_sequence_step to add steps, then activate_sequence to start.`,
490
+ });
491
+ }
492
+ );
493
+
494
+ server.tool(
495
+ 'list_sequences_full',
496
+ 'List email sequences with full details including status, step count, and enrollment metrics. Enterprise only.',
497
+ {
498
+ status: z.string().optional().describe('Filter by status: draft, active, paused, completed'),
499
+ limit: z.number().optional().describe('Results per page (default 25)'),
500
+ page: z.number().optional().describe('Page number'),
501
+ },
502
+ async (args) => {
503
+ const params = {
504
+ limit: args.limit || 25,
505
+ page: args.page || 1,
506
+ };
507
+ if (args.status) params.status = args.status;
508
+ const data = await api('GET', '/api/v1/sequences', { params });
509
+ return ok({
510
+ sequences: data?.data || [],
511
+ total: data?.total || data?.meta?.total || (data?.data || []).length,
512
+ page: args.page || 1,
513
+ });
514
+ }
515
+ );
516
+
517
+ server.tool(
518
+ 'add_sequence_step',
519
+ 'Add a step to an existing sequence. Steps are executed in order with configurable delays between them. Enterprise only.',
520
+ {
521
+ sequenceId: z.string().describe('Sequence ID to add the step to'),
522
+ order: z.number().describe('Step order (1-based). Inserts at this position.'),
523
+ delayDays: z
524
+ .number()
525
+ .describe('Days to wait before sending this step (relative to previous step)'),
526
+ subject: z
527
+ .string()
528
+ .describe('Email subject line (supports {{firstName}}, {{company}} variables)'),
529
+ body: z
530
+ .string()
531
+ .describe('Email body template (supports {{firstName}}, {{company}}, {{title}} variables)'),
532
+ type: z
533
+ .string()
534
+ .optional()
535
+ .describe('Step type: email (default), linkedin_view, linkedin_connect, call, manual'),
536
+ },
537
+ async (args) => {
538
+ const stepBody = {
539
+ order: args.order,
540
+ delayDays: args.delayDays,
541
+ subject: args.subject,
542
+ body: args.body,
543
+ };
544
+ if (args.type) stepBody.type = args.type;
545
+
546
+ const data = await api('POST', `/api/v1/sequences/${args.sequenceId}/steps`, {
547
+ body: stepBody,
548
+ });
549
+ return ok({
550
+ sequenceId: args.sequenceId,
551
+ step: data?.data || data,
552
+ message: `Step ${args.order} added to sequence. Subject: "${args.subject}"`,
553
+ });
554
+ }
555
+ );
556
+
557
+ server.tool(
558
+ 'activate_sequence',
559
+ 'Activate a sequence to start sending emails to enrolled contacts. The sequence must have at least one step and enrolled contacts. Enterprise only.',
560
+ {
561
+ sequenceId: z.string().describe('Sequence ID to activate'),
562
+ },
563
+ async (args) => {
564
+ // The sequences API models activation as resume (pause/resume pair).
565
+ const data = await api('POST', `/api/v1/sequences/${args.sequenceId}/resume`, {
566
+ body: {},
567
+ });
568
+ return ok({
569
+ sequenceId: args.sequenceId,
570
+ activated: true,
571
+ details: data?.data || data,
572
+ message: 'Sequence activated. Emails will begin sending according to the step schedule.',
573
+ });
574
+ }
575
+ );
576
+
577
+ server.tool(
578
+ 'pause_sequence',
579
+ 'Pause a running sequence. Enrolled contacts will stop receiving emails until the sequence is reactivated. Enterprise only.',
580
+ {
581
+ sequenceId: z.string().describe('Sequence ID to pause'),
582
+ },
583
+ async (args) => {
584
+ const data = await api('POST', `/api/v1/sequences/${args.sequenceId}/pause`, {
585
+ body: {},
586
+ });
587
+ return ok({
588
+ sequenceId: args.sequenceId,
589
+ paused: true,
590
+ details: data?.data || data,
591
+ message: 'Sequence paused. No further emails will be sent until reactivated.',
592
+ });
593
+ }
594
+ );
595
+
596
+ server.tool(
597
+ 'get_sequence_analytics',
598
+ 'Get performance analytics for a sequence. Returns open rates, reply rates, bounce rates, unsubscribe rates, and per-step metrics. Enterprise only.',
599
+ {
600
+ sequenceId: z.string().describe('Sequence ID to get analytics for'),
601
+ },
602
+ async (args) => {
603
+ const data = await api('GET', `/api/v1/sequences/${args.sequenceId}/analytics`);
604
+ return ok({
605
+ sequenceId: args.sequenceId,
606
+ analytics: data?.data || data,
607
+ });
608
+ }
609
+ );
610
+
611
+ server.tool(
612
+ 'enroll_contacts',
613
+ 'Enroll contacts into a sequence. Contacts will begin receiving sequence emails according to the step schedule. Enterprise only.',
614
+ {
615
+ sequenceId: z.string().describe('Sequence ID to enroll contacts into'),
616
+ contactIds: z.array(z.string()).describe('Array of person/contact IDs to enroll'),
617
+ mailboxId: z.string().optional().describe('Override mailbox ID for these enrollments'),
618
+ },
619
+ async (args) => {
620
+ if (args.contactIds.length === 0) {
621
+ return ok({
622
+ error: 'empty_contacts',
623
+ message: 'No contact IDs provided. Provide at least one contact ID to enroll.',
624
+ });
625
+ }
626
+
627
+ if (args.contactIds.length > 500) {
628
+ return ok({
629
+ error: 'too_many_contacts',
630
+ message: `Cannot enroll ${args.contactIds.length} contacts at once. Maximum is 500. Split into smaller batches.`,
631
+ });
632
+ }
633
+
634
+ const enrollBody = {
635
+ contactIds: args.contactIds,
636
+ };
637
+ if (args.mailboxId) enrollBody.mailboxId = args.mailboxId;
638
+
639
+ const data = await api('POST', `/api/v1/sequences/${args.sequenceId}/enroll`, {
640
+ body: enrollBody,
641
+ });
642
+
643
+ return ok({
644
+ sequenceId: args.sequenceId,
645
+ enrolled: data?.data?.enrolled || args.contactIds.length,
646
+ skipped: data?.data?.skipped || 0,
647
+ details: data?.data || data,
648
+ message: `Enrolled ${data?.data?.enrolled || args.contactIds.length} contacts into sequence.`,
649
+ });
650
+ }
651
+ );
652
+ }