{"_id":"@activecampaign/postmark-mcp","_rev":"3-fdd74445f3770afeabad80a00be85305","name":"@activecampaign/postmark-mcp","dist-tags":{"latest":"2.1.1"},"versions":{"1.0.0":{"name":"@activecampaign/postmark-mcp","version":"1.0.0","keywords":["postmark","activecampaign","email","mcp","model-context-protocol","claude","cursor","ai"],"author":{"name":"Jabal Torres"},"license":"MIT","_id":"@activecampaign/postmark-mcp@1.0.0","maintainers":[{"name":"activecampaign-eng","email":"admin@activecampaign.com"},{"name":"dandigangi_ac","email":"ddigangi@activecampaign.com"},{"name":"scottplumlee-ac","email":"splumlee@activecampaign.com"}],"homepage":"https://github.com/activecampaign/postmark-mcp#readme","bugs":{"url":"https://github.com/activecampaign/postmark-mcp/issues"},"dist":{"shasum":"f8124a45f80b6019fe9dd3c57a07e1fb90ba4bd8","tarball":"https://registry.npmjs.org/@activecampaign/postmark-mcp/-/postmark-mcp-1.0.0.tgz","fileCount":4,"integrity":"sha512-LgIC0nt60R8+yBwxJalnGqLuF2mVP8Iqdc8aLK3VHo9cbVJPIntUBe0Rv0wujd1wwxJYmwp7x/3UFWpmI+xMJQ==","signatures":[{"sig":"MEYCIQCKYCrEubLwYEwJ2bCSHjk0UpGxIwdyGWtRSEZj64UthQIhALz13nI2fTtIqKmt3XHj/JdW/C51z60JTozT1zTLprEq","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":19182},"main":"index.js","type":"module","engines":{"node":">=16.0.0"},"gitHead":"80db79b59ace5ebaa640c092b853e761f099aabd","scripts":{"start":"node index.js","inspector":"npx @modelcontextprotocol/inspector index.js"},"_npmUser":{"name":"dandigangi_ac","email":"ddigangi@activecampaign.com"},"repository":{"url":"git+https://github.com/activecampaign/postmark-mcp.git","type":"git"},"_npmVersion":"11.6.0","description":"Official Postmark MCP server for sending emails via Claude and AI assistants","directories":{},"_nodeVersion":"24.9.0","dependencies":{"zod":"^3.23.8","dotenv":"^16.4.5","postmark":"^4.0.5","node-fetch":"^3.3.2","@modelcontextprotocol/sdk":"^1.12.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/postmark-mcp_1.0.0_1764191481312_0.17600136231794083","host":"s3://npm-registry-packages-npm-production"}},"2.1.1":{"name":"@activecampaign/postmark-mcp","version":"2.1.1","keywords":["postmark","activecampaign","email","mcp","model-context-protocol","claude","cursor","ai"],"author":{"url":"https://postmarkapp.com","name":"Postmark","email":"support@postmarkapp.com"},"license":"MIT","_id":"@activecampaign/postmark-mcp@2.1.1","maintainers":[{"name":"activecampaign-eng","email":"admin@activecampaign.com"},{"name":"dandigangi_ac","email":"ddigangi@activecampaign.com"},{"name":"scottplumlee-ac","email":"splumlee@activecampaign.com"}],"homepage":"https://github.com/activecampaign/postmark-mcp#readme","bugs":{"url":"https://github.com/activecampaign/postmark-mcp/issues"},"bin":{"postmark-mcp":"index.js"},"dist":{"shasum":"c208b241999bdf43b82f9bcc852d23c5ce419a4f","tarball":"https://registry.npmjs.org/@activecampaign/postmark-mcp/-/postmark-mcp-2.1.1.tgz","fileCount":8,"integrity":"sha512-ygQM80bgMY7z3nKrpVQMw2Y6/EoUH12OCoNIaWdQw+rZdDsAJysyi5F8oKTTS+Ks8OOoVtRhiJakrIzBd3RC5Q==","signatures":[{"sig":"MEUCIQDpxM2LW8p8FEij6hpo7OEVvik5XHYbokszhWbkRgwyzQIgFXujYISl5wBgEA6zR4gBEPiWY56JLB7WXNc+YS38vGA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":137463},"main":"index.js","type":"module","engines":{"node":">=20.0.0"},"gitHead":"63ef055861820b12b748697339488d564d754a5c","scripts":{"test":"node --test test-unit.mjs","smoke":"node smoke-test.mjs","start":"node index.js","presmoke":"node --input-type=module --eval \"import{existsSync}from'fs';if(!existsSync('./smoke-test.mjs')){console.error('\\nsmoke-test.mjs not found.\\nCopy the example file and add your verified sender address:\\n\\n  cp smoke-test.example.mjs smoke-test.mjs\\n\\nThen edit smoke-test.mjs and set FROM_EMAIL to a verified Postmark sender.\\n');process.exit(1)}\"","test:e2e":"node --test test-e2e.mjs","inspector":"npx @modelcontextprotocol/inspector index.js","test:offline":"node --test test-offline.mjs","prepublishOnly":"npm test","smoke:mutating":"node smoke-test-mutating.mjs","presmoke:mutating":"node --input-type=module --eval \"import{existsSync}from'fs';if(!existsSync('./smoke-test-mutating.mjs')){console.error('\\nsmoke-test-mutating.mjs not found.\\nCopy the example file and add your verified sender address:\\n\\n  cp smoke-test-mutating.example.mjs smoke-test-mutating.mjs\\n\\nThen edit smoke-test-mutating.mjs and set FROM_EMAIL to a verified Postmark sender.\\n');process.exit(1)}\""},"_npmUser":{"name":"dandigangi_ac","email":"ddigangi@activecampaign.com"},"repository":{"url":"git+https://github.com/activecampaign/postmark-mcp.git","type":"git"},"_npmVersion":"11.6.0","description":"Official Postmark MCP server for sending emails via Claude and AI assistants","directories":{},"_nodeVersion":"24.9.0","dependencies":{"zod":"3.25.76","dotenv":"16.6.1","@modelcontextprotocol/sdk":"1.29.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/postmark-mcp_2.1.1_1783975691207_0.661868101914902","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2025-11-26T21:11:21.162Z","modified":"2026-07-14T21:06:38.771Z","1.0.0":"2025-11-26T21:11:21.495Z","2.1.1":"2026-07-13T20:48:11.358Z"},"bugs":{"url":"https://github.com/activecampaign/postmark-mcp/issues"},"author":{"url":"https://postmarkapp.com","name":"Postmark","email":"support@postmarkapp.com"},"license":"MIT","homepage":"https://github.com/activecampaign/postmark-mcp#readme","keywords":["postmark","activecampaign","email","mcp","model-context-protocol","claude","cursor","ai"],"repository":{"url":"git+https://github.com/activecampaign/postmark-mcp.git","type":"git"},"description":"Official Postmark MCP server for sending emails via Claude and AI assistants","maintainers":[{"email":"admin@activecampaign.com","name":"activecampaign-eng"},{"email":"shayhowe@gmail.com","name":"shayhowe"},{"email":"ddigangi@activecampaign.com","name":"dandigangi_ac"},{"email":"splumlee@activecampaign.com","name":"scottplumlee-ac"}],"readme":"# Official Postmark MCP Server&nbsp;&nbsp;&nbsp;[![NPM Version](https://img.shields.io/npm/v/@activecampaign/postmark-mcp.svg)](https://www.npmjs.com/package/@activecampaign/postmark-mcp)&nbsp;&nbsp;![MIT licensed](https://img.shields.io/npm/l/%40activecampaign%2Fpostmark-mcp)\n\nSend emails with Postmark using Claude and other MCP-compatible AI assistants.\n\n## Features\n- Exposes a Model Context Protocol (MCP) server backed by your [Postmark account](https://account.postmarkapp.com/sign_up)\n- 24 tools spanning email sending (single + batch), templates (CRUD + validation), message search, delivery diagnostics, bounces, suppressions, stats, server info, and webhooks\n- MCP tool annotations (`readOnlyHint`, `destructiveHint`) let supporting clients auto-approve safe reads and require confirmation before mutating or destructive operations\n- Simple configuration via environment variables\n- Comprehensive error handling and graceful shutdown\n- Structured JSON logging to stderr with optional log-file persistence; email addresses are partially masked by default\n- HTTPS enforcement and optional domain allowlist for webhook registration\n- Automatic open/click tracking on every send\n\n## Useful Docs\n- [📒 API Documentation](https://postmarkapp.com/developer)\n- [🔎 API Explorer](https://postmarkapp.com/api-explorer)\n- [📖 Engineering Articles](https://postmarkapp.com/blog/topics/engineering)\n- [📝 Changelog](CHANGELOG.md) — what's new in each release\n\n## Feedback\nWe'd love to hear from you! Please share your feedback and suggestions using our [feedback form](https://forms.gle/zVdZLAJPM81Vo2Wh8).\n\nFollow us on X - [@postmarkapp](https://x.com/postmarkapp)\n\n---\n\n# Setup\n\n## Requirements\n- Node.js v20 or higher\n- A [Postmark account](https://account.postmarkapp.com/sign_up) and server token\n\n## Installation (Local Development)\n**Clone the repository:**\n```sh\ngit clone https://github.com/ActiveCampaign/postmark-mcp\ncd postmark-mcp\n```\n\n**Install dependencies:**\n```sh\nnpm install\n# or\nyarn\n# or\nbun install\n```\n\n## Configuration (Local Development)\n\nCreate your own environment file from the example\n\n```sh\ncp .env.example .env\n```\n\nEdit your `.env` to contain your Postmark credentials and settings.\n\n**Important:** This is intended for local development purposes only. Secrets should never be stored in version control and `.env` type files should be added to `.gitignore`.\n\n### Required\n\n| Variable | Description |\n|---|---|\n| `POSTMARK_SERVER_TOKEN` | Your Postmark server API token |\n| `DEFAULT_SENDER_EMAIL` | Default sender email address (must be a verified sender in Postmark) |\n| `DEFAULT_MESSAGE_STREAM` | Postmark message stream (e.g., `outbound`) |\n\n### Optional\n\n| Variable | Default | Description |\n|---|---|---|\n| `AGENT_LABEL` | — | A label for this instance (e.g., `prod`, `staging`). Sent as `X-Agent-Label` on every Postmark API request, useful for identifying traffic sources in logs or support tickets. |\n| `WEBHOOK_URL_ALLOWLIST` | — | Comma-separated list of HTTPS URL prefixes that `createWebhook` will accept (e.g., `https://hooks.yourapp.com,https://inbound.corp.io`). When unset, any valid HTTPS URL is accepted. |\n| `LOG_FILE` | — | Path to a file where structured JSON logs are appended in addition to stderr. The file is created if it does not exist. No rotation or size cap is applied — use an external tool such as `logrotate` to manage the file in long-running deployments. |\n| `LOG_EMAIL_FULL` | `false` | Set to `true` to log email addresses without masking. By default the mailbox portion is partially masked in logs (`u**r@example.com`). |\n\n**Run the server:**\n\n```sh\nnpm start\n# or\nyarn start\n# or\nbun start\n```\n\n**Smoke test (requires valid `.env`):**\n\nThe repo ships two smoke-test example files. Copy each to its non-example name (which is gitignored) before running, so your local edits — including any verified-sender addresses — never end up committed.\n\n```sh\n# Read-only suite (25 checks). Optionally edit RECIPIENT_WITH_HISTORY.\ncp smoke-test.example.mjs smoke-test.mjs\nnpm run smoke\n\n# Mutating suite (full lifecycles + real email sends).\n# REQUIRED: edit SENDER and RECIPIENT to two of your verified addresses.\ncp smoke-test-mutating.example.mjs smoke-test-mutating.mjs\nnode smoke-test-mutating.mjs\n```\n\nThe read-only suite spawns the server over stdio and exercises every read tool against your Postmark account, plus the validation paths for `editTemplate` and `createWebhook`. Does not send mail or mutate state.\n\nThe mutating suite runs full create→edit→delete lifecycles for templates (including layout binding), webhooks, and suppressions, and sends real emails between the two addresses you configure. It cleans up after itself. The script refuses to run while the placeholder values are still in place.\n\n## Cursor Quick Install\n<div>\n  <a href=\"cursor://anysphere.cursor-deeplink/mcp/install?name=Postmark&config=eyJjb21tYW5kIjoibm9kZSIsImFyZ3MiOlsiaW5kZXguanMiXSwiZW52Ijp7IlBPU1RNQVJLX1NFUlZFUl9UT0tFTiI6IiIsIkRFRkFVTFRfU0VOREVSX0VNQUlMIjoiIiwiREVGQVVMVF9NRVNTQUdFX1NUUkVBTSI6Im91dGJvdW5kIn19\">\n    <img src=\"https://img.shields.io/badge/Add_Postmark_MCP_Server-to_Cursor-00A4DB?style=for-the-badge&logo=cursor&logoColor=white\" alt=\"Add Postmark MCP Server to Cursor\" />\n  </a>\n</div>\n<br />\n\nAfter installing the MCP, update your configuration to set:\n- `POSTMARK_SERVER_TOKEN`\n- `DEFAULT_SENDER_EMAIL`\n- `DEFAULT_MESSAGE_STREAM` (default: `outbound`)\n\n## MCP Client Configuration\n\n### Using npx (recommended — no clone required)\n\nInstall directly from npm without managing a local copy:\n\n```json\n{\n  \"mcpServers\": {\n    \"postmark\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@activecampaign/postmark-mcp\"],\n      \"env\": {\n        \"POSTMARK_SERVER_TOKEN\": \"your-postmark-server-token\",\n        \"DEFAULT_SENDER_EMAIL\": \"your-sender-email@example.com\",\n        \"DEFAULT_MESSAGE_STREAM\": \"outbound\"\n      }\n    }\n  }\n}\n```\n\n### Using a local clone\n\n```json\n{\n  \"mcpServers\": {\n    \"postmark\": {\n      \"command\": \"node\",\n      \"args\": [\"/absolute/path/to/postmark-mcp/index.js\"],\n      \"env\": {\n        \"POSTMARK_SERVER_TOKEN\": \"your-postmark-server-token\",\n        \"DEFAULT_SENDER_EMAIL\": \"your-sender-email@example.com\",\n        \"DEFAULT_MESSAGE_STREAM\": \"outbound\"\n      }\n    }\n  }\n}\n```\n\nBoth snippets work with **Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`), **Cursor** (`.cursor/mcp.json`), and any other MCP client that accepts the standard JSON configuration format.\n\n## Tools\nThis section provides a complete reference for the Postmark MCP server tools including example prompts and payloads. The server registers **24 tools** organized into eight categories.\n\n### Table of Contents\n- [Email](#email)\n  - [sendEmail](#sendemail)\n  - [sendEmailWithTemplate](#sendemailwithtemplate)\n  - [sendBatch](#sendbatch)\n  - [sendBatchWithTemplate](#sendbatchwithtemplate)\n- [Templates](#templates)\n  - [listTemplates](#listtemplates)\n  - [getTemplate](#gettemplate)\n  - [createTemplate](#createtemplate)\n  - [editTemplate](#edittemplate)\n  - [deleteTemplate](#deletetemplate)\n  - [validateTemplate](#validatetemplate)\n- [Messages](#messages)\n  - [searchOutboundMessages](#searchoutboundmessages)\n  - [getMessageDetails](#getmessagedetails)\n- [Diagnostics](#diagnostics)\n  - [diagnoseDelivery](#diagnosedelivery)\n- [Bounces](#bounces)\n  - [searchBounces](#searchbounces)\n  - [getBounceDump](#getbouncedump)\n  - [activateBounce](#activatebounce)\n- [Suppressions](#suppressions)\n  - [listSuppressions](#listsuppressions)\n  - [createSuppressions](#createsuppressions)\n  - [deleteSuppressions](#deletesuppressions)\n- [Stats & Server](#stats--server)\n  - [getDeliveryStats](#getdeliverystats)\n  - [getServerInfo](#getserverinfo)\n- [Webhooks](#webhooks)\n  - [listWebhooks](#listwebhooks)\n  - [createWebhook](#createwebhook)\n  - [deleteWebhook](#deletewebhook)\n\n---\n\n## Email\n\n### sendEmail\nSends a transactional email to one recipient or up to 50 recipients.\n\n**Example Prompt:**\n```\nSend an email using Postmark to recipient@example.com with the subject \"Meeting Reminder\" and the message \"Don't forget our team meeting tomorrow at 2 PM.\"\n```\n\n**Expected Payload:**\n```json\n{\n  \"to\": \"recipient@example.com\",\n  \"subject\": \"Meeting Reminder\",\n  \"textBody\": \"Don't forget our team meeting tomorrow at 2 PM.\",\n  \"htmlBody\": \"<p>Don't forget our team meeting tomorrow at 2 PM.</p>\",\n  \"from\": \"sender@example.com\",\n  \"cc\": \"manager@example.com\",\n  \"bcc\": \"archive@example.com\",\n  \"replyTo\": \"support@example.com\",\n  \"tag\": \"meetings\"\n}\n```\n\n`to` accepts a single address or an array of up to 50 addresses. `htmlBody`, `from`, `cc`, `bcc`, `replyTo`, and `tag` are optional. If `from` is omitted, `DEFAULT_SENDER_EMAIL` is used.\n\n**Response:**\n```\nEmail sent successfully!\nMessageID: 0a1b2c3d-...\nTo: recipient@example.com\nSubject: Meeting Reminder\n```\n\n### sendEmailWithTemplate\nSends an email using a Postmark template.\n\n**Example Prompt:**\n```\nSend the \"welcome\" template to customer@example.com with name \"John Doe\" and login_url \"https://myapp.com/login\".\n```\n\n**Expected Payload:**\n```json\n{\n  \"to\": \"customer@example.com\",\n  \"templateAlias\": \"welcome\",\n  \"templateModel\": {\n    \"name\": \"John Doe\",\n    \"login_url\": \"https://myapp.com/login\"\n  },\n  \"from\": \"sender@example.com\",\n  \"tag\": \"onboarding\"\n}\n```\n\nProvide **either** `templateId` (number) **or** `templateAlias` (string), not both.\n\n**Response:**\n```\nTemplate email sent successfully!\nMessageID: 0a1b2c3d-...\nTo: customer@example.com\nTemplate: welcome\n```\n\n### sendBatch\nSends up to 500 emails in a single API call. Each message is fully independent — its own recipient, subject, and body. This wraps Postmark's *synchronous* batch endpoint (`POST /email/batch`), which returns immediate per-message results.\n\n> **Note:** Postmark also offers an asynchronous [bulk email API](https://postmarkapp.com/developer/api/bulk-email) (`POST /email/bulk`) for large-volume jobs with no message-count cap and a 50 MB payload limit. That endpoint uses a submit-and-poll workflow and is not currently wrapped by this MCP server.\n\n**Expected Payload:**\n```json\n{\n  \"messages\": [\n    {\n      \"to\": \"alice@example.com\",\n      \"subject\": \"Order #1234 confirmed\",\n      \"textBody\": \"Thanks Alice — your order is on its way.\",\n      \"tag\": \"order-confirmation\"\n    },\n    {\n      \"to\": \"bob@example.com\",\n      \"subject\": \"Order #1235 confirmed\",\n      \"textBody\": \"Thanks Bob — your order is on its way.\",\n      \"tag\": \"order-confirmation\"\n    }\n  ]\n}\n```\n\nPer-message fields: `to`, `subject`, `textBody` are required. `htmlBody`, `from`, `cc`, `bcc`, `replyTo`, and `tag` are optional. If `from` is omitted on a message, `DEFAULT_SENDER_EMAIL` is used.\n\n**Response:**\n```\nSent 2/2 successfully\n\nSuccesses:\n  - alice@example.com — abc-123-def\n  - bob@example.com — abc-456-ghi\n```\n\nWhen some messages fail at submission (e.g., suppressed recipients), failures are listed first with their `ErrorCode` and reason:\n```\nSent 8/10 successfully (2 failed)\n\nFailures:\n  - blocked@example.com — 406: Address has been suppressed.\n  - bad@example.com — 300: Inactive recipient\n...\n```\n\n### sendBatchWithTemplate\nSends up to 500 templated emails — same template, per-recipient template models. Ideal for \"render this onboarding template for each new user\" flows.\n\n**Expected Payload:**\n```json\n{\n  \"templateAlias\": \"welcome\",\n  \"from\": \"hello@yourapp.com\",\n  \"tag\": \"onboarding\",\n  \"recipients\": [\n    { \"to\": \"alice@example.com\", \"templateModel\": { \"name\": \"Alice\", \"plan\": \"Pro\" } },\n    { \"to\": \"bob@example.com\", \"templateModel\": { \"name\": \"Bob\", \"plan\": \"Free\" } }\n  ]\n}\n```\n\nProvide **either** `templateId` (number) **or** `templateAlias` (string). Top-level `from` and `tag` apply to all recipients but can be overridden per-recipient. Each recipient also accepts optional `cc`, `bcc`, and `replyTo`.\n\n**Response:** same format as `sendBatch`.\n\n---\n\n## Templates\n\n### listTemplates\nLists saved templates on this server. Returns the first 100 templates; if a server has more than 100, pagination is not yet supported and the response will indicate that results are truncated.\n\n**Response:**\n```\nFound 2 templates:\n\n• **Welcome**\n  - ID: 12345678\n  - Alias: welcome\n  - Subject: Welcome to {{product_name}}\n```\n\n### getTemplate\nRetrieves a single template's full content (HTML body, text body, subject, type).\n\n**Payload:** `{ \"templateIdOrAlias\": \"welcome\" }` — accepts numeric ID or string alias.\n\n### createTemplate\nCreates a new template. Requires `name`. At least one of `htmlBody` or `textBody` must be provided.\n\n`subject` is required for Standard templates and must be **omitted** for Layout templates — Postmark rejects the field on Layouts.\n\n`layoutTemplate` (Standard only) binds the new template to an existing Layout by alias. Without it, the new template renders unwrapped (no chrome from any layout).\n\n**Expected Payload:**\n```json\n{\n  \"name\": \"Order Confirmation\",\n  \"subject\": \"Your order #{{order_id}} is confirmed\",\n  \"htmlBody\": \"<h1>Thanks {{name}}</h1>\",\n  \"textBody\": \"Thanks {{name}}\",\n  \"alias\": \"order-confirmation\",\n  \"templateType\": \"Standard\",\n  \"layoutTemplate\": \"basic\"\n}\n```\n\n`templateType` may be `\"Standard\"` (default) or `\"Layout\"`.\n\n### editTemplate\nUpdates an existing template. Requires `templateIdOrAlias` plus **at least one** updated field (`name`, `subject`, `htmlBody`, `textBody`, `alias`, or `layoutTemplate`).\n\nPass `\"layoutTemplate\": null` to unbind a template from its current Layout (the MCP translates this to the empty-string the Postmark API requires for clearing the association).\n\n### deleteTemplate\nPermanently deletes a template by ID or alias. Layout templates cannot be deleted while Standard templates are still bound to them — unbind via `editTemplate` first.\n\n**Payload:** `{ \"templateIdOrAlias\": \"order-confirmation\" }`\n\n### validateTemplate\nValidates template content (Mustachio syntax, undefined variables) without saving. At least one of `subject`, `htmlBody`, or `textBody` is required.\n\n**Expected Payload:**\n```json\n{\n  \"subject\": \"Order #{{order_id}}\",\n  \"htmlBody\": \"<p>Thanks {{name}}</p>\",\n  \"textBody\": \"Thanks {{name}}\",\n  \"testRenderModel\": { \"order_id\": 42, \"name\": \"John\" },\n  \"templateType\": \"Standard\"\n}\n```\n\n---\n\n## Messages\n\n### searchOutboundMessages\nSearches the outbound message history.\n\n**Expected Payload (all filters optional):**\n```json\n{\n  \"recipient\": \"user@example.com\",\n  \"fromEmail\": \"sender@example.com\",\n  \"tag\": \"marketing\",\n  \"subject\": \"Welcome\",\n  \"status\": \"sent\",\n  \"messageStream\": \"outbound\",\n  \"fromDate\": \"2025-05-01\",\n  \"toDate\": \"2025-05-15\",\n  \"count\": 50,\n  \"offset\": 0\n}\n```\n\n`status` is one of `queued`, `sent`, `processed`. `count` is 1–500 (default 50).\n\n### getMessageDetails\nRetrieves full details and event timeline for a single outbound message.\n\n**Payload:** `{ \"messageId\": \"0a1b2c3d-...\" }`\n\n---\n\n## Diagnostics\n\n### diagnoseDelivery\nComposite triage tool. Answers \"did my email reach X, and if not, why?\" by running message search, suppression check, and bounce history lookups in parallel against a recipient address, then synthesizing a plain-English recommendation.\n\nThis is a **diagnostic** tool: it composes multiple Postmark API calls into a single coherent answer rather than mirroring a single endpoint.\n\n**Example Prompt:**\n```\nDid my email to recipient@example.com get delivered? If not, what should I do?\n```\n\n**Expected Payload:**\n```json\n{\n  \"recipient\": \"recipient@example.com\",\n  \"messageId\": \"0a1b2c3d-...\",\n  \"fromDate\": \"2026-04-21\",\n  \"toDate\": \"2026-04-28\",\n  \"messageStream\": \"outbound\"\n}\n```\n\nAll fields except `recipient` are optional. If `messageId` is omitted, the most recent message to the recipient is used. The default search window is the last 7 days.\n\n**Sample response:**\n```\nDelivery Diagnosis: recipient@example.com\n────────────────────────────────────────────────\n\nSuppression: not suppressed on stream \"outbound\"\n\nMost recent message:\n  MessageID: fadeae4e-fb04-4102-9303-9876078c7b81\n  Subject:   Welcome to MyApp\n  Sent:      2026-04-27T18:42:19.0000000-04:00\n  Status:    Sent\n  Events:    Delivered, Opened×2, Clicked\n\nBounce history: none\n\nRecommended action:\n  Email was delivered. If recipient says they didn't see it, check their\n  spam folder or ask them to whitelist the sender domain.\n```\n\nWhen the recipient is suppressed, the recommendation differs based on reason: `SpamComplaint` is permanent, `HardBounce` may be reactivatable, `ManualSuppression` can be deleted via `deleteSuppressions`.\n\n---\n\n## Bounces\n\n### searchBounces\nSearches the bounce log with optional filters by type, recipient, tag, message ID, message stream, date range, and active/inactive status.\n\n**Expected Payload (all optional):**\n```json\n{\n  \"type\": \"HardBounce\",\n  \"inactive\": true,\n  \"emailFilter\": \"@example.com\",\n  \"tag\": \"marketing\",\n  \"messageID\": \"0a1b2c3d-...\",\n  \"messageStream\": \"outbound\",\n  \"fromDate\": \"2025-05-01\",\n  \"toDate\": \"2025-05-15\",\n  \"count\": 50,\n  \"offset\": 0\n}\n```\n\nSupported `type` values (matches Postmark's `BounceType` enum — 22 values): `AddressChange`, `AutoResponder`, `BadEmailAddress`, `Blocked`, `ChallengeVerification`, `DMARCPolicy`, `DnsError`, `HardBounce`, `InboundError`, `ManuallyDeactivated`, `OpenRelayTest`, `SMTPApiError`, `SoftBounce`, `SpamComplaint`, `SpamNotification`, `Subscribe`, `TemplateRenderingFailed`, `Transient`, `Unconfirmed`, `Unknown`, `Unsubscribe`, `VirusNotification`.\n\n### getBounceDump\nReturns the raw SMTP dump for a bounce. Bounce dumps are retained for 30 days.\n\n**Payload:** `{ \"bounceId\": 123456 }`\n\n### activateBounce\nReactivates a deactivated email address (only bounces where `CanActivate: true`).\n\n**Payload:** `{ \"bounceId\": 123456 }`\n\n---\n\n## Suppressions\n\n### listSuppressions\nLists suppressions for a message stream.\n\n**Expected Payload (all optional):**\n```json\n{\n  \"messageStream\": \"outbound\",\n  \"suppressionReason\": \"HardBounce\",\n  \"origin\": \"Recipient\",\n  \"emailAddress\": \"user@example.com\",\n  \"fromDate\": \"2025-05-01\",\n  \"toDate\": \"2025-05-15\"\n}\n```\n\n`suppressionReason` ∈ `HardBounce`, `SpamComplaint`, `ManualSuppression`. `origin` ∈ `Recipient`, `Customer`, `Admin`. If `messageStream` is omitted, `DEFAULT_MESSAGE_STREAM` is used.\n\n### createSuppressions\nSuppresses up to 50 email addresses on a message stream.\n\n**Payload:** `{ \"emailAddresses\": [\"a@example.com\", \"b@example.com\"], \"messageStream\": \"outbound\" }`\n\n### deleteSuppressions\nRemoves up to 50 addresses from the suppression list. Note: `SpamComplaint` suppressions cannot be deleted.\n\n**Payload:** `{ \"emailAddresses\": [\"a@example.com\"], \"messageStream\": \"outbound\" }`\n\n---\n\n## Stats & Server\n\n### getDeliveryStats\nUnified stats tool. Default behavior returns a friendly headline summary; pass an optional `stat` for a focused breakdown.\n\n**Expected Payload (all optional):**\n```json\n{\n  \"stat\": \"summary\",\n  \"tag\": \"marketing\",\n  \"fromDate\": \"2025-05-01\",\n  \"toDate\": \"2025-05-15\",\n  \"messageStream\": \"outbound\"\n}\n```\n\nSupported `stat` values:\n\n| `stat` | What it returns |\n|---|---|\n| `summary` *(default)* | Headline open / click / bounce / spam rates |\n| `overview` | All overview counts (sent, tracked, opens, clicks, bounces, …) |\n| `sent` | Sent count |\n| `bounces` | Bounce breakdown by type |\n| `spam` | Spam complaint count |\n| `tracked` | Tracked email count |\n| `opens` | Total + unique opens |\n| `openPlatforms` | Open platform breakdown (Desktop / Mobile / WebMail / Unknown) |\n| `openClients` | Top 10 email clients (Apple Mail, Gmail, …) |\n| `openReadTimes` | Read-time histogram |\n| `clicks` | Total + unique link clicks |\n| `clickBrowsers` | Top 10 browsers used to click |\n| `clickPlatforms` | Click platform breakdown (Desktop / Mobile / WebMail / Unknown) |\n| `clickLocation` | HTML vs. plain-text click location |\n\n**Default summary response:**\n```\nEmail Delivery Summary\n\nSent:        74\nTracked:     33  (44.6% of sent)\nOpen rate:   93.9%  (31/33 unique opens)\nClick rate:  4.8%  (10/207 unique links clicked)\nBounced:     1  (1.4%)\nSpam:        0  (0.0%)\n\nPeriod: 2025-05-01 → 2025-05-15\nTag: marketing\n```\n\n**Sample `stat: \"openPlatforms\"` response:**\n```\nOpen Platform Usage\n\n  Desktop        20  (64.5%)\n  Mobile          0  (0.0%)\n  WebMail        11  (35.5%)\n  Unknown         0  (0.0%)\n```\n\n### getServerInfo\nReturns the Postmark server's name, color, tracking settings, and webhook URLs.\n\n**Payload:** `{}`\n\n---\n\n## Webhooks\n\n### listWebhooks\nLists configured webhooks. Optional `messageStream` filter.\n\n### createWebhook\nCreates a webhook subscription. Requires a `url` and **at least one** trigger.\n\n**Security note:** Webhooks are persistent — once registered, Postmark will POST event data (opens, clicks, bounces, spam complaints, etc.) to the target URL for all future matching events on that server, until the webhook is deleted. Only register webhooks pointing to URLs you control. Use `WEBHOOK_URL_ALLOWLIST` to restrict accepted URLs to known prefixes.\n\nThe `url` must use **HTTPS**. HTTP URLs are rejected. If `WEBHOOK_URL_ALLOWLIST` is set, the URL must also match one of the configured prefixes.\n\n**Expected Payload:**\n```json\n{\n  \"url\": \"https://hooks.yourapp.com/postmark\",\n  \"messageStream\": \"outbound\",\n  \"openEnabled\": true,\n  \"clickEnabled\": true,\n  \"deliveryEnabled\": false,\n  \"bounceEnabled\": true,\n  \"spamComplaintEnabled\": true,\n  \"subscriptionChangeEnabled\": false\n}\n```\n\n### deleteWebhook\nDeletes a webhook by ID.\n\n**Payload:** `{ \"webhookId\": 1234567 }`\n\n## Implementation Details\n### API Request Headers\nAll requests to the Postmark API include the following headers for client identification:\n\n| Header | Description |\n|---|---|\n| `X-Postmark-Client` | Always `postmark-mcp` — identifies this server as the request origin |\n| `X-Postmark-Client-Version` | Version of this MCP server, matching the package version |\n| `X-Postmark-MCP-Client` | Name and version of the MCP host application (e.g., `claude-desktop/1.0`), captured from the MCP `initialize` handshake. Omitted if the client does not provide this information. |\n| `X-Agent-Label` | Value of the `AGENT_LABEL` environment variable. Omitted when not set. |\n\nThis server uses its own HTTP client (no postmark npm package) so that MCP traffic is identifiable as `postmark-mcp` in Postmark logs and support tickets.\n\n### Automatic Configuration\nAll emails are automatically configured with:\n- `TrackOpens: true`\n- `TrackLinks: \"HtmlAndText\"`\n- Message stream from `DEFAULT_MESSAGE_STREAM` environment variable\n\n### Error Handling\nThe server implements comprehensive error handling:\n- Validation of all required environment variables\n- Graceful shutdown on SIGTERM and SIGINT\n- Proper error handling for API calls\n- No exposure of sensitive information in logs\n- Consistent error message formatting\n\n### Logging\nEvery tool invocation emits a structured JSON line to **stderr**. If `LOG_FILE` is set, the same line is also appended to that file.\n\n**Log entry shape:**\n```json\n{\n  \"timestamp\": \"2026-06-16T20:34:01.123Z\",\n  \"tool\": \"sendEmail\",\n  \"clientName\": \"claude-desktop\",\n  \"clientVersion\": \"1.0\",\n  \"args\": {\n    \"to\": \"u**r@example.com\",\n    \"subject\": \"Meeting Reminder\",\n    \"textBody\": \"[312ch]\"\n  },\n  \"status\": \"ok\",\n  \"durationMs\": 243\n}\n```\n\nOn error, `status` is `\"error\"` and an `error` field contains the message.\n\n**What is and isn't logged:**\n\n| Data | Logged as |\n|---|---|\n| Email addresses | Partially masked: `u**r@example.com` (set `LOG_EMAIL_FULL=true` to disable) |\n| `htmlBody` / `textBody` | Byte count only: `[312ch]` |\n| `templateModel` and other data objects | Key names only: `{ \"_keys\": [\"name\", \"plan\"] }` |\n| Batch `messages` / `recipients` arrays | Count + recipient list: `{ \"_count\": 2, \"_recipients\": [\"u**r@…\", \"a*b@…\"] }` |\n| Fields matching `password`, `secret`, `token`, `apikey` | `[redacted]` |\n| Tool name, duration, MCP client identity, status | Logged in full |\n\n> Unstructured operational messages (startup, shutdown, API connectivity) continue to write to stderr as plain text alongside the JSON tool logs.\n\n---\n\n## Security Considerations\n\n### Access scope\nThis MCP server acts with the full permissions of the configured `POSTMARK_SERVER_TOKEN`. It exposes 24 tools — including bulk email sends, template management, webhook registration, and suppression list edits — to any MCP client that connects.\n\nPostmark has [two token types](https://postmarkapp.com/developer/api/overview#authentication): a **Server Token** (used here) and an **Account Token**. Neither supports sub-scoped permissions — a Server Token grants full access to all operations on the server it belongs to. The practical way to limit exposure is structural:\n\n- Create a **dedicated Postmark server** used exclusively for MCP traffic. A compromise is then limited to that server's data and settings rather than your entire account.\n- Configure that server with only the message streams and verified sender signatures it actually needs.\n- Rotate the token if it is ever exposed.\n\n### Tool blast radius\nThe 24 tools include several high-impact operations. Below is the breakdown by risk level:\n\n| Category | Tools |\n|---|---|\n| **Destructive** (irreversible) | `editTemplate`, `deleteTemplate`, `deleteWebhook`, `deleteSuppressions` |\n| **Sending** (outbound email) | `sendEmail`, `sendEmailWithTemplate`, `sendBatch`, `sendBatchWithTemplate` |\n| **Additive** (account-state changes) | `createTemplate`, `createSuppressions`, `createWebhook`, `activateBounce` |\n| **Read-only** | All remaining 12 tools |\n\nAll tools carry MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`). MCP clients that respect annotations — including Cursor and Claude Desktop — can auto-approve safe read-only lookups (`readOnlyHint: true`) and will surface confirmation prompts for anything that isn't a read: sends, template edits, suppression changes, and webhook management. Tools that permanently delete data additionally carry `destructiveHint: true` for clients that distinguish destructive from merely mutating actions.\n\n### Webhook URL policy\n`createWebhook` enforces HTTPS on all URLs. Once a webhook is registered, Postmark will POST event data (opens, clicks, bounces, spam complaints) to that URL on an ongoing basis until the webhook is deleted. To prevent a compromised or misdirected tool call from registering a callback you don't control, set `WEBHOOK_URL_ALLOWLIST` to the HTTPS prefixes you own. Audit registered webhooks regularly with `listWebhooks` or via the [Postmark dashboard](https://account.postmarkapp.com).\n\nTo secure your webhook receiver, whitelist [Postmark's published sending IPs](https://postmarkapp.com/support/article/800-what-are-the-postmark-ip-addresses) at the network or firewall level so only Postmark can POST to your endpoint.\n\n### Prompt injection risk\nBecause this MCP server can send email and register webhooks, it is a potential target for [prompt injection](https://owasp.org/www-project-top-10-for-large-language-model-applications/) — where malicious content in an email, template, or repository tricks the AI into invoking a tool with unintended arguments. The mitigations above (dedicated token, tool approval in your MCP client, `WEBHOOK_URL_ALLOWLIST`) reduce the blast radius if this occurs. Never configure auto-approval for sending or destructive tools in untrusted environments.\n\n---\n\n*For more information about the Postmark API, visit [Postmark's Developer Documentation](https://postmarkapp.com/developer).*\n\n## License\n[MIT](LICENSE) © ActiveCampaign","readmeFilename":"README.md"}