@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.
- package/README.md +548 -0
- package/access/auth.js +289 -0
- package/access/oauth.js +1059 -0
- package/access/resource-metadata.js +167 -0
- package/access/tiers.js +422 -0
- package/analytics.js +634 -0
- package/api-bridge.js +499 -0
- package/governance/money.js +141 -0
- package/output-formatter.js +589 -0
- package/package.json +68 -0
- package/resources.js +246 -0
- package/security.js +690 -0
- package/server.js +2139 -0
- package/server.json +55 -0
- package/skills/backlog-triage/SKILL.md +115 -0
- package/skills/board-review/SKILL.md +96 -0
- package/skills/incident-to-card/SKILL.md +126 -0
- package/skills/log-outreach.md +62 -0
- package/skills/ship-the-card/SKILL.md +155 -0
- package/tool-annotations.js +269 -0
- package/tools/billing.js +149 -0
- package/tools/email-tools.js +652 -0
- package/tools/enterprise-tools.js +651 -0
- package/tools/free-search.js +160 -0
- package/tools/memory.js +440 -0
- package/tools/morning-brief.js +551 -0
- package/tools/paper-tools.js +563 -0
- package/tools/scheduling.js +322 -0
- package/tools/work-board-tools.js +758 -0
- package/toolsets/communications.js +276 -0
- package/toolsets/crm.js +495 -0
- package/toolsets/extensibility.js +1131 -0
- package/toolsets/infrastructure.js +757 -0
- package/toolsets/intelligence.js +232 -0
- package/toolsets/knowledge.js +154 -0
- package/toolsets/matrix.js +217 -0
- package/toolsets/outreach.js +432 -0
- package/toolsets/prospecting.js +314 -0
- package/toolsets/revenue/always-loaded.js +341 -0
- package/toolsets/revenue/sloan-tools.js +81 -0
- 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
|
+
}
|