Guide · MCP SendGrid Integration
MCP Server SendGrid — transactional email, bounce management, and /health via GET /scopes
SendGrid is the dominant transactional email platform for agent-driven notifications, receipts, and alerts. This guide covers building TypeScript MCP tools for the SendGrid Web API v3: API key Bearer authentication, sending transactional email with Dynamic Template support and confirm guards, managing suppression lists (bounces, blocks, unsubscribes) for deliverability maintenance, removing bounces to reactivate previously failed recipients, and wiring a /health/sendgrid endpoint that calls GET /v3/scopes to validate API key permissions before AliveMCP catches authentication failures and scope erosion.
TL;DR
SendGrid v3 API uses Authorization: Bearer SG.xxxx with API keys — never expose the full account API key, create restricted keys with only the scopes your MCP tool needs. The from address must be a verified Sender Identity or a domain with verified DNS records. Emails cannot be recalled after the API accepts them — add a confirm: z.literal(true) guard. Rate limit is 600 requests/minute per API key on the default tier, tracked via X-RateLimit-* response headers. Health: GET /v3/scopes lists the API key's granted permissions — it validates the key is active, not revoked, and shows exactly which operations it can perform.
SDK setup and authentication
SendGrid provides the @sendgrid/mail package for mail sending and @sendgrid/client for other API operations. Using both gives full coverage. Set API key via the setter rather than passing it per-call.
import sgMail from '@sendgrid/mail';
import sgClient from '@sendgrid/client';
import { z } from 'zod';
// API keys start with SG.
// Always use restricted API keys scoped to minimum required permissions:
// - mail.send — to send email
// - suppression.read — to list bounces/blocks
// - suppression.delete — to remove bounces
// Never use the full-access key in production MCP servers.
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
sgClient.setApiKey(process.env.SENDGRID_API_KEY!);
// For raw HTTP, all v3 endpoints are:
// https://api.sendgrid.com/v3/{resource}
// Authorization: Bearer SG.your-api-key-here
// Content-Type: application/json
// Rate limit headers on every response:
// X-RateLimit-Limit: 600 (requests per minute)
// X-RateLimit-Remaining: 599 (remaining this minute)
// X-RateLimit-Reset: 1720534860 (Unix timestamp when limit resets)
| API key scope | What it allows | Needed for |
|---|---|---|
mail.send |
Send transactional email | send_email tool |
suppression.read |
List bounces, blocks, unsubscribes | list_bounces, list_blocks tools |
suppression.delete |
Remove email from suppression lists | remove_bounce tool |
templates.read |
List and fetch Dynamic Templates | list_templates tool |
sender_verification.read |
List verified sender identities | Verifying from address at startup |
Sending email with Dynamic Template support and confirm guards
SendGrid Dynamic Templates allow structured email with Handlebars variable substitution, separating content from code. The template_id parameter replaces subject, HTML, and plain text when provided — the API ignores subject and content fields when a template is set. The from address must match a verified Sender Identity or a domain with valid DNS records including DKIM and SPF.
server.tool('send_email', {
to: z.union([
z.string().email(),
z.array(z.object({
email: z.string().email(),
name: z.string().optional()
}))
]).describe('Recipient email or array of recipients.'),
from: z.object({
email: z.string().email().describe('Must be a verified Sender Identity.'),
name: z.string().optional()
}),
subject: z.string().optional().describe('Email subject. Ignored if template_id is set.'),
text_content: z.string().optional().describe('Plain text body. Ignored if template_id is set.'),
html_content: z.string().optional().describe('HTML body. Ignored if template_id is set.'),
template_id: z.string().optional().describe('SendGrid Dynamic Template ID (d-xxxx). Overrides subject and content.'),
template_data: z.record(z.unknown()).optional().describe('Handlebars variables for Dynamic Template.'),
confirm: z.literal(true).describe('Emails cannot be recalled after acceptance. Pass true to confirm.')
}, async ({ to, from, subject, text_content, html_content, template_id, template_data }) => {
const recipients = Array.isArray(to)
? to
: [{ email: to as string }];
const msg: sgMail.MailDataRequired = {
to: recipients,
from,
...(template_id
? {
templateId: template_id,
dynamicTemplateData: template_data ?? {}
}
: {
subject: subject!,
content: [
...(text_content ? [{ type: 'text/plain', value: text_content }] : []),
...(html_content ? [{ type: 'text/html', value: html_content }] : [])
]
}
)
};
const [response] = await sgMail.send(msg);
// HTTP 202 Accepted — email queued, not yet delivered
return {
content: [{
type: 'text',
text: JSON.stringify({
accepted: response.statusCode === 202,
status_code: response.statusCode,
// Message-Id header returned on successful queue
message_id: response.headers['x-message-id']
}, null, 2)
}]
};
});
Managing suppression lists for deliverability
Suppression lists prevent SendGrid from re-sending to addresses that have previously bounced, unsubscribed, or been blocked. When an email bounces, SendGrid adds it to the bounce list and will silently skip future sends to that address — important for diagnosing why certain recipients stop receiving messages. The list_bounces and remove_bounce tools let agents surface and remediate deliverability issues.
server.tool('list_bounces', {
start_time: z.number().int().optional().describe('Unix timestamp. Filter bounces after this time.'),
end_time: z.number().int().optional().describe('Unix timestamp. Filter bounces before this time.'),
limit: z.number().int().min(1).max(500).default(100),
offset: z.number().int().min(0).default(0)
}, async ({ start_time, end_time, limit, offset }) => {
const query = new URLSearchParams({ limit: String(limit), offset: String(offset) });
if (start_time) query.set('start_time', String(start_time));
if (end_time) query.set('end_time', String(end_time));
const [response, body] = await sgClient.request({
method: 'GET',
url: `/v3/suppression/bounces?${query}`
});
const bounces = (body as Array<{
created: number;
email: string;
reason: string;
status: string;
}>).map(b => ({
email: b.email,
reason: b.reason,
status: b.status, // e.g. '5.1.1' = invalid address, '5.5.0' = spam rejection
created_at: new Date(b.created * 1000).toISOString()
}));
return {
content: [{
type: 'text',
text: JSON.stringify({ count: bounces.length, bounces }, null, 2)
}]
};
});
server.tool('list_blocks', {
limit: z.number().int().min(1).max(500).default(100),
offset: z.number().int().min(0).default(0)
}, async ({ limit, offset }) => {
const [, body] = await sgClient.request({
method: 'GET',
url: `/v3/suppression/blocks?limit=${limit}&offset=${offset}`
});
return {
content: [{
type: 'text',
text: JSON.stringify(body, null, 2)
}]
};
});
Removing bounces to reactivate recipients
Once a bounce is cleared, SendGrid will attempt delivery to that address again. This is appropriate when the bounce was transient (e.g. a full mailbox that has since been emptied) or when the user provides a new valid email address. The remove bounce operation is destructive — misuse will cause emails to be sent to invalid addresses and damage your sender reputation.
server.tool('remove_bounce', {
email: z.string().email().describe('Email address to remove from the bounce list.'),
confirm: z.literal(true).describe('Removing a bounce reactivates future sends to this address. Confirm.')
}, async ({ email }) => {
const [response] = await sgClient.request({
method: 'DELETE',
url: `/v3/suppression/bounces/${encodeURIComponent(email)}`
});
// DELETE returns 204 No Content on success
return {
content: [{
type: 'text',
text: response.statusCode === 204
? `Bounce removed. ${email} will receive future emails.`
: `Unexpected status: ${response.statusCode}`
}]
};
});
| Suppression list | Who ends up here | Remove to reactivate? |
|---|---|---|
| Bounces | Invalid or unreachable addresses | Yes, if address is now valid |
| Blocks | Rejected by recipient's ISP | Yes, if ISP block lifted |
| Spam Reports | Recipient marked as spam | Only with explicit consent |
| Unsubscribes | Recipient clicked unsubscribe | Never without explicit re-opt-in |
Wiring /health/sendgrid via GET /scopes
GET /v3/scopes is the ideal SendGrid health probe — it validates that the API key is active, not revoked, and returns exactly which permissions the key has. This catches scope-reduced keys (e.g. when a key is edited to remove permissions) as well as fully revoked keys. Unlike most API health probes that just check connectivity, /scopes verifies that the key can actually perform the operations you need.
app.get('/health/sendgrid', async (req, res) => {
try {
const [response, body] = await sgClient.request({
method: 'GET',
url: '/v3/scopes'
});
const scopes = (body as { scopes: string[] }).scopes;
const requiredScopes = ['mail.send', 'suppression.read'];
const missingScopes = requiredScopes.filter(s => !scopes.includes(s));
if (missingScopes.length > 0) {
return res.status(503).json({
status: 'degraded',
error: `API key missing required scopes: ${missingScopes.join(', ')}`,
granted_scopes: scopes
});
}
return res.json({
status: 'healthy',
granted_scopes: scopes,
has_mail_send: scopes.includes('mail.send'),
has_suppression_read: scopes.includes('suppression.read'),
has_suppression_delete: scopes.includes('suppression.delete')
});
} catch (err: any) {
// SendGrid errors: err.response.status (HTTP) and err.response.body.errors[]
return res.status(503).json({
status: 'unhealthy',
http_status: err.response?.statusCode ?? 503,
errors: err.response?.body?.errors ?? [{ message: err.message }]
});
}
});
| HTTP status | Meaning | Remediation |
|---|---|---|
| 200 | Key valid — body contains granted scopes array | Check that required scopes are present |
| 401 | API key is invalid or revoked | Generate a new restricted API key in SendGrid dashboard |
| 403 | Key lacks permission to read scopes (rare) | Use a key with at least one real scope |
| 429 | Rate limit hit | Respect X-RateLimit-Reset — don't poll health endpoint aggressively |
Frequently asked questions
Why does SendGrid return HTTP 202 Accepted rather than 200 OK for sent emails?
The 202 Accepted response means SendGrid has queued the email for delivery — it does not mean the email has been delivered or even attempted. Email delivery is asynchronous: the API accepts the message, validates basic formatting, and then processes it through the sending pipeline which handles throttling, retry, bounce detection, and delivery. To track actual delivery status, configure an Event Webhook in SendGrid settings that POSTs event notifications to your endpoint as the email progresses through states: processed, deferred, delivered, bounce, spam report, etc.
What's a Sender Identity and why does the from address need to be verified?
A Sender Identity in SendGrid is either a Single Sender (a specific email address verified via a confirmation link) or a Domain Authentication (a full domain with DNS records for DKIM and SPF). If you send from an unverified address, SendGrid returns a 403 error with code 553. Domain Authentication is strongly preferred for production: it allows sending from any address at your domain, improves deliverability by proving ownership, and enables DKIM signing. Single Sender verification is simpler to set up but only allows one specific address.
How do Dynamic Templates interact with the subject and content fields?
When you specify a templateId, SendGrid completely ignores the subject and content` fields in your API request — the template's defined subject and HTML/text body are used instead. Variables in the template (using Handlebars syntax like {{first_name}}) are substituted with values from dynamicTemplateData. If a variable referenced in the template is missing from dynamicTemplateData, it renders as an empty string — no error is thrown. Always test template rendering in the SendGrid UI's preview before sending to production recipients.
What's the rate limit for the SendGrid API and how do I respect it?
The default rate limit is 600 requests per minute per API key, tracked via X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response. The reset timestamp is Unix epoch seconds. When you hit the limit, you receive HTTP 429. The @sendgrid/client package does not automatically retry on 429 — implement exponential backoff using the X-RateLimit-Reset value. For high-volume sending (newsletters, batch notifications), use the Batch Email feature which handles rate limiting and queuing on SendGrid's side rather than making individual API calls per email.
Further reading
- MCP Server Twilio — SMS with E.164 validation, call listing, and number lookup
- MCP Server PagerDuty — create incidents, manage on-call schedules, and health probes via /users/me
- MCP Server Slack — Block Kit messages, cursor pagination, and health probes via auth.test
- MCP Server Health Check — wiring /health endpoints for uptime monitoring
- MCP Server Webhooks — sending structured payloads with retry logic