NewAn MCP server for coding agents

Lay out email
like a design file.

A visual editor and a compiler built on one small document format. Rows, frames, padding and gaps work the way they do in a design tool, and anything mail clients don't render reliably can't get into your email in the first place.

Free, no sign-up. It runs in your browser and keeps your draft there.

One document compiles to

  • HTML
  • MJML
  • React Email
  • Handlebars
  • Jinja2
  • Liquid
  • Thymeleaf

One document. Everything else reads it.

The editor, the compiler, the lint and the MCP tools all work on an EmailDocument: one tree, validated by a schema generated from a single block registry. The canvas has no model of its own, so the preview and the export are the same bytes.

  1. bodythe root; holds sections
  2. sectiona full-width band
  3. framea row or a stack, nested up to 3 deep; a row holds at most 4 children, each fixed px or a fill %
  4. leafheading, text, image, button, divider, spacer, list, table
  • Content says what, styles say how. Text is stored as runs of text and variables, never as HTML.
  • Order is the array index. There is no position field, and no z-index to go with it.
  • Agents don't write ids. hydrate() stamps them, and they stay put across moves and undo.
welcome.jsonas an agent writes it, with no ids
{
  "version": 2,
  "root": {
    "type": "body",
    "children": [{
      "type": "section",
      "children": [
        { "type": "image",
          "content": {
            "src": "https://cdn.example.com/hero.png", "alt": "" },
          "styles": { "width": 560 } },
        { "type": "heading",
          "content": {
            "level": 1, "runs": [{ "text": "Your order shipped" }] } },
        { "type": "text",
          "content": { "runs": [
            { "text": "Hi " },
            { "var": "user.firstName", "fallback": "there" },
            { "text": ", it's on the way." } ] } },
        { "type": "button",
          "content": { "runs": [{ "text": "Click here" }], "url": "#" },
          "styles": { "borderRadius": "6px" } }
      ]
    }]
  }
}

The syntax your backend already uses.

Target and templating dialect are two options on one call. The dialect is never stored in the document, so the same file can go out as Liquid to one service and Jinja2 to another.

compile(doc, { target: "mjml", variables: "liquid" })

Output is escaped by default, and a fallback fires on null or empty. The Jinja2, Liquid and Thymeleaf output is run through the real engines in CI. A block a target can't render makes the compiler throw; it never quietly emits nothing.

One text run, five outputsuser.firstName, fallback "there"
Handlebars
Hi {{#if user.firstName}}{{user.firstName}}{{else}}there{{/if}}, it's on the way.
Jinja2
Hi {% if user.firstName is defined and user.firstName is not none and user.firstName != '' %}{{ user.firstName|e }}{% else %}there{% endif %}, it's on the way.
Liquid
Hi {% if user.firstName != nil and user.firstName != '' %}{{ user.firstName | escape }}{% else %}there{% endif %}, it's on the way.
Thymeleaf
Hi <span th:text="${#strings.isEmpty(user.firstName) ? 'there' : user.firstName}">there</span>, it's on the way.
React Email
Hi {isEmpty(user?.firstName) ? "there" : user?.firstName}, it's on the way.
Terminalrun on welcome.json above
$ heddlemail check welcome.json
warning image-alt              root.children.0.children.0
        The image has no alt text. Alt text is what shows when images are blocked (Outlook's default) and what screen readers read.
        hint: Describe the image in content.alt, or set content.decorative: true if it carries no meaning.
info    hydrated               document
        Stamped block ids and filled defaults on an agent-written document.
        hint: Keep the returned document (with ids) for follow-up calls so blocks keep their identity.
info    style-tier             root.children.0.children.3
        borderRadius is progressive (border-radius): corners render square.
        hint: acceptable degradation, or emit a VML roundrect fallback.
info    link-placeholder       root.children.0.children.3
        The button still points at a placeholder (#).
        hint: Set the real destination URL.
info    link-text              root.children.0.children.3
        Button label "Click here" does not say what happens.
        hint: Use an action, e.g. "Track your parcel".
0 error(s), 1 warning(s)

It tells you what a client will do with it.

Every style key carries a safety tier, and lint reports each problem at a path, with a hint specific enough for an agent to act on.

safe
Renders the same in every client that matters. Padding, colour, type, borders.
progressive
Degrades acceptably. Rounded corners go square in Outlook's Word engine; dark-mode colours are ignored where unsupported.
restricted
Some client loses content. Allowed for people, with a warning; left out of the schema agents write against.

Your coding agent writes the email. These tools keep it valid.

heddlemail-mcp is a local stdio server: no network, no keys, nothing generated on our side. The agent you already use produces the document, and five tools check, repair and compile it.

The same functions ship as the heddlemail CLI and the heddlemail-check GitHub Action, for CI. Not on npm yet. For now, build it from the repo with pnpm -F heddlemail-mcp build. Setup and tool reference →

  • describe_blocksThe contract: grammar and limits, every block's fields and style keys with their tiers, bindings, targets, dialects and lint codes.
  • check_irValidates and lints. Stamps ids on an agent's draft and returns the canonical document with path-addressed issues.
  • repair_irFixes what has exactly one sensible fix (stray blocks, over-deep frames, foreign keys) and reports every change. Never invents content.
  • compile_irHTML, MJML or React Email in any dialect. Byte-identical to the package's compile().
  • preview_irThe exact HTML a recipient gets, with its size in bytes against Gmail's ~102 KB clipping limit.
.mcp.jsonfrom a clone of the repo
{
  "mcpServers": {
    "heddlemail": {
      "command": "node",
      "args": ["packages/mcp-server/dist/server.js"]
    }
  }
}

Not built yet

Everything above has shipped. These are next, in this order. Sending isn't on the list: Heddlemail hands templates to your ESP or backend and doesn't operate a mail server.

  1. Phase FRendering checks in real browser engines, and an Outlook-engine report
  2. Phase GSaved templates, workspaces, and sessions an agent and the editor can share
  3. Phase HImport from HTML, MJML and Figma, with a warning for everything that doesn't map
The full roadmap and its gates →