This is page 1 of 2. Use http://codebase.md/mailgun/mailgun-mcp-server?page={x} to view the full context.
# Directory Structure
```
├── .gitignore
├── jest.config.mjs
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── mailgun-mcp.js
│ └── openapi-final.yaml
└── test
└── mailgun-mcp.test.js
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependencies
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
.pnpm-debug.log
# Build outputs
dist/
build/
out/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Coverage directory used by tools like istanbul
coverage/
# Logs
logs
*.log
# Cache directories
.npm
.eslintcache
.parcel-cache
# IDE specific files
.idea/
.vscode/
*.swp
*.swo
.DS_Store
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Mailgun MCP Server
[](https://github.com/modelcontextprotocol)
## Overview
A Model Context Protocol (MCP) server implementation for [Mailgun](https://mailgun.com), enabling MCP-compatible AI clients like Claude Desktop to interract with the service.
## Prerequisites
- Node.js (v18 or higher)
- Git
- Claude Desktop (for Claude integration)
- Mailgun account and an API key
## Quick Start
### Manual Installation
1. Clone the repository:
```bash
git clone https://github.com/mailgun/mailgun-mcp-server.git
cd mailgun-mcp-server
```
2. Install dependencies and build:
```bash
npm install
```
3. Configure Claude Desktop:
Create or modify the config file:
- MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%/Claude/claude_desktop_config.json`
Add the following configuration:
```json
{
"mcpServers": {
"mailgun": {
"command": "node",
"args": ["CHANGE/THIS/PATH/TO/mailgun-mcp-server/src/mailgun-mcp.js"],
"env": {
"MAILGUN_API_KEY": "YOUR-mailgun-api-key"
}
}
}
}
```
## Testing
Run the local test suite with:
```bash
NODE_ENV=test npm test
```
### Sample Prompts with Claude
#### Send an Email
> Note: sending an email currently (2025-03-18) seems to require a paid account with Anthropic. You'll get a silent failure on the free account
```
Can you send an email to EMAIL_HERE with a funny email body that makes it sound like it's from the IT Desk from Office Space?
Please use the sending domain DOMAIN_HERE, and make the email from "postmaster@DOMAIN_HERE"!
```
#### Fetch and Visualize Sending Statistics
```
Would you be able to make a chart with email delivery statistics for the past week?
```
## Debugging
The MCP server communicates over stdio, please refer to [Debugging](https://modelcontextprotocol.io/docs/tools/debugging) section of the Model Context Protocol.
## License
[LICENSE](LICENSE) file for details
## Contributing
We welcome contributions! Please feel free to submit a Pull Request.
```
--------------------------------------------------------------------------------
/jest.config.mjs:
--------------------------------------------------------------------------------
```
export default {
testEnvironment: 'node',
transform: {},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
collectCoverage: true,
coverageReporters: ['text', 'lcov'],
testMatch: ['**/test/**/*.test.js'],
};
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"type": "module",
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
},
"devDependencies": {
"@jest/globals": "^29.0.0",
"jest": "^29.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.7.0",
"js-yaml": "^4.1.0",
"zod": "^3.24.2"
}
}
```
--------------------------------------------------------------------------------
/test/mailgun-mcp.test.js:
--------------------------------------------------------------------------------
```javascript
import { jest } from '@jest/globals';
import * as serverModule from '../src/mailgun-mcp.js';
// Disable console.error during tests
const originalConsoleError = console.error;
console.error = jest.fn();
// Override process.exit during tests
const originalProcessExit = process.exit;
process.exit = jest.fn();
describe('Mailgun MCP Server', () => {
// Focus on testing the pure utility functions that don't need mocks
describe('processPathParameters()', () => {
test('replaces path parameters with values', () => {
const path = '/v3/{domain_name}/messages';
const operation = {
parameters: [
{
name: 'domain_name',
in: 'path',
required: true
}
]
};
const params = { domain_name: 'example.com', to: '[email protected]' };
const result = serverModule.processPathParameters(path, operation, params);
expect(result.actualPath).toBe('/v3/example.com/messages');
expect(result.remainingParams).toEqual({ to: '[email protected]' });
});
test('throws error if required path parameter is missing', () => {
const path = '/v3/{domain_name}/messages';
const operation = {
parameters: [
{
name: 'domain_name',
in: 'path',
required: true
}
]
};
const params = { to: '[email protected]' };
expect(() => {
serverModule.processPathParameters(path, operation, params);
}).toThrow(/required path parameter.*missing/i);
});
});
describe('separateParameters()', () => {
test('separates query and body parameters', () => {
const params = {
limit: 10,
page: 1,
to: '[email protected]',
from: '[email protected]'
};
const operation = {
parameters: [
{ name: 'limit', in: 'query' },
{ name: 'page', in: 'query' }
]
};
const result = serverModule.separateParameters(params, operation, 'POST');
expect(result.queryParams).toEqual({ limit: 10, page: 1 });
expect(result.bodyParams).toEqual({
to: '[email protected]',
from: '[email protected]'
});
});
test('moves all params to query for GET requests', () => {
const params = {
limit: 10,
page: 1,
to: '[email protected]',
from: '[email protected]'
};
const operation = {
parameters: [
{ name: 'limit', in: 'query' },
{ name: 'page', in: 'query' }
]
};
const result = serverModule.separateParameters(params, operation, 'GET');
expect(result.queryParams).toEqual({
limit: 10,
page: 1,
to: '[email protected]',
from: '[email protected]'
});
expect(result.bodyParams).toEqual({});
});
});
describe('appendQueryString()', () => {
test('appends query parameters to path', () => {
const path = '/v3/domains';
const queryParams = { limit: 10, skip: 0 };
const result = serverModule.appendQueryString(path, queryParams);
expect(result).toBe('/v3/domains?limit=10&skip=0');
});
test('returns original path if no query parameters', () => {
const path = '/v3/domains';
const queryParams = {};
const result = serverModule.appendQueryString(path, queryParams);
expect(result).toBe('/v3/domains');
});
});
// Clean up after all tests
afterAll(() => {
console.error = originalConsoleError;
process.exit = originalProcessExit;
});
});
describe('openapiToZod()', () => {
test('converts string schema correctly', () => {
const schema = { type: 'string', description: 'A test string' };
const result = serverModule.openapiToZod(schema, {});
expect(result._def.typeName).toBe('ZodString');
expect(result._def.description).toBe('A test string');
});
test('converts enum schema correctly', () => {
const schema = { type: 'string', enum: ['yes', 'no', 'maybe'] };
const result = serverModule.openapiToZod(schema, {});
expect(result._def.typeName).toBe('ZodEnum');
expect(result._def.values).toEqual(['yes', 'no', 'maybe']);
});
test('converts number schema with constraints', () => {
const schema = {
type: 'number',
minimum: 1,
maximum: 100,
description: 'A constrained number'
};
const result = serverModule.openapiToZod(schema, {});
expect(result._def.typeName).toBe('ZodNumber');
// Check for min constraint
expect(result._def.checks.some(check =>
check.kind === 'min' && check.value === 1
)).toBe(true);
// Check for max constraint
expect(result._def.checks.some(check =>
check.kind === 'max' && check.value === 100
)).toBe(true);
});
test('resolves references correctly', () => {
const schema = { $ref: '#/components/schemas/TestType' };
const fullSpec = {
components: {
schemas: {
TestType: { type: 'string', description: 'Referenced type' }
}
}
};
const result = serverModule.openapiToZod(schema, fullSpec);
expect(result._def.typeName).toBe('ZodString');
expect(result._def.description).toBe('Referenced type');
});
});
describe('getOperationDetails()', () => {
test('returns operation details for valid path and method', () => {
const openApiSpec = {
paths: {
'/test/path': {
get: {
operationId: 'getTest',
summary: 'Test operation'
}
}
}
};
const result = serverModule.getOperationDetails(openApiSpec, 'get', '/test/path');
expect(result).toEqual({
operation: {
operationId: 'getTest',
summary: 'Test operation'
},
operationId: 'get--test-path'
});
});
test('returns null for invalid path', () => {
const openApiSpec = {
paths: {
'/test/path': {
get: {
operationId: 'getTest',
summary: 'Test operation'
}
}
}
};
const result = serverModule.getOperationDetails(openApiSpec, 'get', '/nonexistent/path');
expect(result).toBeNull();
});
test('returns null for invalid method', () => {
const openApiSpec = {
paths: {
'/test/path': {
get: {
operationId: 'getTest',
summary: 'Test operation'
}
}
}
};
const result = serverModule.getOperationDetails(openApiSpec, 'post', '/test/path');
expect(result).toBeNull();
});
});
describe('resolveReference()', () => {
test('resolves reference path correctly', () => {
const openApiSpec = {
components: {
schemas: {
TestSchema: { type: 'string', description: 'Test schema' }
}
}
};
const result = serverModule.resolveReference('#/components/schemas/TestSchema', openApiSpec);
expect(result).toEqual({ type: 'string', description: 'Test schema' });
});
test('handles nested reference path', () => {
const openApiSpec = {
components: {
schemas: {
Parent: {
NestedType: { type: 'number', minimum: 0 }
}
}
}
};
const result = serverModule.resolveReference('#/components/schemas/Parent/NestedType', openApiSpec);
expect(result).toEqual({ type: 'number', minimum: 0 });
});
});
```
--------------------------------------------------------------------------------
/src/mailgun-mcp.js:
--------------------------------------------------------------------------------
```javascript
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();
}
```