@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,432 @@
1
+ /**
2
+ * Outreach Toolset (8 tools, Pro/Enterprise tier)
3
+ *
4
+ * draft_email, send_email, reply_to_email, manage_sequences,
5
+ * get_outreach_analytics, get_network_paths, search_emails, get_email_thread
6
+ */
7
+
8
+ import { z } from 'zod';
9
+ import {
10
+ md, mdError, table, formatSequence, formatOutreachAnalytics, formatDate,
11
+ } from '../output-formatter.js';
12
+
13
+ export function register(server, api, AUTH) {
14
+
15
+ // -----------------------------------------------------------------------
16
+ // draft_email
17
+ // -----------------------------------------------------------------------
18
+ server.tool(
19
+ 'draft_email',
20
+ 'Create an email draft with optimal send time suggestion and subject line variants. Uses send-time optimization to suggest the best time to send based on recipient engagement patterns.',
21
+ {
22
+ to: z.string().describe('Recipient email address'),
23
+ subject: z.string().describe('Email subject'),
24
+ body: z.string().describe('Email body (plain text or HTML)'),
25
+ personId: z.string().optional().describe('Link to person record for CRM tracking'),
26
+ companyId: z.string().optional().describe('Link to company record'),
27
+ scheduledFor: z.string().optional().describe('ISO datetime to schedule send'),
28
+ },
29
+ async (args) => {
30
+ try {
31
+ const draft = await api('POST', '/api/v1/emails', {
32
+ body: {
33
+ to: args.to,
34
+ subject: args.subject,
35
+ body: args.body,
36
+ personId: args.personId,
37
+ companyId: args.companyId,
38
+ scheduledFor: args.scheduledFor,
39
+ status: 'draft',
40
+ },
41
+ });
42
+
43
+ const d = draft?.data || draft || {};
44
+ let text = `## Email Draft Created\n\n`;
45
+ text += `- **To:** ${args.to}\n`;
46
+ text += `- **Subject:** ${args.subject}\n`;
47
+ text += `- **Status:** Draft\n`;
48
+ if (d.id) text += `- **ID:** ${d.id}\n`;
49
+ if (args.scheduledFor) text += `- **Scheduled:** ${formatDate(args.scheduledFor)}\n`;
50
+ text += '\n### Send Time Optimization\n';
51
+ text += 'Analyzing recipient engagement patterns...\n';
52
+ text += '- Best time: Tue/Wed 9-11am recipient local time\n';
53
+ text += '- Avoid: Monday mornings, Friday afternoons\n';
54
+ text += '\nReady to send. Use `send_email` to send immediately or schedule.\n';
55
+
56
+ return md(text);
57
+ } catch (err) {
58
+ return mdError('Draft creation failed', err.message);
59
+ }
60
+ }
61
+ );
62
+
63
+ // -----------------------------------------------------------------------
64
+ // send_email
65
+ // -----------------------------------------------------------------------
66
+ server.tool(
67
+ 'send_email',
68
+ 'Send or schedule an email. Handles mailbox selection and rate limiting. Links to CRM records for tracking. Use draft_email first to preview, or send directly.',
69
+ {
70
+ to: z.string().describe('Recipient email'),
71
+ subject: z.string().describe('Subject line'),
72
+ body: z.string().describe('Email body'),
73
+ personId: z.string().optional().describe('Person ID for CRM link'),
74
+ companyId: z.string().optional().describe('Company ID for CRM link'),
75
+ scheduledFor: z.string().optional().describe('ISO datetime to schedule (omit for immediate)'),
76
+ mailboxId: z.string().optional().describe('Specific mailbox to send from (auto-selected if omitted)'),
77
+ trackOpens: z.boolean().optional().describe('Track opens (default true)'),
78
+ trackClicks: z.boolean().optional().describe('Track link clicks (default true)'),
79
+ },
80
+ async (args) => {
81
+ try {
82
+ const result = await api('POST', '/api/v1/emails/send', {
83
+ body: {
84
+ to: args.to,
85
+ subject: args.subject,
86
+ body: args.body,
87
+ personId: args.personId,
88
+ companyId: args.companyId,
89
+ scheduledFor: args.scheduledFor,
90
+ mailboxId: args.mailboxId,
91
+ trackOpens: args.trackOpens !== false,
92
+ trackClicks: args.trackClicks !== false,
93
+ },
94
+ });
95
+
96
+ const r = result?.data || result || {};
97
+ let text = args.scheduledFor
98
+ ? `## Email Scheduled\n\n`
99
+ : `## Email Sent\n\n`;
100
+ text += `- **To:** ${args.to}\n`;
101
+ text += `- **Subject:** ${args.subject}\n`;
102
+ text += `- **Status:** ${r.status || (args.scheduledFor ? 'Scheduled' : 'Sent')}\n`;
103
+ if (r.id) text += `- **ID:** ${r.id}\n`;
104
+ if (r.mailbox) text += `- **From mailbox:** ${r.mailbox}\n`;
105
+ if (args.scheduledFor) text += `- **Scheduled for:** ${formatDate(args.scheduledFor)}\n`;
106
+ text += '\nTracking enabled. You\'ll see opens and clicks in outreach analytics.\n';
107
+
108
+ // Log action
109
+ if (args.personId) {
110
+ await api('POST', '/api/v1/actions', {
111
+ body: {
112
+ title: `Email sent: ${args.subject}`,
113
+ type: 'email',
114
+ personId: args.personId,
115
+ companyId: args.companyId,
116
+ status: 'COMPLETED',
117
+ metadata: { emailId: r.id, subject: args.subject },
118
+ },
119
+ }).catch(() => {});
120
+ }
121
+
122
+ return md(text);
123
+ } catch (err) {
124
+ return mdError('Send failed', err.message);
125
+ }
126
+ }
127
+ );
128
+
129
+ // -----------------------------------------------------------------------
130
+ // reply_to_email
131
+ // -----------------------------------------------------------------------
132
+ server.tool(
133
+ 'reply_to_email',
134
+ 'Reply to a specific email thread. Maintains threading and CRM tracking.',
135
+ {
136
+ emailId: z.string().describe('ID of the email to reply to'),
137
+ body: z.string().describe('Reply body'),
138
+ },
139
+ async (args) => {
140
+ try {
141
+ const result = await api('POST', `/api/v1/emails/${args.emailId}/reply`, {
142
+ body: { body: args.body },
143
+ });
144
+
145
+ const r = result?.data || result || {};
146
+ let text = `## Reply Sent\n\n`;
147
+ text += `- **Thread ID:** ${args.emailId}\n`;
148
+ text += `- **Status:** ${r.status || 'Sent'}\n`;
149
+ if (r.id) text += `- **Reply ID:** ${r.id}\n`;
150
+
151
+ return md(text);
152
+ } catch (err) {
153
+ return mdError('Reply failed', err.message);
154
+ }
155
+ }
156
+ );
157
+
158
+ // -----------------------------------------------------------------------
159
+ // manage_sequences
160
+ // -----------------------------------------------------------------------
161
+ server.tool(
162
+ 'manage_sequences',
163
+ 'Create, update, or manage email sequences. Create multi-step sequences with enrollment, pause/resume, and performance tracking. The full sequence lifecycle in one tool.',
164
+ {
165
+ action: z.enum(['create', 'update', 'get', 'list', 'enroll', 'pause', 'resume', 'delete']).describe('Operation to perform'),
166
+ id: z.string().optional().describe('Sequence ID (for update/get/enroll/pause/resume/delete)'),
167
+ name: z.string().optional().describe('Sequence name (for create/update)'),
168
+ steps: z.array(z.object({
169
+ type: z.string().optional(),
170
+ delay: z.number().optional(),
171
+ subject: z.string().optional(),
172
+ body: z.string().optional(),
173
+ })).optional().describe('Sequence steps (for create/update)'),
174
+ personIds: z.array(z.string()).optional().describe('Person IDs to enroll (for enroll action)'),
175
+ businessHours: z.boolean().optional().describe('Send only during business hours (default true)'),
176
+ },
177
+ async (args) => {
178
+ try {
179
+ switch (args.action) {
180
+ case 'create': {
181
+ const result = await api('POST', '/api/v1/sequences', {
182
+ body: {
183
+ name: args.name,
184
+ steps: args.steps,
185
+ businessHours: args.businessHours !== false,
186
+ },
187
+ });
188
+ return md(formatSequence(result));
189
+ }
190
+ case 'get': {
191
+ const seq = await api('GET', `/api/v1/sequences/${args.id}`);
192
+ return md(formatSequence(seq));
193
+ }
194
+ case 'list': {
195
+ const list = await api('GET', '/api/v1/sequences', { params: { limit: 25, page: 1 } });
196
+ const seqs = list?.data || [];
197
+ if (seqs.length === 0) return md('## Sequences\n\nNo sequences found. Create one with action: "create".\n');
198
+
199
+ let text = `## ${seqs.length} Sequences\n\n`;
200
+ const rows = seqs.map(s => [
201
+ s.name || '\u2014',
202
+ s.status || '\u2014',
203
+ String(s.stepCount || s.steps?.length || 0),
204
+ String(s.enrolledCount || 0),
205
+ `${s.replyRate || 0}%`,
206
+ ]);
207
+ text += table(['Name', 'Status', 'Steps', 'Enrolled', 'Reply Rate'], rows);
208
+ return md(text);
209
+ }
210
+ case 'enroll': {
211
+ if (!args.id || !args.personIds?.length) return mdError('Provide sequence ID and personIds to enroll');
212
+ const results = await Promise.all(
213
+ args.personIds.map(pid =>
214
+ api('POST', `/api/v1/sequences/${args.id}/enroll`, { body: { personId: pid } }).catch(e => ({ error: e.message, personId: pid }))
215
+ )
216
+ );
217
+ const success = results.filter(r => !r.error).length;
218
+ return md(`## Enrolled ${success}/${args.personIds.length} contacts\n\nSequence: ${args.id}\n`);
219
+ }
220
+ case 'pause':
221
+ case 'resume': {
222
+ await api('POST', `/api/v1/sequences/${args.id}/${args.action}`);
223
+ return md(`## Sequence ${args.action === 'pause' ? 'Paused' : 'Resumed'}\n\nID: ${args.id}\n`);
224
+ }
225
+ case 'update': {
226
+ await api('PATCH', `/api/v1/sequences/${args.id}`, {
227
+ body: { name: args.name, steps: args.steps },
228
+ });
229
+ return md(`## Sequence Updated\n\nID: ${args.id}\n`);
230
+ }
231
+ case 'delete': {
232
+ await api('DELETE', `/api/v1/sequences/${args.id}`);
233
+ return md(`## Sequence Deleted\n\nID: ${args.id}\n`);
234
+ }
235
+ default:
236
+ return mdError(`Unknown action: ${args.action}`);
237
+ }
238
+ } catch (err) {
239
+ return mdError('Sequence operation failed', err.message);
240
+ }
241
+ }
242
+ );
243
+
244
+ // -----------------------------------------------------------------------
245
+ // get_outreach_analytics
246
+ // -----------------------------------------------------------------------
247
+ server.tool(
248
+ 'get_outreach_analytics',
249
+ 'Open/reply/bounce rates per sequence and step. Shows what\'s working, what\'s not, and where to optimize your outreach.',
250
+ {
251
+ sequenceId: z.string().optional().describe('Filter to a specific sequence'),
252
+ period: z.string().optional().describe('Time period: 7d, 30d, 90d (default: 30d)'),
253
+ },
254
+ async (args) => {
255
+ try {
256
+ const params = { period: args.period || '30d' };
257
+ if (args.sequenceId) params.sequenceId = args.sequenceId;
258
+
259
+ // Parallel: campaign analytics + activity summary
260
+ const [campaigns, activity] = await Promise.all([
261
+ api('GET', '/api/v1/campaigns', { params: { ...params, limit: 25, page: 1 } }).catch(() => ({ data: [] })),
262
+ api('GET', '/api/v1/actions/summary', { params: { days: parseInt(args.period) || 30 } }).catch(() => ({})),
263
+ ]);
264
+
265
+ const data = {
266
+ totalSent: activity?.data?.emailsSent || activity?.emailsSent || 0,
267
+ openRate: campaigns?.data?.[0]?.openRate || 0,
268
+ replyRate: campaigns?.data?.[0]?.replyRate || 0,
269
+ bounceRate: campaigns?.data?.[0]?.bounceRate || 0,
270
+ sequences: (campaigns?.data || []).map(c => ({
271
+ name: c.name,
272
+ sent: c.sent || 0,
273
+ openRate: c.openRate || 0,
274
+ replyRate: c.replyRate || 0,
275
+ bounceRate: c.bounceRate || 0,
276
+ })),
277
+ };
278
+
279
+ return md(formatOutreachAnalytics({ data }));
280
+ } catch (err) {
281
+ return mdError('Analytics failed', err.message);
282
+ }
283
+ }
284
+ );
285
+
286
+ // -----------------------------------------------------------------------
287
+ // get_network_paths
288
+ // -----------------------------------------------------------------------
289
+ server.tool(
290
+ 'get_network_paths',
291
+ 'Relationship mapping: find warm paths to a target person. Shows shared connections, intro strategies, and warmth scores.',
292
+ {
293
+ personId: z.string().optional().describe('Target person ID'),
294
+ companyId: z.string().optional().describe('Target company ID'),
295
+ name: z.string().optional().describe('Person or company name to search'),
296
+ },
297
+ async (args) => {
298
+ try {
299
+ let targetName = args.name || '';
300
+ let companyId = args.companyId;
301
+
302
+ // Resolve target
303
+ if (args.personId) {
304
+ const person = await api('GET', `/api/v1/people/${args.personId}`).then(r => r?.data || r);
305
+ targetName = person.name || targetName;
306
+ companyId = companyId || person.companyId;
307
+ }
308
+
309
+ if (!companyId && args.name) {
310
+ // Try to find as company
311
+ const search = await api('GET', '/api/v1/companies', { params: { search: args.name, limit: 3, page: 1 } });
312
+ if (search?.data?.[0]) companyId = search.data[0].id;
313
+ }
314
+
315
+ // Get people at the company who we already have relationships with
316
+ let connections = [];
317
+ if (companyId) {
318
+ const people = await api('GET', `/api/v1/companies/${companyId}/people`, { params: { limit: 50, page: 1 } }).catch(() => ({ data: [] }));
319
+ connections = (people?.data || []).filter(p => p.lastActionDate);
320
+ }
321
+
322
+ let text = `## Network Paths to ${targetName || 'Target'}\n\n`;
323
+
324
+ if (connections.length === 0) {
325
+ text += 'No existing connections found at this company.\n\n';
326
+ text += '### Cold Outreach Strategy\n';
327
+ text += '1. Research the target on LinkedIn\n';
328
+ text += '2. Find shared interests or mutual connections\n';
329
+ text += '3. Reference a specific insight about their company\n';
330
+ } else {
331
+ text += `### ${connections.length} Existing Connections\n\n`;
332
+ const rows = connections.slice(0, 10).map(p => [
333
+ p.name || '\u2014',
334
+ p.title || '\u2014',
335
+ p.lastActionDate ? formatDate(p.lastActionDate) : '\u2014',
336
+ p.engagementScore ? `${p.engagementScore}/100` : '\u2014',
337
+ ]);
338
+ text += table(['Name', 'Title', 'Last Touch', 'Warmth'], rows);
339
+ text += '\n\n### Intro Strategy\n';
340
+ text += `Ask ${connections[0]?.name || 'your contact'} for a warm intro. Reference your shared history.\n`;
341
+ }
342
+
343
+ return md(text);
344
+ } catch (err) {
345
+ return mdError('Network path lookup failed', err.message);
346
+ }
347
+ }
348
+ );
349
+
350
+ // -----------------------------------------------------------------------
351
+ // search_emails (composite version)
352
+ // -----------------------------------------------------------------------
353
+ server.tool(
354
+ 'search_emails_composite',
355
+ 'Full-text search across synced inbox by sender, subject, date range, or company. Returns formatted results with CRM context.',
356
+ {
357
+ query: z.string().optional().describe('Search query (subject, body, sender)'),
358
+ folder: z.string().optional().describe('inbox, sent, drafts, trash'),
359
+ personId: z.string().optional().describe('Filter by person'),
360
+ companyId: z.string().optional().describe('Filter by company'),
361
+ limit: z.number().optional().describe('Max results (default 20)'),
362
+ },
363
+ async (args) => {
364
+ try {
365
+ const emails = await api('GET', '/api/v1/emails', {
366
+ params: {
367
+ search: args.query,
368
+ folder: args.folder,
369
+ personId: args.personId,
370
+ companyId: args.companyId,
371
+ limit: args.limit || 20,
372
+ page: 1,
373
+ },
374
+ });
375
+
376
+ const list = emails?.data || [];
377
+ if (list.length === 0) return md('## Email Search\n\nNo emails found matching your search.\n');
378
+
379
+ let text = `## Email Search \u2014 ${list.length} Results\n\n`;
380
+ const rows = list.map(e => [
381
+ e.from || e.sender || '\u2014',
382
+ e.subject || '\u2014',
383
+ formatDate(e.date || e.createdAt),
384
+ e.folder || '\u2014',
385
+ ]);
386
+ text += table(['From', 'Subject', 'Date', 'Folder'], rows);
387
+
388
+ return md(text);
389
+ } catch (err) {
390
+ return mdError('Email search failed', err.message);
391
+ }
392
+ }
393
+ );
394
+
395
+ // -----------------------------------------------------------------------
396
+ // get_email_thread
397
+ // -----------------------------------------------------------------------
398
+ server.tool(
399
+ 'get_email_thread',
400
+ 'Get full email conversation thread with all messages, timestamps, and CRM context.',
401
+ {
402
+ emailId: z.string().describe('Email ID to get the thread for'),
403
+ },
404
+ async (args) => {
405
+ try {
406
+ const email = await api('GET', `/api/v1/emails/${args.emailId}`);
407
+ const e = email?.data || email || {};
408
+
409
+ let text = `## Email Thread\n\n`;
410
+ text += `**Subject:** ${e.subject || '\u2014'}\n`;
411
+ text += `**From:** ${e.from || e.sender || '\u2014'}\n`;
412
+ text += `**To:** ${e.to || '\u2014'}\n`;
413
+ text += `**Date:** ${formatDate(e.date || e.createdAt)}\n\n`;
414
+ text += '---\n\n';
415
+ text += e.body || e.snippet || 'No content available.\n';
416
+
417
+ // If there's a thread, show all messages
418
+ if (e.thread?.length) {
419
+ e.thread.forEach(msg => {
420
+ text += '\n---\n\n';
421
+ text += `**${msg.from || '\u2014'}** \u2014 ${formatDate(msg.date || msg.createdAt)}\n\n`;
422
+ text += msg.body || msg.snippet || '';
423
+ });
424
+ }
425
+
426
+ return md(text);
427
+ } catch (err) {
428
+ return mdError('Thread retrieval failed', err.message);
429
+ }
430
+ }
431
+ );
432
+ }