Generated from Postwind commit a19b2e4 OpenAPI 0.1.0 synced

MCP

Postwind is an MCP server. It serves Streamable HTTP at /mcp on the API listener, with the same Authorization: Bearer pw_… keys and scopes as the API. There are no sessions: every request is served on its own, so any api+mcp node answers any request. Each tool wraps exactly one API operation and is named after its operationId.

Connecting

URL: http://<api-host>:8080/mcp (the path is mcp.path, default /mcp). Header: Authorization: Bearer pw_…. stdio is not supported: the server is remote and keys are mandatory. Both client lifecycles are supported: initialize (protocol versions up to 2025-11-25) and server/discover (2026-07-28).

  • The API listener speaks plain HTTP and expects a TLS terminator in front of it. Behind one, the URL is https://<host>/mcp; the examples below use https://mail.example.com/mcp.
  • The server issues no Mcp-Session-Id. Every POST answers application/json, so a load balancer needs no sticky sessions.
  • The key is checked on every HTTP request to the path, before anything else. A bad key gets the API’s 401 problem+json, byte for byte.
  • Then the Host header is checked against mcp.allowed_hosts. The default is ["localhost", "127.0.0.1", "::1"], and a host outside the list gets 403. A remote client works only after the operator adds the node’s public host name. An entry without a port allows any port; [] turns the check off.
  • With mcp.enabled = false the path is not mounted and answers 404.

Client configuration

Use a project key with the scopes the agent needs, not an admin key, unless the agent has to manage projects. A key string in a config file is readable by anyone who can read the file.

Claude Code

claude mcp add --transport http postwind https://mail.example.com/mcp \
  --header "Authorization: Bearer pw_prj_…"

Add --scope user to use the server in every project, or --scope project to write it to the project’s .mcp.json. Run /mcp in Claude Code to check the connection.

Cursor

In .cursor/mcp.json of the project, or ~/.cursor/mcp.json for every project:

{
  "mcpServers": {
    "postwind": {
      "url": "https://mail.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${env:POSTWIND_KEY}"
      }
    }
  }
}

Cursor reads ${env:POSTWIND_KEY} from the environment, so the key string stays out of the file.

Claude Desktop

Custom connectors take a remote server by URL. A static Authorization header is set in the Request headers section of the Add custom connector dialog, with No sign-in as the authentication and Bearer pw_… as the value. That section is in beta and only some organizations have it. Claude then connects to the node itself, so the node’s MCP URL must be reachable by Claude, not only by your computer.

Without that section, use the mcp-remote bridge in claude_desktop_config.json. mcp-remote is a community bridge: a local stdio process, run with npx, that forwards to a remote server. It is not part of Postwind or of Claude Desktop.

{
  "mcpServers": {
    "postwind": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "https://mail.example.com/mcp",
        "--transport", "http-only",
        "--header", "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "Bearer pw_prj_…"
      }
    }
  }
}

The header value is in env because some clients do not escape spaces inside args. --transport http-only skips the SSE fallback, which Postwind does not serve.

Other clients

Any MCP client that speaks Streamable HTTP and can send a static header works: give it the URL and Authorization: Bearer pw_….

First call

Call whoami with no arguments. Your client sends initialize or server/discover first, then:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": { "name": "whoami", "arguments": {} }
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{ "type": "text", "text": "<the same JSON as structuredContent>" }],
    "structuredContent": {
      "key_id": "01900000-0000-7000-8000-000000000001",
      "name": "ops",
      "kind": "project",
      "project_id": "<project_id>",
      "scopes": ["read", "write"],
      "expires_at": null
    },
    "isError": false
  }
}

The answer tells the key’s kind, its project, its scopes and its expiry: the same body as GET /api/v1/whoami. Then get_server_info returns the node’s version, enabled subservices and transports.

Keys and project context

A project key works in its own project; the project_id argument is ignored. An admin key must pass project_id to project tools.

  • Project tools are the tools of operations under /api/v1/projects/{project_id}/…. Their project_id argument is optional in the schema.
  • With a project key the argument is ignored entirely, even when it is not a UUID.
  • With an admin key and no argument the call fails with validation_failed, field project_id, code required. A value that is not a UUID is not_found.
  • get_project, update_project_settings and delete_project require project_id from every key and check it as the API checks its path: another project is not_found.

After that the checks are those of the API; see Authentication and scopes.

Tool rules

Every description answers three questions in order: Call it … (when), Needs … (key, scope, state), Returns … (what comes back). Arguments are strict: an unknown argument is validation_failed. Output is structuredContent, field for field the body of the API operation, plus a text block with the same JSON.

The full rules, with the error shape and the limits, are on Tool rules.

Confirm and dry run

launch_campaign, cancel_campaign, revoke_api_key, revoke_admin_key, delete_project, delete_list, delete_sending_domain, erase_subscriber, add_suppression and remove_suppression need confirm: true. Without it the tool checks everything the real call checks, says what would happen (outcome, effect) and writes nothing.

confirm defaults to false, so a call that leaves it out is always a dry run. Run the dry run first, show its effect to a person when it matters, then call again with confirm: true.

Errors

isError: true with code and detail from the same table as the API, plus errors for validation_failed and precondition_failed.

The codes are on the API errors page. The rate limit (429) and the body limit (413) are answered at the HTTP level, as in the API; they are not tool errors.

Limits

Lists return up to 100 items (limit 1–100, default 50) with the same cursor as the API. import_subscribers takes up to 1 000 records per call; larger imports go through the API. There is no Idempotency-Key: a tool that got no answer is checked with the matching list_* tool before it is called again.

Requests share the key’s rate limit with the API, and every JSON-RPC message counts. See Rate limits.

Resources

  • postwind://docs/templates — template syntax and variables
  • postwind://docs/deliverability — the deliverability checklist
  • postwind://docs/errors — the error codes

The resources are built into the binary.

Guides

Tools

57 tools, one per API operation; names equal operationId.

Server The same group in the HTTP API

ToolConfirmProjectWhat it does
get_server_info—Describes the Postwind node that answered.
whoami—Tells which API key this connection uses.

Projects The same group in the HTTP API

ToolConfirmProjectWhat it does
list_projects—Lists projects, oldest first, at most 100 per call.
create_project—Creates a project, the unit of isolation: its own keys, domains, lists, templates and campaigns.
get_projectrequiredReads one project with all its settings.
update_project_settingsrequiredRenames a project and changes its settings: sender, transport, tracking, footer, branding of the public pages, quotas.
delete_projectconfirmrequiredDeletes a project: it refuses every change from then on, admin keys can still read it, and its slug becomes free.

Keys The same group in the HTTP API

ToolConfirmProjectWhat it does
list_admin_keys—Lists the admin keys in id order, at most 100 per call, the bootstrap key and revoked and expired ones included.
create_admin_key—Creates an admin key, which may do everything: projects, their keys, admin keys.
revoke_admin_keyconfirm—Revokes an admin key for good, this connection’s own included.
list_api_keysoptionalLists the API keys of a project in id order, at most 100 per call, revoked and expired ones included.
create_api_keyoptionalCreates an API key of a project with the scopes it needs: read, write, send, keys.
revoke_api_keyconfirmoptionalRevokes an API key of a project for good: every request with it is refused from then on.

Lists The same group in the HTTP API

ToolConfirmProjectWhat it does
list_listsoptionalLists the mailing lists of a project, oldest first, at most 100 per call.
create_listoptionalCreates a mailing list in a project, the audience campaigns go to.
get_listoptionalReads one mailing list: its name, double opt-in, confirmation template and sender.
update_listoptionalRenames a mailing list and changes its options: double opt-in, the confirmation template, the sender of its own letters.
delete_listconfirmoptionalDeletes a mailing list: it stays readable, takes no change from then on and gets no new subscribers; its subscribers are kept.

Subscribers The same group in the HTTP API

ToolConfirmProjectWhat it does
list_subscribersoptionalLists a list’s subscribers, ordered by id, at most 100 per call, with an optional status filter.
add_subscriberoptionalAdds a subscriber to a list, or merges attributes into one already on it.
find_subscriberoptionalFinds a subscriber by its email on a list.
import_subscribersoptionalAdds a batch of up to 1 000 subscribers to a list in one call and reports record by record.
get_subscriberoptionalReads one subscriber by its id, whatever list it is on: its email, attributes and status.
update_subscriber_attributesoptionalChanges a subscriber’s attributes, merging the ones you give into the ones it has (new values win, null removes a key); the status is never changed.
erase_subscriberconfirmoptionalErases a subscriber for good, on the person’s request to delete their data: its row (address, attributes, status) is deleted, and in its events and the…
get_subscriber_eventsoptionalLists a subscriber’s history: the events about it (subscriber.created, subscriber.unsubscribed, subscriber.resubscribed, …) since it was added, oldest…
reactivate_subscriberoptionalTakes a bounced or complained subscriber back to active.
resubscribe_subscriberoptionalSubscribes an unsubscribed subscriber again, with fresh consent: active, or pending on a list with double opt-in; the suppression its unsubscribe left is…
unsubscribe_subscriberoptionalUnsubscribes a subscriber: from its list, or (scope: "project") the address from every list of the project.

Suppressions The same group in the HTTP API

ToolConfirmProjectWhat it does
list_suppressionsoptionalLists the project’s suppressions by address, then scope, at most 100 per call, or those of one address (email).
add_suppressionconfirmoptionalSuppresses an address by hand (reason manual) for the project or one of its lists: it gets no mail of that scope; subscriber statuses are not changed.
remove_suppressionconfirmoptionalRemoves an address’s suppressions: every one, or only that of scope.

Domains The same group in the HTTP API

ToolConfirmProjectWhat it does
list_sending_domainsoptionalLists the sending domains of a project in creation order, at most 100 per call.
add_sending_domainoptionalAdds a sending domain to a project and generates its DKIM key pair (RSA 2048; the private key is stored sealed and no call returns it).
get_sending_domainoptionalReads one sending domain of a project.
update_sending_domainoptionalChanges a sending domain’s ip_pool, tracking_host or dns_provider.
delete_sending_domainconfirmoptionalDeletes a sending domain for good: no read finds it again, its DKIM key is destroyed, and its name is free to add again (with a new key); its records stay in…
apply_dns_recordsoptionalApplies a sending domain’s required DNS records through its provider.
get_required_dns_recordsoptionalLists the DNS records a sending domain needs, for entry by hand at any registrar.
verify_sending_domainoptionalChecks a sending domain’s DNS records through a resolver now and records the outcome.

Templates The same group in the HTTP API

ToolConfirmProjectWhat it does
list_templatesoptionalLists the templates of a project in id order, each by its latest version and without its sources, at most 100 per call.
create_templateoptionalCreates a template, its version 1: a message (kind message, the default: a subject, an HTML body and an optional text body) or a partial (kind partial: a…
get_templateoptionalReads the latest version of a template, with its sources.
update_templateoptionalSaves a new version of a template: its latest version’s number plus one, with the sources given; the name and kind stay.
preview_templateoptionalRenders a message template as a subscriber would get it, before anything is sent: the subject, the HTML and the text, with warnings.
send_test_emailoptionalSends a message template as a test to a few addresses through the project’s transport: rendered as preview_template renders it with sample_attributes…
get_template_versionoptionalReads one version of a template, with its sources.

Campaigns The same group in the HTTP API

ToolConfirmProjectWhat it does
list_campaignsoptionalLists the campaigns of a project, oldest first, at most 100 per call, optionally of one status.
create_campaignoptionalCreates a campaign in a project, a draft: one message template sent to one or more of its lists.
get_campaignoptionalReads one campaign: its status, lists, template, sender and the other fields, and what its launch fixed.
update_campaignoptionalChanges a campaign’s fields: name, lists, template, sender, reply-to, subject, transport, tracking, throttle.
cancel_campaignconfirmoptionalCancels a scheduled, sending or paused campaign for good: its letters not sent yet are cancelled; letters already sent stay sent.
launch_campaignconfirmoptionalLaunches a draft campaign: its recipients’ messages are queued and sending starts.
pause_campaignoptionalPauses a sending campaign: no new letter of it goes out until resume_campaign; the letters under way finish.
preflight_campaignoptionalShows what launching a draft campaign would do now: its recipients after deduplication and suppression, its sender’s sending domain with its warnings, its…
resume_campaignoptionalResumes a paused campaign: it is sending again and its remaining letters go out.
get_campaign_statsoptionalReads where a campaign’s sending is: its status, how many recipients its launch wrote, and its deliveries by status (queued, deferred, sent, bounced…