πŸ€– Teach your AI coding agent how to send email with Postmark Skills
x
The Postmark MCP server, one year later: from 4 tools to 24 | Postmark

The Postmark MCP server, one year later: from 4 tools to 24

About a year ago, we introduced something experimental from Postmark Labs: an MCP server that let an AI assistant send email through Postmark. It shipped with exactly one useful tool, sendEmail, plus three supporting ones (four total). You gave it a recipient, a subject, and a body, and it sent. We said at the time we'd started with a single Postmark server "because we had to start somewhere."

A lot has happened since. The project graduated from Labs and became the official @activecampaign/postmark-mcp package, and along the way, we learned a lot about what it actually takes to put an AI assistant in front of a production email API responsibly. The latest release, v2.1.1, ships 24 tools across eight categories: sending, templates, message search, delivery diagnostics, bounces, suppressions, stats, and webhooks. But the tool count isn't really the story. The story is what a year of running this thing taught us: designing tools for an AI agent is a different discipline than wrapping an API, and once your server has an official name, everything downstream of that name needs to earn it.

From one endpoint to a whole surface #

The original four tools mirrored individual Postmark endpoints directly. Getting from there to 24 the naive way β€” one tool per endpoint, mechanically β€” is where we started. But somewhere around tool ten, we realized that users interacting with an AI assistant don't conceptualize their needs as API calls. They approach it with human inquiries. That reframing shaped everything that mattered about the v2.0 release.

Designing for outcomes, not endpoints #

The best example of that shift doesn't map to a single Postmark endpoint at all. It's called diagnoseDelivery, and it exists to answer one very human question: did my email actually reach this person, and if not, why?

Before, answering that meant a five-step investigation across the dashboard: search outbound messages, pull the message details, read the event timeline, check the suppression list, check the bounce log β€” then assemble a story out of five separate answers. That's a reasonable workflow for a person with a browser tab open. It's a bad one to make an AI agent orchestrate call by call, guessing at the next step each time.

So diagnoseDelivery does it in one call. It fans those lookups out in parallel, tolerates any single one failing, and returns a synthesized answer:

Delivery Diagnosis: recipient@example.com
────────────────────────────────────────────────

Suppression: not suppressed on stream "outbound"

Most recent message:
  MessageID: fadeae4e-fb04-4102-9303-9876078c7b81
  Subject:   Welcome to MyApp
  Sent:      2026-04-27T18:42:19.0000000-04:00
  Status:    Sent
  Events:    Delivered, OpenedΓ—2, Clicked

Bounce history: none

Recommended action:
  Email was delivered. If recipient says they didn't see it, check their
  spam folder or ask them to whitelist the sender domain.

The Recommended action line is the point. The tool doesn't hand back five payloads and leave the model to infer what they mean together β€” it hands back a judgment, and the judgment changes based on why a recipient is suppressed (a spam complaint is permanent; a hard bounce may be reactivatable; a manual suppression can just be removed). That's the pattern we kept coming back to across the whole surface: collapse the multi-step human workflow into one outcome-shaped call, rather than exposing the steps and hoping the model reconstructs the workflow correctly every time.

Being identifiable, on purpose #

Here's a decision that looks backwards until you know why: starting in v2.0.0, none of the 24 tools use Postmark's official Node SDK anymore. Every one of them goes through a single small HTTP client built on native fetch.

Three reasons drove that, in order of how much they mattered:

  1. Identity. We wanted MCP-driven traffic to be unmistakably attributable to this server in Postmark's own logs β€” not blended in as anonymous SDK calls, indistinguishable from any other integration. v2.1.0 pushed this further: the server now captures the connecting MCP client's own name and version during the handshake and carries that identity through to every outbound request and log line, alongside an optional operator-supplied label (AGENT_LABEL) for teams running multiple instances who want to tell them apart in their own traffic.
  2. Control. One hardened client means one place that owns auth headers, a request timeout, and consistent error mapping, so a Postmark ErrorCode surfaces the same way no matter which of the 24 tools triggered it.
  3. Fewer moving parts. Dropping the SDK, and a second HTTP dependency alongside it, left the runtime with three dependencies total: the MCP SDK itself, dotenv, and zod, all pinned to exact versions to keep the supply chain narrow and auditable.

The tradeoff is real β€” we gave up the SDK's convenience and now track Postmark's API shape ourselves. For a server whose entire job is to be a clean, observable bridge between an AI agent and an email provider, that tradeoff was worth it.

Make misuse fail loudly, and make risk visible #

AI agents make a specific kind of mistake: they call a tool confidently with not-quite-enough information. So the mutating tools validate before they touch the API. createWebhook won't run without at least one trigger enabled, and β€” since v2.1.0 β€” won't accept a non-HTTPS URL at all; an optional allowlist lets teams restrict it further to their own domains (see our webhook setup guide for how to configure it). editTemplate requires at least one field to actually change. sendEmailWithTemplate rejects a call that supplies both templateId and templateAlias instead of exactly one.

None of that is clever. It's the difference between a tool that fails with a sentence the model can recover from, and one that silently no-ops or burns an API call for nothing. When your caller is a probabilistic system, fail-fast isn't a nicety β€” it's part of the interface contract.

v2.1.0 extended that same instinct to the client side of the interface: all 24 tools are now annotated with readOnlyHint, destructiveHint, and idempotentHint, so any MCP client can show a risk indicator or gate a destructive call before it runs β€” a delete, a webhook creation, a real send β€” without having to guess from the tool's name what it's about to do. And logs are PII-reduced by default now: email addresses get masked to their first and last character before anything is written, with a LOG_EMAIL_FULL escape hatch for teams that explicitly want full addresses in their own logs.

First major release #

The least glamorous change, and maybe the most important one: we started treating this like software other people depend on.

v2.0.0 was a proper semver major, because it was a breaking one β€” the Node floor moved from 16 to 20. v2.1.0 added a 52-test automated suite across three tiers β€” unit tests for logging and masking, offline validation of webhook and annotation behavior, and end-to-end checks of the env-var wiring β€” on top of the two smoke-test harnesses that already exercised every read-only tool and ran full create-edit-delete lifecycles, including real sends that clean up after themselves, against a live account before anything shipped. v2.1.1 added a prepublishOnly gate so a broken build literally can't reach npm. Small, unglamorous, exactly the kind of thing that turns "a project" into "a package with an owner."

A year of perspective #

The four-tool version answered "can you do this?" The 24-tool version, three releases later, answers a better question: "what should this actually be?" The biggest change isn't the tool count β€” it's that we stopped thinking about wrapping endpoints and started thinking about how an agent experiences a tool: the questions it asks, the mistakes it's prone to, the judgments it wants handed to it instead of assembled itself.

If you want to look under the hood, the project lives on GitHub and ships to npm as @activecampaign/postmark-mcp.

We'd love to hear how you're using it, and what you think the next 24 tools should look like. Drop a note through our feedback form or find us at @postmarkapp.

Jabal Torres