Skip to main content
Glama

Mailgun MCP Server

Official
by mailgun
mailgun-mcp.js19.1 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import https from "node:https"; import { URL } from "node:url"; import yaml from "js-yaml"; import fs from "node:fs"; import * as path from 'path'; import { fileURLToPath } from 'url'; // Resolve directory path when using ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Initialize Model Context Protocol server export const server = new McpServer({ name: "mailgun", version: "1.0.0", }); // Mailgun API configuration const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY; const MAILGUN_API_HOSTNAME = "api.mailgun.net"; const OPENAPI_YAML = path.resolve(__dirname, 'openapi-final.yaml'); // Define Mailgun API endpoints supported by this integration const endpoints = [ "POST /v3/{domain_name}/messages", "GET /v4/domains", "GET /v4/domains/{name}", "GET /v3/domains/{name}/sending_queues", "GET /v5/accounts/subaccounts/ip_pools", "GET /v3/ips", "GET /v3/ips/{ip}", "GET /v3/ips/{ip}/domains", "GET /v3/ip_pools", "GET /v3/ip_pools/{pool_id}", "GET /v3/ip_pools/{pool_id}/domains", "GET /v3/{domain_name}/events", "GET /v3/{domain}/tags", "GET /v3/{domain}/tag", "GET /v3/{domain}/tag/stats/aggregates", "GET /v3/{domain}/tag/stats", "GET /v3/domains/{domain}/tag/devices", "GET /v3/domains/{domain}/tag/providers", "GET /v3/domains/{domain}/tag/countries", "GET /v3/stats/total", "GET /v3/{domain}/stats/total", "GET /v3/stats/total/domains", "GET /v3/stats/filter", "GET /v3/domains/{domain}/limits/tag", "GET /v3/{domain}/aggregates/providers", "GET /v3/{domain}/aggregates/devices", "GET /v3/{domain}/aggregates/countries", "POST /v1/analytics/metrics", "POST /v1/analytics/usage/metrics", "POST /v1/analytics/logs", "GET /v3/{domainID}/bounces/{address}", "GET /v3/{domainID}/bounces", "GET /v3/{domainID}/unsubscribes/{address}", "GET /v3/{domainID}/unsubscribes", "GET /v3/{domainID}/complaints/{address}", "GET /v3/{domainID}/complaints", "GET /v3/{domainID}/whitelists/{value}", "GET /v3/{domainID}/whitelists", "GET /v3/accounts/email_domain_suppressions/{email_domain}", "GET /v5/accounts/limit/custom/monthly", ]; /** * Makes an authenticated request to the Mailgun API * @param {string} method - HTTP method (GET, POST, etc.) * @param {string} path - API endpoint path * @param {Object} data - Request payload data (for POST/PUT requests) * @returns {Promise<Object>} - Response data as JSON */ export async function makeMailgunRequest(method, path, data = null) { return new Promise((resolve, reject) => { // Normalize path format (handle paths with or without leading slash) const cleanPath = path.startsWith('/') ? path.substring(1) : path; // Create basic auth credentials from API key const auth = Buffer.from(`api:${MAILGUN_API_KEY}`).toString("base64"); const options = { hostname: MAILGUN_API_HOSTNAME, path: `/${cleanPath}`, method: method, headers: { "Authorization": `Basic ${auth}`, "Content-Type": "application/x-www-form-urlencoded" } }; // Create and send the HTTP request const req = https.request(options, (res) => { let responseData = ""; res.on("data", (chunk) => { responseData += chunk; }); res.on("end", () => { try { const parsedData = JSON.parse(responseData); if (res.statusCode >= 200 && res.statusCode < 300) { resolve(parsedData); } else { reject(new Error(`Mailgun API error: ${parsedData.message || responseData}`)); } } catch (e) { reject(new Error(`Failed to parse response: ${e.message}`)); } }); }); req.on("error", (error) => { reject(error); }); // For non-GET requests, serialize and send the form data if (data && method !== "GET") { // Convert object to URL encoded form data const formData = new URLSearchParams(); for (const [key, value] of Object.entries(data)) { if (Array.isArray(value)) { for (const item of value) { formData.append(key, item); } } else if (value !== undefined && value !== null) { formData.append(key, value.toString()); } } req.write(formData.toString()); } req.end(); }); } /** * Loads and parses the OpenAPI specification from a YAML file * @param {string} filePath - Path to the OpenAPI YAML file * @returns {Object} - Parsed OpenAPI specification */ export function loadOpenApiSpec(filePath) { try { const fileContents = fs.readFileSync(filePath, 'utf8'); return yaml.load(fileContents); } catch (error) { console.error(`Error loading OpenAPI spec: ${error.message}`); // Don't exit in test mode if (process.env.NODE_ENV !== 'test') { process.exit(1); } throw error; // Throw so tests can catch it } } /** * Converts OpenAPI schema definitions to Zod validation schemas * @param {Object} schema - OpenAPI schema object * @param {Object} fullSpec - Complete OpenAPI specification * @returns {z.ZodType} - Corresponding Zod schema */ export function openapiToZod(schema, fullSpec) { if (!schema) return z.any(); // Handle schema references (e.g. #/components/schemas/...) if (schema.$ref) { // For #/components/schemas/EventSeverityType type references if (schema.$ref.startsWith('#/')) { const refPath = schema.$ref.substring(2).split('/'); // Navigate through the object using the path segments let referenced = fullSpec; for (const segment of refPath) { if (!referenced || !referenced[segment]) { // If we can't resolve it but know it's EventSeverityType, use our knowledge if (segment === 'EventSeverityType' || schema.$ref.endsWith('EventSeverityType')) { return z.enum(['temporary', 'permanent']) .describe('Filter by event severity'); } console.error(`Failed to resolve reference: ${schema.$ref}, segment: ${segment}`); return z.any().describe(`Failed reference: ${schema.$ref}`); } referenced = referenced[segment]; } return openapiToZod(referenced, fullSpec); } // Handle other reference formats if needed console.error(`Unsupported reference format: ${schema.$ref}`); return z.any().describe(`Unsupported reference: ${schema.$ref}`); } // Convert different schema types to Zod equivalents switch (schema.type) { case 'string': let zodString = z.string(); if (schema.enum) { return z.enum(schema.enum); } if (schema.format === 'email') { zodString = zodString.email(); } if (schema.format === 'uri') { zodString = zodString.describe(`URI: ${schema.description || ''}`); } return zodString.describe(schema.description || ''); case 'number': case 'integer': let zodNumber = z.number(); if (schema.minimum !== undefined) { zodNumber = zodNumber.min(schema.minimum); } if (schema.maximum !== undefined) { zodNumber = zodNumber.max(schema.maximum); } return zodNumber.describe(schema.description || ''); case 'boolean': return z.boolean().describe(schema.description || ''); case 'array': return z.array(openapiToZod(schema.items, fullSpec)).describe(schema.description || ''); case 'object': if (!schema.properties) return z.record(z.any()); const shape = {}; for (const [key, prop] of Object.entries(schema.properties)) { shape[key] = schema.required?.includes(key) ? openapiToZod(prop, fullSpec) : openapiToZod(prop, fullSpec).optional(); } return z.object(shape).describe(schema.description || ''); default: // For schemas without a type but with properties if (schema.properties) { const shape = {}; for (const [key, prop] of Object.entries(schema.properties)) { shape[key] = schema.required?.includes(key) ? openapiToZod(prop, fullSpec) : openapiToZod(prop, fullSpec).optional(); } return z.object(shape).describe(schema.description || ''); } // For YAML that defines "oneOf", "anyOf", etc. if (schema.oneOf) { const unionTypes = schema.oneOf.map(s => openapiToZod(s, fullSpec)); return z.union(unionTypes).describe(schema.description || ''); } if (schema.anyOf) { const unionTypes = schema.anyOf.map(s => openapiToZod(s, fullSpec)); return z.union(unionTypes).describe(schema.description || ''); } return z.any().describe(schema.description || ''); } } /** * Generates MCP tools from the OpenAPI specification * @param {Object} openApiSpec - Parsed OpenAPI specification */ export function generateToolsFromOpenApi(openApiSpec) { for (const endpoint of endpoints) { try { const [method, path] = endpoint.split(' '); const operationDetails = getOperationDetails(openApiSpec, method, path); if (!operationDetails) { console.warn(`Could not match endpoint: ${method} ${path} in OpenAPI spec`); continue; } const { operation, operationId } = operationDetails; const paramsSchema = buildParamsSchema(operation, openApiSpec); const toolId = sanitizeToolId(operationId); const toolDescription = operation.summary || `${method.toUpperCase()} ${path}`; registerTool(toolId, toolDescription, paramsSchema, method, path, operation); } catch (error) { console.error(`Failed to process endpoint ${endpoint}: ${error.message}`); } } return; } /** * Retrieves operation details from the OpenAPI spec for a given method and path * @param {Object} openApiSpec - Parsed OpenAPI specification * @param {string} method - HTTP method (GET, POST, etc.) * @param {string} path - API endpoint path * @returns {Object|null} - Operation details or null if not found */ export function getOperationDetails(openApiSpec, method, path) { const lowerMethod = method.toLowerCase(); if (!openApiSpec.paths?.[path]?.[lowerMethod]) { return null; } return { operation: openApiSpec.paths[path][lowerMethod], operationId: `${method}-${path.replace(/[^\w-]/g, '-').replace(/-+/g, '-')}` }; } /** * Sanitizes an operation ID to be used as a tool ID * @param {string} operationId - The operation ID to sanitize * @returns {string} - Sanitized tool ID */ export function sanitizeToolId(operationId) { return operationId.replace(/[^\w-]/g, '-').toLowerCase(); } /** * Builds a Zod parameter schema from an OpenAPI operation * @param {Object} operation - OpenAPI operation object * @param {Object} openApiSpec - Complete OpenAPI specification * @returns {Object} - Zod parameter schema */ export function buildParamsSchema(operation, openApiSpec) { const paramsSchema = {}; // Process path parameters const pathParams = operation.parameters?.filter(p => p.in === 'path') || []; processParameters(pathParams, paramsSchema, openApiSpec); // Process query parameters const queryParams = operation.parameters?.filter(p => p.in === 'query') || []; processParameters(queryParams, paramsSchema, openApiSpec); // Process request body if it exists if (operation.requestBody) { processRequestBody(operation.requestBody, paramsSchema, openApiSpec); } return paramsSchema; } /** * Processes OpenAPI parameters into Zod schemas * @param {Array} parameters - OpenAPI parameter objects * @param {Object} paramsSchema - Target schema object to populate * @param {Object} openApiSpec - Complete OpenAPI specification */ export function processParameters(parameters, paramsSchema, openApiSpec) { for (const param of parameters) { const zodParam = openapiToZod(param.schema, openApiSpec); paramsSchema[param.name] = param.required ? zodParam : zodParam.optional(); } } /** * Processes request body schema into Zod schemas * @param {Object} requestBody - OpenAPI request body object * @param {Object} paramsSchema - Target schema object to populate * @param {Object} openApiSpec - Complete OpenAPI specification */ export function processRequestBody(requestBody, paramsSchema, openApiSpec) { if (!requestBody.content) return; // Try different content types in priority order const contentTypes = [ 'application/json', 'multipart/form-data', 'application/x-www-form-urlencoded' ]; for (const contentType of contentTypes) { if (!requestBody.content[contentType]) continue; let bodySchema = requestBody.content[contentType].schema; // Handle schema references if (bodySchema.$ref) { bodySchema = resolveReference(bodySchema.$ref, openApiSpec); } // Process schema properties if (bodySchema?.properties) { for (const [prop, schema] of Object.entries(bodySchema.properties)) { let propSchema = schema; // Handle nested references if (propSchema.$ref) { propSchema = resolveReference(propSchema.$ref, openApiSpec); } const zodProp = openapiToZod(propSchema, openApiSpec); paramsSchema[prop] = bodySchema.required?.includes(prop) ? zodProp : zodProp.optional(); } } break; // We found and processed a content type } } /** * Resolves a schema reference within an OpenAPI spec * @param {string} ref - Reference string (e.g. #/components/schemas/ModelName) * @param {Object} openApiSpec - Complete OpenAPI specification * @returns {Object} - Resolved schema */ export function resolveReference(ref, openApiSpec) { const refPath = ref.replace('#/', '').split('/'); return refPath.reduce((obj, path) => obj[path], openApiSpec); } /** * Registers a tool with the MCP server * @param {string} toolId - Unique tool identifier * @param {string} toolDescription - Human-readable description * @param {Object} paramsSchema - Zod schema for parameters * @param {string} method - HTTP method (GET, POST, etc.) * @param {string} path - API endpoint path * @param {Object} operation - OpenAPI operation object */ export function registerTool(toolId, toolDescription, paramsSchema, method, path, operation) { server.tool( toolId, toolDescription, paramsSchema, async (params) => { try { const { actualPath, remainingParams } = processPathParameters(path, operation, params); const { queryParams, bodyParams } = separateParameters(remainingParams, operation, method); const finalPath = appendQueryString(actualPath, queryParams); // Make the API request const result = await makeMailgunRequest( method.toUpperCase(), finalPath, method.toUpperCase() === 'GET' ? null : bodyParams ); return { content: [ { type: "text", text: `✅ ${method.toUpperCase()} ${finalPath} completed successfully:\n${JSON.stringify(result, null, 2)}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error.message || String(error)}`, }, ], }; } } ); } /** * Processes path parameters from the request parameters * @param {string} path - API endpoint path with placeholders * @param {Object} operation - OpenAPI operation object * @param {Object} params - Request parameters * @returns {Object} - Processed path and remaining parameters */ export function processPathParameters(path, operation, params) { let actualPath = path; const pathParams = operation.parameters?.filter(p => p.in === 'path') || []; const remainingParams = { ...params }; for (const param of pathParams) { if (params[param.name]) { actualPath = actualPath.replace( `{${param.name}}`, encodeURIComponent(params[param.name]) ); delete remainingParams[param.name]; } else { throw new Error(`Required path parameter '${param.name}' is missing`); } } return { actualPath, remainingParams }; } /** * Separates parameters into query parameters and body parameters * @param {Object} params - Request parameters * @param {Object} operation - OpenAPI operation object * @param {string} method - HTTP method (GET, POST, etc.) * @returns {Object} - Separated query and body parameters */ export function separateParameters(params, operation, method) { const queryParams = {}; const bodyParams = {}; // Get query parameters from operation definition const definedQueryParams = operation.parameters?.filter(p => p.in === 'query').map(p => p.name) || []; // Sort parameters into body or query for (const [key, value] of Object.entries(params)) { if (definedQueryParams.includes(key)) { queryParams[key] = value; } else { bodyParams[key] = value; } } // For GET requests, move all params to query if (method.toUpperCase() === 'GET') { Object.assign(queryParams, bodyParams); Object.keys(bodyParams).forEach(key => delete bodyParams[key]); } return { queryParams, bodyParams }; } /** * Appends query string parameters to a path * @param {string} path - API endpoint path * @param {Object} queryParams - Query parameters * @returns {string} - Path with query string */ export function appendQueryString(path, queryParams) { if (Object.keys(queryParams).length === 0) { return path; } const queryString = new URLSearchParams(); for (const [key, value] of Object.entries(queryParams)) { if (value !== undefined && value !== null) { queryString.append(key, value.toString()); } } return `${path}?${queryString.toString()}`; } /** * Main function to initialize and start the MCP server */ export async function main() { try { // Load and parse OpenAPI spec const openApiSpec = loadOpenApiSpec(OPENAPI_YAML); // Generate tools from the spec generateToolsFromOpenApi(openApiSpec); // Connect to the transport const transport = new StdioServerTransport(); await server.connect(transport); console.error("Mailgun MCP Server running on stdio"); } catch (error) { console.error("Fatal error in main():", error); if (process.env.NODE_ENV !== 'test') { process.exit(1); } } } // Only auto-execute when not in test environment if (process.env.NODE_ENV !== 'test') { main(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mailgun/mailgun-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server