Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ bun.lock
.env
.env.*
!.env.example
test-api.js
Comment thread
coaldonac marked this conversation as resolved.
Outdated
*.log
npm-debug.log*
yarn-debug.log*
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,17 @@ Tag: marketing
```

## Implementation Details
### API Request Headers
All requests to the Postmark API that are made directly by this server include the following headers for client identification and correlation:

| Header | Description |
|--------|-------------|
| `X-Postmark-Client` | Client identifier: `postmark-mcp` |
| `X-Postmark-Client-Version` | Version of this MCP server (matches package version) |
| `X-Postmark-Correlation-Id` | A unique ID per request (UUID v4) for correlating requests with your logs or support. The API may use this in the future; it is safe to send now. |

These headers are sent on **every** request to the Postmark API. This server uses its own HTTP client (no postmark npm package) so that MCP traffic is identified as `postmark-mcp` and not as the Node.js SDK.

### Automatic Configuration
All emails are automatically configured with:
- `TrackOpens: true`
Expand Down
127 changes: 77 additions & 50 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,61 @@
*/

import 'dotenv/config';
import { createRequire } from 'module';
import { randomUUID } from 'crypto';
import fetch from 'node-fetch';
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import postmark from "postmark";

const require = createRequire(import.meta.url);
const { version: clientVersion } = require('./package.json');

const POSTMARK_CLIENT_ID = 'postmark-mcp';
const POSTMARK_API_BASE = 'https://api.postmarkapp.com';

const serverToken = process.env.POSTMARK_SERVER_TOKEN;
const defaultSender = process.env.DEFAULT_SENDER_EMAIL;
const defaultMessageStream = process.env.DEFAULT_MESSAGE_STREAM;

// Initialize Postmark client and MCP server
/** Headers sent on all requests to Postmark API for client identification and correlation. */
function postmarkRequestHeaders() {
return {
'X-Postmark-Client': POSTMARK_CLIENT_ID,
'X-Postmark-Client-Version': clientVersion,
'X-Postmark-Correlation-Id': randomUUID()
};
}

/** Make a request to the Postmark API with auth and client identification headers. */
async function postmarkRequest(path, options = {}) {
const url = path.startsWith('http') ? path : `${POSTMARK_API_BASE}${path}`;
const headers = {
'Accept': 'application/json',
'X-Postmark-Server-Token': serverToken,
...postmarkRequestHeaders(),
...options.headers
};
if (options.body && !headers['Content-Type']) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(url, {
...options,
headers
});
const text = await response.text();
if (!response.ok) {
let message = response.statusText;
try {
const data = JSON.parse(text);
if (data.Message) message = data.Message;
} catch (_) {}
throw new Error(`API request failed: ${response.status} ${message}`);
}
return text ? JSON.parse(text) : null;
}
Copy link
Copy Markdown
Contributor

@dandigangi dandigangi Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit but.... in Javascript land we use more linebreaks between code. Lol

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, please review


// Initialize MCP server and verify Postmark token
async function initializeServices() {
try {
if (!serverToken) {
Expand All @@ -39,32 +83,25 @@ async function initializeServices() {
console.error('Default sender: ', defaultSender);
console.error('Message stream: ', defaultMessageStream);

const client = new postmark.ServerClient(serverToken);

// Verify Postmark client by making a test API call
await client.getServer();
await postmarkRequest('/server');

const mcpServer = new McpServer({
name: "postmark-mcp",
version: "1.0.0"
});

return { postmarkClient: client, mcpServer };
return { mcpServer };
} catch (error) {
if (error.code || error.message) {
throw new Error(`Initialization failed: ${error.code ? `${error.code} - ` : ''}${error.message}`);
}

throw new Error('Initialization failed: An unexpected error occurred');
throw new Error(`Initialization failed: ${error.message}`);
}
}

// Start the server
async function main() {
try {
const { postmarkClient, mcpServer: server } = await initializeServices();
const { mcpServer: server } = await initializeServices();

registerTools(server, postmarkClient);
registerTools(server);

console.error('Connecting to MCP transport..');
const transport = new StdioServerTransport();
Expand Down Expand Up @@ -107,7 +144,7 @@ process.on('unhandledRejection', (reason) => {
});

// Move tool registration to a separate function for better organization
function registerTools(server, postmarkClient) {
function registerTools(server) {
// Define and register the sendEmail tool
server.tool(
"sendEmail",
Expand All @@ -120,7 +157,7 @@ function registerTools(server, postmarkClient) {
tag: z.string().optional().describe("Optional tag for categorization")
},
async ({ to, subject, textBody, htmlBody, from, tag }) => {
const emailData = {
const body = {
From: from || defaultSender,
To: to,
Subject: subject,
Expand All @@ -129,12 +166,14 @@ function registerTools(server, postmarkClient) {
TrackOpens: true,
TrackLinks: "HtmlAndText"
};

if (htmlBody) emailData.HtmlBody = htmlBody;
if (tag) emailData.Tag = tag;
if (htmlBody) body.HtmlBody = htmlBody;
if (tag) body.Tag = tag;

console.error('Sending email..', { to, subject });
const result = await postmarkClient.sendEmail(emailData);
const result = await postmarkRequest('/email', { method: 'POST', body: JSON.stringify(body) });
if (result.ErrorCode !== 0) {
throw new Error(result.Message || 'Failed to send email');
}
console.error('Email sent successfully: ', result.MessageID);

return {
Expand Down Expand Up @@ -162,30 +201,28 @@ function registerTools(server, postmarkClient) {
throw new Error("Either templateId or templateAlias must be provided");
}

const emailData = {
const body = {
From: from || defaultSender,
To: to,
TemplateModel: templateModel,
MessageStream: defaultMessageStream,
TrackOpens: true,
TrackLinks: "HtmlAndText"
};

if (templateId) {
emailData.TemplateId = templateId;
} else {
emailData.TemplateAlias = templateAlias;
}

if (tag) emailData.Tag = tag;
if (templateId) body.TemplateId = templateId;
else body.TemplateAlias = templateAlias;
if (tag) body.Tag = tag;

console.error('Sending template email..', { to, templateId: templateId || templateAlias });
const result = await postmarkClient.sendEmailWithTemplate(emailData);
const result = await postmarkRequest('/email/withTemplate', { method: 'POST', body: JSON.stringify(body) });
if (result.ErrorCode !== 0) {
throw new Error(result.Message || 'Failed to send template email');
}
console.error('Template email sent successfully: ', result.MessageID);

return {
content: [{
type: "text",
type: "text",
text: `Template email sent successfully!\nMessageID: ${result.MessageID}\nTo: ${to}\nTemplate: ${templateId || templateAlias}`
}]
};
Expand All @@ -198,17 +235,18 @@ function registerTools(server, postmarkClient) {
{},
async () => {
console.error('Fetching templates..');
const result = await postmarkClient.getTemplates();
console.error(`Found ${result.Templates.length} templates`);
const result = await postmarkRequest('/templates?count=100&offset=0');
const templates = result.Templates || [];
console.error(`Found ${templates.length} templates`);

const templateList = result.Templates.map(t =>
`• **${t.Name}**\n - ID: ${t.TemplateId}\n - Alias: ${t.Alias || 'none'}\n - Subject: ${t.Subject || 'none'}`
const templateList = templates.map(t =>
`• **${t.Name}**\n - ID: ${t.TemplateId}\n - Alias: ${t.Alias || 'none'}\n - Subject: ${t.Subject ?? 'none'}`
).join('\n\n');

return {
content: [{
type: "text",
text: `Found ${result.Templates.length} templates:\n\n${templateList}`
text: `Found ${templates.length} templates:\n\n${templateList}`
}]
};
}
Expand All @@ -228,22 +266,11 @@ function registerTools(server, postmarkClient) {
if (toDate) query.push(`todate=${encodeURIComponent(toDate)}`);
if (tag) query.push(`tag=${encodeURIComponent(tag)}`);

const url = `https://api.postmarkapp.com/stats/outbound${query.length ? '?' + query.join('&') : ''}`;
const url = `${POSTMARK_API_BASE}/stats/outbound${query.length ? '?' + query.join('&') : ''}`;

console.error('Fetching delivery stats..');

const response = await fetch(url, {
headers: {
"Accept": "application/json",
"X-Postmark-Server-Token": serverToken
}
});

if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}

const data = await response.json();
const data = await postmarkRequest(url);
console.error('Stats retrieved successfully');

const sent = data.Sent || 0;
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"@modelcontextprotocol/sdk": "^1.12.1",
"dotenv": "^16.4.5",
"node-fetch": "^3.3.2",
"postmark": "^4.0.5",
"zod": "^3.23.8"
},
"publishConfig": {
Expand Down