Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ bun.lock
npm-debug.log*
yarn-debug.log*
yarn-error.log*
logs/
logs/
.idea/

# Local smoke-test copies (created from *.example.mjs counterparts).
# Keep personal verified-sender addresses out of the repo.
smoke-test.mjs
smoke-test-mutating.mjs
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2026-04-29

This release expands the MCP tool surface from 4 tools to 24, organized into eight categories. It includes one breaking change for users on Node 16 or 18.

### ⚠ Breaking changes

- **Minimum Node.js version raised from 16 to 20.** The `@modelcontextprotocol/sdk` dependency requires Node ≥ 18; we set the floor at 20 to match a current LTS line. Users on Node 16 or 18 will see `EBADENGINE` warnings on install, or hard failures with `engine-strict=true`. The previous `>=16` declaration was already incompatible with the SDK at runtime — this release makes the floor honest.

### Added

- **20 new tools** across templates, messages, diagnostics, bounces, suppressions, webhooks, and server info. See the README "Tools" section for the complete reference.
- **`diagnoseDelivery`** — composite triage tool. Answers "did my email reach X, and if not, why?" by running message search, suppression check, and bounce history in parallel for a recipient, then synthesizing a plain-English recommendation. First tool in a new "Diagnostics" category.
- **`sendBatch`** and **`sendBatchWithTemplate`** — wraps Postmark's *synchronous* batch endpoints (`/email/batch`, `/email/batchWithTemplates`). Send up to 500 distinct messages or templated recipients in a single HTTP request, with immediate per-message success/failure reporting. Note: these are Postmark's batch endpoints; Postmark's separate *asynchronous* bulk email API at `/email/bulk` (submit-and-poll, no message count cap, 50 MB payload limit, subject to approval) is a parallel capability for large-volume jobs and is tracked as a v2.1 follow-up — not a replacement for batch.
- **Template CRUD + validation** — `getTemplate`, `createTemplate`, `editTemplate`, `deleteTemplate`, `validateTemplate`. End-to-end template authoring including layout binding (pass `layoutTemplate: "<alias>"` to bind, `null` to unbind).
- **Message search and details** — `searchOutboundMessages` (with `messageStream` filter), `getMessageDetails` (full event timeline).
- **Bounce tooling** — `searchBounces`, `getBounceDump`, `activateBounce`.
- **Suppression management** — `listSuppressions`, `createSuppressions`, `deleteSuppressions` (up to 50 addresses per call).
- **Webhook lifecycle** — `listWebhooks`, `createWebhook`, `deleteWebhook`. `createWebhook` requires at least one trigger enabled.
- **`getServerInfo`** — server name, color, tracking settings, configured webhook URLs.
- **`getDeliveryStats` `stat` parameter (optional).** With no argument, returns a friendly summary (preserves v1 behavior). With `stat: "<name>"`, returns a polished per-stat breakdown. Supported values: `summary`, `overview`, `sent`, `bounces`, `spam`, `tracked`, `opens`, `openPlatforms`, `openClients`, `openReadTimes`, `clicks`, `clickBrowsers`, `clickPlatforms`, `clickLocation`.
- **Validation guards** on `editTemplate`, `createWebhook`, `createTemplate`, `validateTemplate`, and `sendEmailWithTemplate` — misuse fails fast with a clear message instead of hitting the API.
- **Smoke-test example harnesses** — `smoke-test.example.mjs` (read-only, 23 checks) and `smoke-test-mutating.example.mjs` (full lifecycles + real sends + cleanup, 14 checks). Copy to the non-example name (gitignored) and edit verified-sender addresses to use. The mutating harness includes a startup guard that refuses to run with placeholder values.

### Changed

- **`getDeliveryStats` default summary output reformatted.** Now includes bounce rate, spam rate, and tracked-of-sent percentage in addition to the v1 sent/open/click rates. All v1 fields are still present, just rendered with thousands separators and aligned columns.
- **All tools now use the official `postmark` SDK exclusively.** Previously `getDeliveryStats` bypassed the SDK with raw `fetch` against the REST API. The SDK handles auth, retries, and error mapping consistently.

### Fixed

- **`getServerInfo` displayed the wrong field for "First Open Only".** Was reading `EnableSmtpApiErrorHooks` (an unrelated boolean about SMTP API error hooks); now correctly reads `PostFirstOpenOnly`.
- **`engines.node` was lying.** Declared `>=16` but the MCP SDK requires `>=18`. The package would `npm install` on Node 16 and then fail at runtime. Floor is now `>=20` and accurate.

### Removed

- **`node-fetch` dependency.** No longer needed; the SDK handles all HTTP.

## [1.0.0] - Initial release

Initial public release of the official Postmark MCP server.

### Added

- Four MCP tools: `sendEmail`, `sendEmailWithTemplate`, `listTemplates`, `getDeliveryStats`.
- Stdio JSON-RPC transport for AI assistant consumption (Claude, Cursor, etc.).
- Configuration via `POSTMARK_SERVER_TOKEN`, `DEFAULT_SENDER_EMAIL`, `DEFAULT_MESSAGE_STREAM` environment variables.
- Automatic open and click tracking on every send.
73 changes: 73 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Official Postmark MCP (Model Context Protocol) server that enables AI assistants (Claude, Cursor, etc.) to interact with the Postmark email API. Published as `@activecampaign/postmark-mcp` on NPM.

## Commands

- **Run server:** `npm start` (runs `node index.js`)
- **Debug with MCP Inspector:** `npm run inspector` (launches `@modelcontextprotocol/inspector`)
- **Smoke test (read-only):** `npm run smoke` — runs `smoke-test.mjs`, which is created from [smoke-test.example.mjs](smoke-test.example.mjs). Spawns the server over stdio and exercises every read-only tool against the live Postmark account configured in `.env`. Does not send mail or mutate state.
- **Smoke test (mutating):** `node smoke-test-mutating.mjs` — runs full create→edit→delete lifecycles for templates (including layout binding), webhooks, and suppressions, plus real email sends between two configured verified addresses. Cleans up after itself. Created from [smoke-test-mutating.example.mjs](smoke-test-mutating.example.mjs); edit `SENDER` and `RECIPIENT` before running. The script refuses to run with the placeholder values still in place.
- **`smoke-test.mjs` and `smoke-test-mutating.mjs` are gitignored.** Copy from the `*.example.mjs` counterparts; this keeps personal verified-sender addresses out of the repo.
- **No linter is configured.**

## Architecture

This is a single-file MCP server (`index.js`) using ES modules (`"type": "module"`). The structure within `index.js`:

1. **Module-level formatting helpers** — `fmtInt`, `fmtPct`, `fmtPlatformUsage`, `fmtTopBreakdown`, `fmtDeliverySummary`, `fmtStatResponse`. Used by `getDeliveryStats` to render polished per-stat output. Keep these pure / dependency-free.
2. **`initializeServices()`** — Validates required env vars, creates a `postmark.ServerClient`, verifies connectivity via `getServer()`, and instantiates `McpServer` from `@modelcontextprotocol/sdk`
3. **`registerTools(server, postmarkClient)`** — Registers all 21 MCP tools with Zod schemas for input validation. Each tool wraps a Postmark SDK call and returns formatted text responses
4. **`main()`** — Orchestrates initialization, tool registration, and connects to `StdioServerTransport`

Communication uses stdio transport (stdin/stdout), not HTTP. All `console.error` calls are diagnostic logging (stdout is reserved for MCP protocol).

## MCP Tools Registered

The server exposes 24 tools across these categories:
- **Email:** sendEmail, sendEmailWithTemplate, sendBatch, sendBatchWithTemplate
- **Templates:** listTemplates, getTemplate, createTemplate, editTemplate, deleteTemplate, validateTemplate
- **Messages:** searchOutboundMessages, getMessageDetails
- **Diagnostics:** diagnoseDelivery
- **Bounces:** searchBounces, getBounceDump, activateBounce
- **Suppressions:** listSuppressions, createSuppressions, deleteSuppressions
- **Stats & Server:** getDeliveryStats, getServerInfo
- **Webhooks:** listWebhooks, createWebhook, deleteWebhook

`sendBatch` and `sendBatchWithTemplate` wrap Postmark's *synchronous* batch endpoints (`/email/batch` and `/email/batchWithTemplates` respectively), each accepting up to 500 messages. Both share a `formatBatchResults` helper defined inside `registerTools` that splits the SDK response by `ErrorCode` (0 = success, non-zero = failure) and renders a summary with capped success/failure lists. `sendBatchWithTemplate` accepts a top-level `from` / `tag` that's overridable per recipient — keep that override semantics if extending. Note: these are Postmark's *synchronous* batch endpoints — the API call returns per-message results in the response. Postmark's separate *asynchronous* bulk email API (`/email/bulk` — submit a job, get a request ID, poll `GET /email/bulk/{id}` for status; no message count cap, 50 MB payload limit; subject to approval) is a parallel capability not covered in v2.0. The two are parallel APIs in Postmark's docs, not one replacing the other; bulk is tracked as a v2.1 follow-up. If anyone asks "where's the bulk send tool," that's why — and adding it is more than a quick wrap because the SDK doesn't expose `/email/bulk` and the async pattern likely needs two tools (`submitBulk` + `getBulkStatus`).

`diagnoseDelivery` is a **composite tool** — it does not mirror a single Postmark endpoint. Instead it runs `getOutboundMessages` (or `getOutboundMessageDetails`), `getSuppressions`, and `getBounces` in parallel for the given recipient, then synthesizes a plain-English recommendation. When adding new diagnostic tools, follow the same pattern: parallelize independent lookups via `Promise.all`, swallow individual failures with `.catch(() => fallback)` so one 404 doesn't sink the whole diagnosis, and end with a `Recommended action` block that interprets the data.

`getDeliveryStats` is unified: with no arguments it returns a friendly headline summary; with `stat: "<name>"` it returns a polished per-stat breakdown. Supported `stat` values: `summary`, `overview`, `sent`, `bounces`, `spam`, `tracked`, `opens`, `openPlatforms`, `openClients`, `openReadTimes`, `clicks`, `clickBrowsers`, `clickPlatforms`, `clickLocation`. Each value maps to a specific `postmarkClient.get*` method in the `fetchers` object inside the tool — when adding a new stat, add both the enum value and the fetcher entry.

## Environment Variables

Required (set in `.env` for local dev, or in MCP client config for production):
- `POSTMARK_SERVER_TOKEN` — Postmark server API token
- `DEFAULT_SENDER_EMAIL` — Fallback sender address
- `DEFAULT_MESSAGE_STREAM` — Message stream ID (typically `outbound`)

## Key Patterns

- All emails are auto-configured with `TrackOpens: true` and `TrackLinks: "HtmlAndText"`
- All tools use the `postmark` npm client (no raw `fetch` — the SDK handles auth, retries, and error mapping consistently)
- Tool handlers follow a consistent pattern: log start, call API, log result, return `{ content: [{ type: "text", text }] }`
- Postmark API field names are PascalCase (`From`, `To`, `Subject`, `EmailAddress`); query-string filters on list endpoints are typically lowercase (`fromdate`, `todate`, `messagestream`). The SDK accepts both since Postmark's API is case-insensitive on query params, but match the convention you see in nearby code
- Numeric IDs use `z.number().int()`; date strings use `.regex(/^\d{4}-\d{2}-\d{2}$/)` for YYYY-MM-DD validation
- Tools that mutate state include lightweight validation guards before hitting the API:
- `editTemplate` requires at least one updated field (otherwise the call would be a wasted no-op)
- `createWebhook` requires at least one trigger enabled (otherwise the webhook is dead-on-arrival)
- `createTemplate` requires at least one of `htmlBody` / `textBody`
- `validateTemplate` requires at least one of `subject` / `htmlBody` / `textBody`
- `sendEmailWithTemplate` requires exactly one of `templateId` / `templateAlias`

## Gotchas

- The Postmark suppressions list endpoint (`/message-streams/{stream}/suppressions/dump`) is **eventually consistent** — a suppression created milliseconds earlier may not appear in the next `listSuppressions` call. Don't write tests that assume strict read-your-writes for this endpoint.
- `getServer()` returns `PostFirstOpenOnly` for the "first open only" tracking setting. `EnableSmtpApiErrorHooks` is a different (unrelated) flag — don't conflate them in `getServerInfo`'s output.
- `getOutboundOverview` includes `Opens` / `UniqueOpens` despite the field naming — the response is broader than the endpoint name suggests, which is why `getDeliveryStats` can use it for both `summary` and `overview` modes.
- Several SDK methods don't exist with intuitive names. Use the correct ones: `getEmailOpenClientUsage` (not `getEmailClientUsage`), `getEmailOpenPlatformUsage` (not `getEmailPlatformUsage`), `getClickLocation` (not `getClickLocationUsage`).
Loading