diff --git a/package-lock.json b/package-lock.json
index eda45a8..d6fbecc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
- "name": "sendgrid-mcp-server",
- "version": "0.1.0",
+ "name": "sendgrid-mcp",
+ "version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "sendgrid-mcp-server",
- "version": "0.1.0",
+ "name": "sendgrid-mcp",
+ "version": "1.0.0",
"dependencies": {
"@modelcontextprotocol/sdk": "0.6.0",
"@sendgrid/client": "^8.1.4",
@@ -14,7 +14,7 @@
"@sendgrid/mail": "^8.1.4"
},
"bin": {
- "sendgrid-mcp-server": "build/index.js"
+ "sendgrid-mcp": "build/index.js"
},
"devDependencies": {
"@types/jest": "^29.5.14",
diff --git a/src/services/__tests__/sendgrid.test.ts b/src/services/__tests__/sendgrid.test.ts
index 87d1b84..27676af 100644
--- a/src/services/__tests__/sendgrid.test.ts
+++ b/src/services/__tests__/sendgrid.test.ts
@@ -87,17 +87,165 @@ describe('SendGridService Integration Tests', () => {
});
});
- describe('listTemplates', () => {
- it('should return an array of templates', async () => {
- const templates = await service.listTemplates();
-
- expect(Array.isArray(templates)).toBe(true);
- if (templates.length > 0) {
- expect(templates[0]).toHaveProperty('id');
- expect(templates[0]).toHaveProperty('name');
- expect(templates[0]).toHaveProperty('generation');
+ describe('Template Management', () => {
+ let testTemplateId: string;
+ let testVersionId: string;
+
+ afterAll(async () => {
+ if (testTemplateId) {
+ try {
+ await service.deleteTemplate(testTemplateId);
+ } catch (error) {
+ console.error('Error cleaning up test template:', error);
+ }
}
});
+
+ it('should create a template', async () => {
+ const templateName = `Test Template ${new Date().getTime()}`;
+
+ const template = await service.createTemplate({
+ name: templateName,
+ generation: 'dynamic'
+ });
+
+ testTemplateId = template.id;
+ expect(template).toBeDefined();
+ expect(template.id).toBeDefined();
+ expect(template.name).toBe(templateName);
+ expect(template.generation).toBe('dynamic');
+ });
+
+ it('should update a template name', async () => {
+ const newName = `Updated Template ${new Date().getTime()}`;
+
+ const updatedTemplate = await service.updateTemplate(testTemplateId, {
+ name: newName
+ });
+
+ expect(updatedTemplate).toBeDefined();
+ expect(updatedTemplate.name).toBe(newName);
+ });
+
+ it('should duplicate a template', async () => {
+ const duplicateName = `Duplicate Template ${new Date().getTime()}`;
+
+ const duplicatedTemplate = await service.duplicateTemplate(testTemplateId, {
+ name: duplicateName
+ });
+
+ expect(duplicatedTemplate).toBeDefined();
+ expect(duplicatedTemplate.id).toBeDefined();
+ expect(duplicatedTemplate.id).not.toBe(testTemplateId);
+ expect(duplicatedTemplate.name).toBe(duplicateName);
+
+ // Clean up duplicated template
+ await service.deleteTemplate(duplicatedTemplate.id);
+ });
+
+ it('should list templates with pagination', async () => {
+ const templateResponse = await service.listTemplates({
+ generations: 'dynamic',
+ page_size: 10
+ });
+
+ expect(templateResponse).toBeDefined();
+ expect(Array.isArray(templateResponse.result)).toBe(true);
+ expect(templateResponse._metadata).toBeDefined();
+ });
+
+ it('should create a template version', async () => {
+ const versionName = `Test Version ${new Date().getTime()}`;
+
+ const version = await service.createTemplateVersion(testTemplateId, {
+ name: versionName,
+ subject: 'Test Subject {{name}}',
+ html_content: '
Hello {{name}}
',
+ plain_content: 'Hello {{name}}',
+ active: 1,
+ generate_plain_content: false
+ });
+
+ testVersionId = version.id;
+ expect(version).toBeDefined();
+ expect(version.id).toBeDefined();
+ expect(version.name).toBe(versionName);
+ expect(version.subject).toBe('Test Subject {{name}}');
+ expect(version.active).toBe(1);
+ });
+
+ it('should get a template version', async () => {
+ const version = await service.getTemplateVersion(testTemplateId, testVersionId);
+
+ expect(version).toBeDefined();
+ expect(version.id).toBe(testVersionId);
+ expect(version.template_id).toBe(testTemplateId);
+ });
+
+ it('should update a template version', async () => {
+ const updatedSubject = 'Updated Subject {{name}}';
+
+ const updatedVersion = await service.updateTemplateVersion(testTemplateId, testVersionId, {
+ name: `Updated Version ${new Date().getTime()}`,
+ subject: updatedSubject,
+ html_content: 'Updated Hello {{name}}
',
+ plain_content: 'Updated Hello {{name}}'
+ });
+
+ expect(updatedVersion).toBeDefined();
+ expect(updatedVersion.subject).toBe(updatedSubject);
+ });
+
+ it('should activate a template version', async () => {
+ // Create a second version
+ const newVersion = await service.createTemplateVersion(testTemplateId, {
+ name: `Version 2 ${new Date().getTime()}`,
+ subject: 'Version 2 Subject',
+ html_content: 'Version 2
',
+ plain_content: 'Version 2',
+ active: 0
+ });
+
+ // Activate the new version
+ const activatedVersion = await service.activateTemplateVersion(testTemplateId, newVersion.id);
+
+ expect(activatedVersion).toBeDefined();
+ expect(activatedVersion.active).toBe(1);
+
+ // Clean up
+ await service.deleteTemplateVersion(testTemplateId, newVersion.id);
+ });
+
+ it('should delete a template version', async () => {
+ // Create a version to delete
+ const versionToDelete = await service.createTemplateVersion(testTemplateId, {
+ name: `Delete Me ${new Date().getTime()}`,
+ subject: 'Delete Me',
+ html_content: 'Delete Me
',
+ plain_content: 'Delete Me'
+ });
+
+ // Delete the version
+ await service.deleteTemplateVersion(testTemplateId, versionToDelete.id);
+
+ // Verify it's deleted by trying to get it
+ try {
+ await service.getTemplateVersion(testTemplateId, versionToDelete.id);
+ throw new Error('Version was not deleted');
+ } catch (error: any) {
+ // Expect 404 error since version should be deleted
+ expect(error.code).toBe(404);
+ }
+ });
+
+ it('should get a template by ID', async () => {
+ const template = await service.getTemplate(testTemplateId);
+
+ expect(template).toBeDefined();
+ expect(template.id).toBe(testTemplateId);
+ expect(template.versions).toBeDefined();
+ expect(Array.isArray(template.versions)).toBe(true);
+ });
});
describe('getStats', () => {
diff --git a/src/services/sendgrid.ts b/src/services/sendgrid.ts
index faa5841..f95198e 100644
--- a/src/services/sendgrid.ts
+++ b/src/services/sendgrid.ts
@@ -1,6 +1,6 @@
import { Client } from '@sendgrid/client';
import sgMail from '@sendgrid/mail';
-import { SendGridContact, SendGridList, SendGridTemplate, SendGridStats, SendGridSingleSend } from '../types/index.js';
+import { SendGridContact, SendGridList, SendGridTemplate, SendGridStats, SendGridSingleSend, SendGridListTemplatesResponse, SendGridTemplateVersionCreateParams, SendGridTemplateVersion } from '../types/index.js';
export class SendGridService {
private client: Client;
@@ -155,61 +155,67 @@ export class SendGridService {
// Template Management
async createTemplate(params: {
name: string;
- html_content: string;
- plain_content: string;
- subject: string;
+ generation?: 'legacy' | 'dynamic';
}): Promise {
const [response] = await this.client.request({
method: 'POST',
url: '/v3/templates',
body: {
name: params.name,
- generation: 'dynamic'
+ generation: params.generation || 'dynamic'
}
});
- const templateId = (response.body as { id: string }).id;
-
- // Create the first version of the template
- const [versionResponse] = await this.client.request({
- method: 'POST',
- url: `/v3/templates/${templateId}/versions`,
- body: {
- template_id: templateId,
- name: `${params.name} v1`,
- subject: params.subject,
- html_content: params.html_content,
- plain_content: params.plain_content,
- active: 1
- }
+ return response.body as SendGridTemplate;
+ }
+
+ async updateTemplate(templateId: string, params: {
+ name: string;
+ }): Promise {
+ const [response] = await this.client.request({
+ method: 'PATCH',
+ url: `/v3/templates/${templateId}`,
+ body: params
});
+ return response.body as SendGridTemplate;
+ }
- return {
- id: templateId,
- name: params.name,
- generation: 'dynamic',
- updated_at: new Date().toISOString(),
- versions: [{
- id: (versionResponse.body as { id: string }).id,
- template_id: templateId,
- active: 1,
- name: `${params.name} v1`,
- html_content: params.html_content,
- plain_content: params.plain_content,
- subject: params.subject
- }]
- };
+ async duplicateTemplate(templateId: string, params: {
+ name?: string;
+ }): Promise {
+ const [response] = await this.client.request({
+ method: 'POST',
+ url: `/v3/templates/${templateId}`,
+ body: params
+ });
+ return response.body as SendGridTemplate;
}
- async listTemplates(): Promise {
+ async listTemplates(params?: {
+ generations?: 'legacy' | 'dynamic' | 'legacy,dynamic';
+ page_size?: number;
+ page_token?: string;
+ }): Promise {
const [response] = await this.client.request({
method: 'GET',
url: '/v3/templates',
qs: {
- generations: 'dynamic'
+ generations: params?.generations || 'legacy',
+ page_size: params?.page_size,
+ page_token: params?.page_token
}
});
- return ((response.body as { templates: SendGridTemplate[] }).templates || []);
+
+ // The SendGrid API returns templates directly as an array when using the v3/templates endpoint
+ // We need to wrap it in the expected structure
+ if (Array.isArray(response.body)) {
+ return {
+ result: response.body,
+ _metadata: response.headers['x-metadata'] ? JSON.parse(response.headers['x-metadata']) : undefined
+ };
+ }
+
+ return response.body as SendGridListTemplatesResponse;
}
async getTemplate(templateId: string): Promise {
@@ -227,6 +233,48 @@ export class SendGridService {
});
}
+ // Template Version Management
+ async createTemplateVersion(templateId: string, params: SendGridTemplateVersionCreateParams): Promise {
+ const [response] = await this.client.request({
+ method: 'POST',
+ url: `/v3/templates/${templateId}/versions`,
+ body: params
+ });
+ return response.body as SendGridTemplateVersion;
+ }
+
+ async getTemplateVersion(templateId: string, versionId: string): Promise {
+ const [response] = await this.client.request({
+ method: 'GET',
+ url: `/v3/templates/${templateId}/versions/${versionId}`
+ });
+ return response.body as SendGridTemplateVersion;
+ }
+
+ async updateTemplateVersion(templateId: string, versionId: string, params: SendGridTemplateVersionCreateParams): Promise {
+ const [response] = await this.client.request({
+ method: 'PATCH',
+ url: `/v3/templates/${templateId}/versions/${versionId}`,
+ body: params
+ });
+ return response.body as SendGridTemplateVersion;
+ }
+
+ async deleteTemplateVersion(templateId: string, versionId: string): Promise {
+ await this.client.request({
+ method: 'DELETE',
+ url: `/v3/templates/${templateId}/versions/${versionId}`
+ });
+ }
+
+ async activateTemplateVersion(templateId: string, versionId: string): Promise {
+ const [response] = await this.client.request({
+ method: 'POST',
+ url: `/v3/templates/${templateId}/versions/${versionId}/activate`
+ });
+ return response.body as SendGridTemplateVersion;
+ }
+
// Email Validation
async validateEmail(email: string) {
const [response] = await this.client.request({
diff --git a/src/tools/index.ts b/src/tools/index.ts
index 511d707..513f5e9 100644
--- a/src/tools/index.ts
+++ b/src/tools/index.ts
@@ -137,20 +137,26 @@ export const getToolDefinitions = (service: SendGridService) => [
type: 'string',
description: 'Name of the template'
},
+ generation: {
+ type: 'string',
+ enum: ['legacy', 'dynamic'],
+ description: 'Template generation type (defaults to dynamic)'
+ },
+ // Legacy parameters for backward compatibility
subject: {
type: 'string',
- description: 'Default subject line for the template'
+ description: '[DEPRECATED] Use create_template_version instead. Subject line for the template'
},
html_content: {
type: 'string',
- description: 'HTML content of the template'
+ description: '[DEPRECATED] Use create_template_version instead. HTML content of the template'
},
plain_content: {
type: 'string',
- description: 'Plain text content of the template'
+ description: '[DEPRECATED] Use create_template_version instead. Plain text content of the template'
}
},
- required: ['name', 'subject', 'html_content', 'plain_content']
+ required: ['name']
}
},
{
@@ -223,10 +229,214 @@ export const getToolDefinitions = (service: SendGridService) => [
description: 'List all email templates in your SendGrid account',
inputSchema: {
type: 'object',
- properties: {},
+ properties: {
+ generations: {
+ type: 'string',
+ enum: ['legacy', 'dynamic', 'legacy,dynamic'],
+ description: 'Which generations of templates to return (defaults to legacy)'
+ },
+ page_size: {
+ type: 'number',
+ description: 'Number of templates per page (1-200)'
+ },
+ page_token: {
+ type: 'string',
+ description: 'Token for a specific page of results'
+ }
+ },
required: []
}
},
+ {
+ name: 'update_template',
+ description: 'Update the name of an existing template',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ template_id: {
+ type: 'string',
+ description: 'ID of the template to update'
+ },
+ name: {
+ type: 'string',
+ description: 'New name for the template'
+ }
+ },
+ required: ['template_id', 'name']
+ }
+ },
+ {
+ name: 'duplicate_template',
+ description: 'Create a copy of an existing template',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ template_id: {
+ type: 'string',
+ description: 'ID of the template to duplicate'
+ },
+ name: {
+ type: 'string',
+ description: 'Name for the new template copy'
+ }
+ },
+ required: ['template_id']
+ }
+ },
+ {
+ name: 'create_template_version',
+ description: 'Create a new version for a template',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ template_id: {
+ type: 'string',
+ description: 'ID of the template'
+ },
+ name: {
+ type: 'string',
+ description: 'Name of the version'
+ },
+ subject: {
+ type: 'string',
+ description: 'Email subject line'
+ },
+ html_content: {
+ type: 'string',
+ description: 'HTML content (max 1MB)'
+ },
+ plain_content: {
+ type: 'string',
+ description: 'Plain text content (optional, generated from HTML if omitted)'
+ },
+ generate_plain_content: {
+ type: 'boolean',
+ description: 'Auto-generate plain content from HTML (default: true)'
+ },
+ active: {
+ type: 'number',
+ enum: [0, 1],
+ description: 'Set as active version (1) or inactive (0)'
+ },
+ editor: {
+ type: 'string',
+ enum: ['code', 'design'],
+ description: 'Editor type used'
+ },
+ test_data: {
+ type: 'string',
+ description: 'Mock JSON data for dynamic templates'
+ }
+ },
+ required: ['template_id', 'name', 'subject', 'html_content']
+ }
+ },
+ {
+ name: 'get_template_version',
+ description: 'Get a specific version of a template',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ template_id: {
+ type: 'string',
+ description: 'ID of the template'
+ },
+ version_id: {
+ type: 'string',
+ description: 'ID of the version'
+ }
+ },
+ required: ['template_id', 'version_id']
+ }
+ },
+ {
+ name: 'update_template_version',
+ description: 'Update an existing template version',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ template_id: {
+ type: 'string',
+ description: 'ID of the template'
+ },
+ version_id: {
+ type: 'string',
+ description: 'ID of the version to update'
+ },
+ name: {
+ type: 'string',
+ description: 'Name of the version'
+ },
+ subject: {
+ type: 'string',
+ description: 'Email subject line'
+ },
+ html_content: {
+ type: 'string',
+ description: 'HTML content (max 1MB)'
+ },
+ plain_content: {
+ type: 'string',
+ description: 'Plain text content'
+ },
+ generate_plain_content: {
+ type: 'boolean',
+ description: 'Auto-generate plain content from HTML'
+ },
+ active: {
+ type: 'number',
+ enum: [0, 1],
+ description: 'Set as active version (1) or inactive (0)'
+ },
+ editor: {
+ type: 'string',
+ enum: ['code', 'design'],
+ description: 'Editor type used'
+ },
+ test_data: {
+ type: 'string',
+ description: 'Mock JSON data for dynamic templates'
+ }
+ },
+ required: ['template_id', 'version_id', 'name', 'subject', 'html_content']
+ }
+ },
+ {
+ name: 'delete_template_version',
+ description: 'Delete a specific template version',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ template_id: {
+ type: 'string',
+ description: 'ID of the template'
+ },
+ version_id: {
+ type: 'string',
+ description: 'ID of the version to delete'
+ }
+ },
+ required: ['template_id', 'version_id']
+ }
+ },
+ {
+ name: 'activate_template_version',
+ description: 'Activate a specific template version',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ template_id: {
+ type: 'string',
+ description: 'ID of the template'
+ },
+ version_id: {
+ type: 'string',
+ description: 'ID of the version to activate'
+ }
+ },
+ required: ['template_id', 'version_id']
+ }
+ },
{
name: 'delete_list',
description: 'Delete a contact list from SendGrid',
@@ -409,8 +619,37 @@ export const handleToolCall = async (service: SendGridService, name: string, arg
return { content: [{ type: 'text', text: `Added ${args.emails.length} contacts to list ${args.list_id}` }] };
case 'create_template':
- const template = await service.createTemplate(args);
- return { content: [{ type: 'text', text: `Template "${args.name}" created with ID: ${template.id}` }] };
+ // Check if using legacy parameters
+ if (args.subject && args.html_content && args.plain_content) {
+ // Legacy behavior: create template and first version
+ const template = await service.createTemplate({
+ name: args.name,
+ generation: args.generation || 'dynamic'
+ });
+
+ try {
+ // Create the first version with the provided content
+ await service.createTemplateVersion(template.id, {
+ name: `${args.name} v1`,
+ subject: args.subject,
+ html_content: args.html_content,
+ plain_content: args.plain_content,
+ active: 1
+ });
+
+ return { content: [{ type: 'text', text: `Template "${args.name}" created with ID: ${template.id} (legacy mode - created with initial version)` }] };
+ } catch (error) {
+ // If version creation fails, still return the template ID
+ return { content: [{ type: 'text', text: `Template "${args.name}" created with ID: ${template.id} (warning: initial version creation failed)` }] };
+ }
+ } else {
+ // New behavior: just create the template
+ const template = await service.createTemplate({
+ name: args.name,
+ generation: args.generation
+ });
+ return { content: [{ type: 'text', text: `Template "${args.name}" created with ID: ${template.id}` }] };
+ }
case 'get_template':
const retrievedTemplate = await service.getTemplate(args.template_id);
@@ -429,17 +668,24 @@ export const handleToolCall = async (service: SendGridService, name: string, arg
return { content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }] };
case 'list_templates':
- const templates = await service.listTemplates();
+ const templateResponse = await service.listTemplates(args);
+
+ // Handle case where result might be undefined or response structure is different
+ const templates = templateResponse?.result || [];
+
return {
content: [{
type: 'text',
- text: JSON.stringify(templates.map(t => ({
- id: t.id,
- name: t.name,
- generation: t.generation,
- updated_at: t.updated_at,
- versions: t.versions.length
- })), null, 2)
+ text: JSON.stringify({
+ templates: Array.isArray(templates) ? templates.map(t => ({
+ id: t.id,
+ name: t.name,
+ generation: t.generation,
+ updated_at: t.updated_at,
+ versions: t.versions ? t.versions.length : 0
+ })) : [],
+ metadata: templateResponse?._metadata
+ }, null, 2)
}]
};
@@ -554,6 +800,36 @@ export const handleToolCall = async (service: SendGridService, name: string, arg
await service.removeContactsFromList(args.list_id, args.emails);
return { content: [{ type: 'text', text: `Removed ${args.emails.length} contacts from list ${args.list_id}` }] };
+ case 'update_template':
+ const updatedTemplate = await service.updateTemplate(args.template_id, { name: args.name });
+ return { content: [{ type: 'text', text: `Template ${args.template_id} renamed to "${args.name}"` }] };
+
+ case 'duplicate_template':
+ const duplicatedTemplate = await service.duplicateTemplate(args.template_id, args.name ? { name: args.name } : {});
+ return { content: [{ type: 'text', text: `Template duplicated with new ID: ${duplicatedTemplate.id}` }] };
+
+ case 'create_template_version':
+ const { template_id, ...versionParams } = args;
+ const newVersion = await service.createTemplateVersion(template_id, versionParams);
+ return { content: [{ type: 'text', text: `Template version created with ID: ${newVersion.id}` }] };
+
+ case 'get_template_version':
+ const version = await service.getTemplateVersion(args.template_id, args.version_id);
+ return { content: [{ type: 'text', text: JSON.stringify(version, null, 2) }] };
+
+ case 'update_template_version':
+ const { template_id: tid, version_id, ...updateParams } = args;
+ const updatedVersion = await service.updateTemplateVersion(tid, version_id, updateParams);
+ return { content: [{ type: 'text', text: `Template version ${version_id} updated successfully` }] };
+
+ case 'delete_template_version':
+ await service.deleteTemplateVersion(args.template_id, args.version_id);
+ return { content: [{ type: 'text', text: `Template version ${args.version_id} deleted successfully` }] };
+
+ case 'activate_template_version':
+ const activatedVersion = await service.activateTemplateVersion(args.template_id, args.version_id);
+ return { content: [{ type: 'text', text: `Template version ${args.version_id} activated successfully` }] };
+
default:
throw new Error(`Unknown tool: ${name}`);
}
diff --git a/src/types/index.ts b/src/types/index.ts
index b55cd77..e539acf 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -17,9 +17,12 @@ export interface SendGridList {
export interface SendGridTemplate {
id: string;
name: string;
- generation: string;
+ generation: 'legacy' | 'dynamic';
updated_at: string;
- versions: SendGridTemplateVersion[];
+ versions?: SendGridTemplateVersion[];
+ warning?: {
+ message: string;
+ };
}
export interface SendGridTemplateVersion {
@@ -30,6 +33,11 @@ export interface SendGridTemplateVersion {
html_content: string;
plain_content: string;
subject: string;
+ generate_plain_content?: boolean;
+ editor?: 'code' | 'design';
+ test_data?: string;
+ updated_at?: string;
+ thumbnail_url?: string;
}
export interface SendGridStats extends Array<{
@@ -74,3 +82,29 @@ export interface SendGridSingleSend {
custom_unsubscribe_url?: string;
};
}
+
+export interface SendGridListTemplatesResponse {
+ result: SendGridTemplate[];
+ _metadata?: {
+ self?: string;
+ next?: string;
+ prev?: string;
+ count?: number;
+ };
+}
+
+export interface SendGridTemplateCreateParams {
+ name: string;
+ generation?: 'legacy' | 'dynamic';
+}
+
+export interface SendGridTemplateVersionCreateParams {
+ active?: 0 | 1;
+ name: string;
+ html_content: string;
+ plain_content?: string;
+ generate_plain_content?: boolean;
+ subject: string;
+ editor?: 'code' | 'design';
+ test_data?: string;
+}