@neschadin/sendgrid-mcp 0.0.0-stage → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/MCP_TOOLS.md +457 -0
- package/README.md +175 -2
- package/bin/sendgrid-mcp +2 -0
- package/mcp.json.example +9 -0
- package/package.json +63 -4
- package/src/client.ts +1585 -0
- package/src/config.ts +197 -0
- package/src/http.ts +132 -0
- package/src/index.ts +147 -0
- package/src/logger.ts +43 -0
- package/src/redact.ts +91 -0
- package/src/tool_signal.ts +50 -0
- package/src/tools/account.ts +566 -0
- package/src/tools/classify_error.ts +133 -0
- package/src/tools/console_settings.ts +329 -0
- package/src/tools/delivery_trace.ts +291 -0
- package/src/tools/diagnostics.ts +1557 -0
- package/src/tools/email.ts +486 -0
- package/src/tools/output_schemas.ts +488 -0
- package/src/tools/preflight.ts +768 -0
- package/src/tools/sync.ts +139 -0
- package/src/tools/templates.ts +602 -0
- package/src/tools/tool_utils.ts +341 -0
- package/src/webhook_receiver.ts +467 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { SendGridClient } from '../client';
|
|
4
|
+
import {
|
|
5
|
+
BatchIdOutputSchema,
|
|
6
|
+
jsonReadResult,
|
|
7
|
+
ScheduledSendListOutputSchema,
|
|
8
|
+
ScheduledSendSchema,
|
|
9
|
+
SendTestEmailOutputSchema,
|
|
10
|
+
} from './output_schemas';
|
|
11
|
+
import {
|
|
12
|
+
SendRequestSchema,
|
|
13
|
+
runSendPreflight,
|
|
14
|
+
toMailSendPayload,
|
|
15
|
+
} from './preflight';
|
|
16
|
+
import { ensureSafeToolRegistration, SEND_PREFLIGHT_HINT } from './tool_utils';
|
|
17
|
+
|
|
18
|
+
const ConfirmTokenSchema = z
|
|
19
|
+
.literal('CONFIRM')
|
|
20
|
+
.describe('Required for mutating scheduled-send state');
|
|
21
|
+
|
|
22
|
+
export function registerEmailTools(
|
|
23
|
+
server: McpServer,
|
|
24
|
+
client: SendGridClient,
|
|
25
|
+
defaultFromEmail: string,
|
|
26
|
+
defaultFromName: string,
|
|
27
|
+
) {
|
|
28
|
+
ensureSafeToolRegistration(server);
|
|
29
|
+
const RecipientSchema = z.object({
|
|
30
|
+
email: z.email(),
|
|
31
|
+
name: z.string().optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
server.registerTool(
|
|
35
|
+
'send_email_advanced',
|
|
36
|
+
{
|
|
37
|
+
description:
|
|
38
|
+
`Send email with full /mail/send payload surface (content/template, tracking, asm, scheduling, categories, etc).${SEND_PREFLIGHT_HINT}`,
|
|
39
|
+
inputSchema: z.object({
|
|
40
|
+
request: SendRequestSchema,
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
async ({ request }) => {
|
|
44
|
+
const result = await client.sendMail(toMailSendPayload(request));
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: [
|
|
51
|
+
'Email accepted by SendGrid.',
|
|
52
|
+
`Status code: ${result.statusCode}`,
|
|
53
|
+
`Message ID: ${result.messageId || '(not returned)'}`,
|
|
54
|
+
].join('\n'),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
server.registerTool(
|
|
62
|
+
'send_template_email_advanced',
|
|
63
|
+
{
|
|
64
|
+
description:
|
|
65
|
+
`Send a dynamic-template email with optional cc/bcc, reply-to, asm, categories, custom args, and scheduling.${SEND_PREFLIGHT_HINT}`,
|
|
66
|
+
inputSchema: z.object({
|
|
67
|
+
to: z.array(RecipientSchema).min(1),
|
|
68
|
+
templateId: z.string().describe('Template ID, e.g. d-xxxxxxxxxxxxxxxx'),
|
|
69
|
+
dynamicTemplateData: z.record(z.string(), z.unknown()),
|
|
70
|
+
subject: z.string().optional(),
|
|
71
|
+
cc: z.array(RecipientSchema).optional(),
|
|
72
|
+
bcc: z.array(RecipientSchema).optional(),
|
|
73
|
+
fromEmail: z.email().optional(),
|
|
74
|
+
fromName: z.string().optional(),
|
|
75
|
+
replyTo: RecipientSchema.optional(),
|
|
76
|
+
categories: z.array(z.string()).optional(),
|
|
77
|
+
customArgs: z.record(z.string(), z.string()).optional(),
|
|
78
|
+
sendAt: z.number().int().optional(),
|
|
79
|
+
batchId: z.string().optional(),
|
|
80
|
+
asmGroupId: z.number().int().optional(),
|
|
81
|
+
asmGroupsToDisplay: z.array(z.number().int()).optional(),
|
|
82
|
+
mailSettings: z.record(z.string(), z.unknown()).optional(),
|
|
83
|
+
trackingSettings: z.record(z.string(), z.unknown()).optional(),
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
async ({
|
|
87
|
+
to,
|
|
88
|
+
templateId,
|
|
89
|
+
dynamicTemplateData,
|
|
90
|
+
subject,
|
|
91
|
+
cc,
|
|
92
|
+
bcc,
|
|
93
|
+
fromEmail,
|
|
94
|
+
fromName,
|
|
95
|
+
replyTo,
|
|
96
|
+
categories,
|
|
97
|
+
customArgs,
|
|
98
|
+
sendAt,
|
|
99
|
+
batchId,
|
|
100
|
+
asmGroupId,
|
|
101
|
+
asmGroupsToDisplay,
|
|
102
|
+
mailSettings,
|
|
103
|
+
trackingSettings,
|
|
104
|
+
}) => {
|
|
105
|
+
const result = await client.sendMail({
|
|
106
|
+
personalizations: [
|
|
107
|
+
{
|
|
108
|
+
to,
|
|
109
|
+
cc,
|
|
110
|
+
bcc,
|
|
111
|
+
dynamic_template_data: dynamicTemplateData,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
from: {
|
|
115
|
+
email: fromEmail ?? defaultFromEmail,
|
|
116
|
+
name: fromName ?? defaultFromName,
|
|
117
|
+
},
|
|
118
|
+
reply_to: replyTo,
|
|
119
|
+
subject,
|
|
120
|
+
template_id: templateId,
|
|
121
|
+
categories,
|
|
122
|
+
custom_args: customArgs,
|
|
123
|
+
send_at: sendAt,
|
|
124
|
+
batch_id: batchId,
|
|
125
|
+
asm:
|
|
126
|
+
asmGroupId !== undefined
|
|
127
|
+
? {
|
|
128
|
+
group_id: asmGroupId,
|
|
129
|
+
groups_to_display: asmGroupsToDisplay,
|
|
130
|
+
}
|
|
131
|
+
: undefined,
|
|
132
|
+
mail_settings: mailSettings,
|
|
133
|
+
tracking_settings: trackingSettings,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: 'text',
|
|
140
|
+
text: [
|
|
141
|
+
'Template email accepted by SendGrid.',
|
|
142
|
+
`Template: ${templateId}`,
|
|
143
|
+
`Recipients: ${to.length}`,
|
|
144
|
+
`Status code: ${result.statusCode}`,
|
|
145
|
+
`Message ID: ${result.messageId || '(not returned)'}`,
|
|
146
|
+
batchId ? `Batch ID: ${batchId}` : '',
|
|
147
|
+
]
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.join('\n'),
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
server.registerTool(
|
|
157
|
+
'send_sandbox_email',
|
|
158
|
+
{
|
|
159
|
+
description:
|
|
160
|
+
`Send using mail_settings.sandbox_mode=true for payload/template validation without live recipient delivery.${SEND_PREFLIGHT_HINT}`,
|
|
161
|
+
inputSchema: z.object({
|
|
162
|
+
request: SendRequestSchema,
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
async ({ request }) => {
|
|
166
|
+
const payload = toMailSendPayload(request);
|
|
167
|
+
payload.mail_settings = {
|
|
168
|
+
...payload.mail_settings,
|
|
169
|
+
sandbox_mode: { enable: true },
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const result = await client.sendMail(payload);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: [
|
|
179
|
+
'Sandbox send accepted by SendGrid.',
|
|
180
|
+
'No live delivery attempted.',
|
|
181
|
+
`Status code: ${result.statusCode}`,
|
|
182
|
+
`Message ID: ${result.messageId || '(not returned)'}`,
|
|
183
|
+
].join('\n'),
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
server.registerTool(
|
|
191
|
+
'create_batch_id',
|
|
192
|
+
{
|
|
193
|
+
description:
|
|
194
|
+
'Create a SendGrid batch ID for scheduled sends and later pause/cancel control.',
|
|
195
|
+
inputSchema: z.object({}),
|
|
196
|
+
outputSchema: BatchIdOutputSchema,
|
|
197
|
+
},
|
|
198
|
+
async () => {
|
|
199
|
+
const batch = await client.createBatchId();
|
|
200
|
+
const structured = { batchId: batch.batch_id };
|
|
201
|
+
return {
|
|
202
|
+
structuredContent: structured,
|
|
203
|
+
content: [{ type: 'text', text: `Batch ID: ${batch.batch_id}` }],
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
server.registerTool(
|
|
209
|
+
'schedule_email',
|
|
210
|
+
{
|
|
211
|
+
description:
|
|
212
|
+
`Schedule an email by setting send_at. Creates batch ID automatically when not provided.${SEND_PREFLIGHT_HINT}`,
|
|
213
|
+
inputSchema: z.object({
|
|
214
|
+
request: SendRequestSchema,
|
|
215
|
+
sendAt: z.number().int().describe('Unix timestamp in seconds'),
|
|
216
|
+
batchId: z.string().optional(),
|
|
217
|
+
autoCreateBatchId: z
|
|
218
|
+
.boolean()
|
|
219
|
+
.optional()
|
|
220
|
+
.describe(
|
|
221
|
+
'Default true. If false and batchId missing, sends without batch control',
|
|
222
|
+
),
|
|
223
|
+
}),
|
|
224
|
+
},
|
|
225
|
+
async ({ request, sendAt, batchId, autoCreateBatchId }) => {
|
|
226
|
+
const now = Math.floor(Date.now() / 1000);
|
|
227
|
+
if (sendAt <= now) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`sendAt must be in the future. Received ${sendAt}, now is ${now}.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const maxSendAt = now + 72 * 60 * 60;
|
|
233
|
+
if (sendAt > maxSendAt) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`sendAt must be within 72 hours. Received ${sendAt}, max is ${maxSendAt}.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let effectiveBatchId = batchId;
|
|
240
|
+
if (!effectiveBatchId && (autoCreateBatchId ?? true)) {
|
|
241
|
+
effectiveBatchId = (await client.createBatchId()).batch_id;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const result = await client.sendMail(
|
|
245
|
+
toMailSendPayload({
|
|
246
|
+
...request,
|
|
247
|
+
sendAt,
|
|
248
|
+
batchId: effectiveBatchId,
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
type: 'text',
|
|
256
|
+
text: [
|
|
257
|
+
'Scheduled email accepted by SendGrid.',
|
|
258
|
+
`send_at: ${sendAt}`,
|
|
259
|
+
effectiveBatchId
|
|
260
|
+
? `Batch ID: ${effectiveBatchId}`
|
|
261
|
+
: 'Batch ID: (none)',
|
|
262
|
+
`Status code: ${result.statusCode}`,
|
|
263
|
+
`Message ID: ${result.messageId || '(not returned)'}`,
|
|
264
|
+
].join('\n'),
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
server.registerTool(
|
|
272
|
+
'list_scheduled_sends',
|
|
273
|
+
{
|
|
274
|
+
description:
|
|
275
|
+
'List paused or canceled scheduled-send batches (GET /v3/user/scheduled_sends). A batch that was only scheduled with send_at, and never paused or canceled, is not in this list.',
|
|
276
|
+
inputSchema: z.object({}),
|
|
277
|
+
outputSchema: ScheduledSendListOutputSchema,
|
|
278
|
+
},
|
|
279
|
+
async () => {
|
|
280
|
+
const sends = await client.listScheduledSends();
|
|
281
|
+
return jsonReadResult(
|
|
282
|
+
{ count: sends.length, sends },
|
|
283
|
+
sends.length === 0
|
|
284
|
+
? 'No paused or canceled scheduled sends.'
|
|
285
|
+
: sends
|
|
286
|
+
.map((send) => `- ${send.batch_id} status=${send.status}`)
|
|
287
|
+
.join('\n'),
|
|
288
|
+
);
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
server.registerTool(
|
|
293
|
+
'get_scheduled_send',
|
|
294
|
+
{
|
|
295
|
+
description:
|
|
296
|
+
'Read pause/cancel state for one scheduled-send batch_id. SendGrid answers a missing batch with 200 and an empty array; this tool reports that as not found.',
|
|
297
|
+
inputSchema: z.object({
|
|
298
|
+
batchId: z.string().min(1),
|
|
299
|
+
}),
|
|
300
|
+
outputSchema: ScheduledSendSchema,
|
|
301
|
+
},
|
|
302
|
+
async ({ batchId }) => {
|
|
303
|
+
const send = await client.getScheduledSend(batchId);
|
|
304
|
+
return jsonReadResult(
|
|
305
|
+
send,
|
|
306
|
+
`Scheduled send ${send.batch_id} status=${send.status}`,
|
|
307
|
+
);
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
server.registerTool(
|
|
312
|
+
'pause_scheduled_send',
|
|
313
|
+
{
|
|
314
|
+
description: 'Pause a scheduled send batch by batch ID.',
|
|
315
|
+
inputSchema: z.object({
|
|
316
|
+
confirmToken: ConfirmTokenSchema,
|
|
317
|
+
batchId: z.string(),
|
|
318
|
+
}),
|
|
319
|
+
},
|
|
320
|
+
async ({ batchId }) => {
|
|
321
|
+
const result = await client.upsertScheduledSend(batchId, 'pause');
|
|
322
|
+
return {
|
|
323
|
+
content: [
|
|
324
|
+
{
|
|
325
|
+
type: 'text',
|
|
326
|
+
text: `Scheduled send ${result.batch_id} status: ${result.status}`,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
server.registerTool(
|
|
334
|
+
'resume_scheduled_send',
|
|
335
|
+
{
|
|
336
|
+
description:
|
|
337
|
+
'Resume a previously paused/canceled batch by deleting its scheduled-send status.',
|
|
338
|
+
inputSchema: z.object({
|
|
339
|
+
confirmToken: ConfirmTokenSchema,
|
|
340
|
+
batchId: z.string(),
|
|
341
|
+
}),
|
|
342
|
+
},
|
|
343
|
+
async ({ batchId }) => {
|
|
344
|
+
await client.resumeScheduledSend(batchId);
|
|
345
|
+
return {
|
|
346
|
+
content: [
|
|
347
|
+
{
|
|
348
|
+
type: 'text',
|
|
349
|
+
text: `Scheduled send ${batchId} resumed (status entry removed).`,
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
server.registerTool(
|
|
357
|
+
'cancel_scheduled_send',
|
|
358
|
+
{
|
|
359
|
+
description:
|
|
360
|
+
'Cancel a scheduled send batch. This is not guaranteed close to send time.',
|
|
361
|
+
inputSchema: z.object({
|
|
362
|
+
confirmToken: ConfirmTokenSchema,
|
|
363
|
+
batchId: z.string(),
|
|
364
|
+
}),
|
|
365
|
+
},
|
|
366
|
+
async ({ batchId }) => {
|
|
367
|
+
const result = await client.upsertScheduledSend(batchId, 'cancel');
|
|
368
|
+
return {
|
|
369
|
+
content: [
|
|
370
|
+
{
|
|
371
|
+
type: 'text',
|
|
372
|
+
text: `Scheduled send ${result.batch_id} status: ${result.status}`,
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
server.registerTool(
|
|
380
|
+
'send_test_email',
|
|
381
|
+
{
|
|
382
|
+
description:
|
|
383
|
+
'Send a test email using a template with mock data. Uses mail_settings.sandbox_mode by default (SendGrid accepts but does not deliver). Set liveDelivery=true with confirmToken="CONFIRM" for real delivery.',
|
|
384
|
+
inputSchema: z.object({
|
|
385
|
+
to: z.email().describe('Recipient email for the test'),
|
|
386
|
+
templateId: z.string().describe('Template ID, e.g. d-xxxxxxxxxxxxxxxx'),
|
|
387
|
+
mockData: z
|
|
388
|
+
.record(z.string(), z.unknown())
|
|
389
|
+
.describe(
|
|
390
|
+
'Mock dynamic template data as JSON, e.g. {"customerName":"Test User","siteUrl":"https://example.com"}',
|
|
391
|
+
),
|
|
392
|
+
fromEmail: z.email().optional().describe('Override sender email'),
|
|
393
|
+
fromName: z.string().optional().describe('Override sender name'),
|
|
394
|
+
liveDelivery: z
|
|
395
|
+
.boolean()
|
|
396
|
+
.optional()
|
|
397
|
+
.describe('Default false. If true, sends live mail instead of sandbox mode.'),
|
|
398
|
+
confirmToken: ConfirmTokenSchema.optional(),
|
|
399
|
+
}),
|
|
400
|
+
outputSchema: SendTestEmailOutputSchema,
|
|
401
|
+
},
|
|
402
|
+
async ({
|
|
403
|
+
to,
|
|
404
|
+
templateId,
|
|
405
|
+
mockData,
|
|
406
|
+
fromEmail,
|
|
407
|
+
fromName,
|
|
408
|
+
liveDelivery = false,
|
|
409
|
+
confirmToken,
|
|
410
|
+
}) => {
|
|
411
|
+
if (liveDelivery && confirmToken !== 'CONFIRM') {
|
|
412
|
+
throw new Error(
|
|
413
|
+
'Set confirmToken="CONFIRM" with liveDelivery=true to send a live test email.',
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const request = {
|
|
418
|
+
personalizations: [
|
|
419
|
+
{
|
|
420
|
+
to: [{ email: to }],
|
|
421
|
+
dynamicTemplateData: mockData as Record<string, unknown>,
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
from: {
|
|
425
|
+
email: fromEmail ?? defaultFromEmail,
|
|
426
|
+
name: fromName ?? defaultFromName,
|
|
427
|
+
},
|
|
428
|
+
templateId,
|
|
429
|
+
};
|
|
430
|
+
const report = await runSendPreflight(client, request, {
|
|
431
|
+
checkSenderIdentity: true,
|
|
432
|
+
});
|
|
433
|
+
if (!report.ok) {
|
|
434
|
+
return {
|
|
435
|
+
structuredContent: {
|
|
436
|
+
sent: false,
|
|
437
|
+
sandbox: !liveDelivery,
|
|
438
|
+
report,
|
|
439
|
+
statusCode: null,
|
|
440
|
+
messageId: null,
|
|
441
|
+
},
|
|
442
|
+
content: [
|
|
443
|
+
{
|
|
444
|
+
type: 'text',
|
|
445
|
+
text: 'Test email skipped because preflight found blockers.',
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const payload = toMailSendPayload(request);
|
|
452
|
+
if (!liveDelivery) {
|
|
453
|
+
payload.mail_settings = {
|
|
454
|
+
...payload.mail_settings,
|
|
455
|
+
sandbox_mode: { enable: true },
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const result = await client.sendMail(payload);
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
structuredContent: {
|
|
463
|
+
sent: true,
|
|
464
|
+
sandbox: !liveDelivery,
|
|
465
|
+
report,
|
|
466
|
+
statusCode: result.statusCode,
|
|
467
|
+
messageId: result.messageId || null,
|
|
468
|
+
},
|
|
469
|
+
content: [
|
|
470
|
+
{
|
|
471
|
+
type: 'text',
|
|
472
|
+
text: [
|
|
473
|
+
liveDelivery
|
|
474
|
+
? 'Live test email accepted by SendGrid.'
|
|
475
|
+
: 'Sandbox test email accepted by SendGrid (no live delivery).',
|
|
476
|
+
`To: ${to}`,
|
|
477
|
+
`Template: ${templateId}`,
|
|
478
|
+
`Status: ${result.statusCode}`,
|
|
479
|
+
`Message ID: ${result.messageId || '(not returned)'}`,
|
|
480
|
+
].join('\n'),
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
};
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
}
|