Utskick MCP Server
Uses Amazon SES to deliver newsletter and test emails, and Amazon SNS to receive bounce and spam-complaint notifications, with signature verification and suppression handling.
Imports Mailchimp audience exports, including subscribers, opt-in times, unsubscribed and cleaned addresses as suppressions, and supports Mailchimp merge tags.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
Utskick
Utskick (Swedish for 'a mailing') is an open source newsletter sender on Cloudflare Workers and Amazon SES.
You keep the list, the templates and the numbers. Amazon SES only delivers the mail. It was built to replace Mailchimp for Sourceful Energy's newsletter, reads Mailchimp's exports and merge tags, and costs a few kronor a month to run.
What it does
Sends an HTML issue to a list, one mail per recipient, with the merge tags
*|FNAME|*,*|UNSUB|*and*|ARCHIVE|*. A send that stops halfway resumes without mailing anyone twice.One-click unsubscribe (RFC 8058): the
List-Unsubscribeheaders that Gmail, Yahoo and Apple Mail act on, plus the footer link.Counts clicks per link, and opens through an optional pixel per list, which feeds a resend to those who did not open.
Bounces and spam complaints arrive from SES through SNS; each message's signature is checked, and the address is suppressed.
Imports a Mailchimp audience export: subscribers with their opt-in time, unsubscribed and cleaned addresses as suppressions.
Sign-ups from your own sites: a browser form, a server route with a key, or a Cloudflare Worker over a service binding (docs/signup.md).
A dashboard for admins with lists, sends, rates, clicks per link and the audit log, and no personal data (docs/dashboard.md).
An MCP server for assistants (Claude, ChatGPT, Grok, Cursor, Codex …) with OAuth 2.1. An assistant can read stats, write drafts and send tests to the team; it can only ask to send to a list, and a person approves each send (docs/mcp.md).
A web archive page per issue, without names or tracking.
Related MCP server: FirstTouch MCP
Architecture
issue HTML ──► CLI ──/admin──► Worker ──SendEmail──► Amazon SES ──► inbox
▲ │ │
readers: /u /c /o /a ─────────┤ ├── D1 (lists, sends, │
sites: /subscribe, RPC ─────┤ │ events, OAuth) └─ SNS ──► /hooks/ses
assistants: /mcp + OAuth ─────┤ └── R2 (images)
admins: /dashboard, /approve ┘ cron: sends what a person approvedWorker (
src/): one Cloudflare Worker. It keeps contacts, lists, sends, deliveries, events and OAuth state in D1, sends through the SES v2 API, and serves everything readers, sites, assistants and admins reach.CLI (
cli/utskick.ts): Node 24 or later, no build step. It imports exports and drives sends through the Worker's/adminAPI. It never talks to SES itself.Config (
config/*.json): your organization, lists, senders, forms, admins, and where the installation runs. Code holds nothing specific to one installation.
How a send works
send … --dry-runcounts the audience (subscribers of the list, minus suppressed addresses), lists the links it will track, and writes a sample mail to.dev/.A live send needs
--confirm <count>, the number the dry run printed. Without it nothing goes out.Creating a send writes one delivery row per recipient, each with a random token. Creating it again with the same content does nothing; other content under the same key is refused.
The CLI asks the Worker for batches of up to 14 mails (SES's default rate), one batch a second. Each row is claimed before SES is called and settled after, so a stopped send resumes where it stopped (
resume <key>). When SES's answer is lost or SES answers with a server error, the row becomesunknownand is not retried on its own ("rather none than two");retry-unknownsends those again. An address SES refuses isfailed(retry-failedputs those back in line). An account or settings error (access denied, sending paused, an unverified domain) stops the send with the rest pending: fix the cause, thenresume. After the last batch the CLI says if any rows are still sending, unknown or failed, and what to run.Unsubscribes, bounces and complaints are checked again at send time.
Getting started
npm install
cp .dev.vars.example .dev.vars && cp .env.example .env
npm run dev # local Worker on :5173 with config/example.json, local D1 and R2
npm run ses-mock # in another terminal: SES stand-in on :4566, mail lands in .dev/outbox/
npm run db:local # create the tables in the local D1
npm test # node:test; D1 runs on node:sqlite, SES is fakedIn development mode links point at localhost, SES calls go to the mock, and sign-in is a stand-in admin that works only on a local host, so a local run cannot reach a real inbox.
Then, against the local Worker:
node cli/utskick.ts import <mailchimp-export-dir> --list newsletter
node cli/utskick.ts send issue.html --list newsletter --key newsletter-1 --subject "…" --dry-run
node cli/utskick.ts send issue.html --list newsletter --key newsletter-1 --subject "…" --test you@example.com --fname You
node cli/utskick.ts send issue.html --list newsletter --key newsletter-1 --subject "…" --confirm <count>
node cli/utskick.ts resend issue.html --list newsletter --parent newsletter-1 --subject "…" --after-days 3 --confirm <count>
node cli/utskick.ts stats newsletter-1
node cli/utskick.ts resume newsletter-1 # carry on after a stop
node cli/utskick.ts retry-unknown newsletter-1 # send again what SES may not have got
node cli/utskick.ts retry-failed newsletter-1 # send again what SES refused--fname is used in that test mail only; it never changes a contact. A resend to those who did not open needs the list's open pixel and is refused without it.
Images next to the issue HTML (src="hero.jpg") are uploaded to R2 and served from /img/<sha256>.jpg.
Your own installation
Copy
config/example.jsontoconfig/<you>.jsonand fill it in: thedeploysection (public URL, AWS region, D1, R2), your organization, lists, forms and admins.Follow docs/setup.md for SES, DNS, SNS, IAM, D1, R2, Cloudflare Access and the secrets.
Deploy with
UTSKICK_CONFIG=config/<you>.json npx cf deploy. WithoutUTSKICK_CONFIGa deploy stops, so nobody ships mail whose links point at someone else's Worker.
More: docs/signup.md (forms), docs/dashboard.md, docs/mcp.md (assistants), docs/examples/ (connecting a Cloudflare Pages site).
Security model
Admin API.
/admintakes a bearer token, compared in constant time; changing calls are written to the audit log. The token, the AWS key, the SNS topic ARN and the form keys are Worker secrets, never files in the repository. Consider putting/adminbehind a Cloudflare Access service token as well.AWS key. An IAM user allowed
ses:SendEmailon one identity and one configuration set, and nothing else.SNS webhook. Nothing in a message is used before its signature checks out: SignatureVersion 1 or 2, the signing certificate fetched only from
sns.<the topic's region>.amazonaws.comwith theSimpleNotificationService-…pempath, and only the configured topic accepted. That includes a subscription confirmation'sSubscribeURL.Reader tokens. Each delivery has its own 128-bit random token, used in its unsubscribe, click and open URLs. A token identifies one mail, not a person, and the API never returns one. Like any newsletter footer link, a forwarded mail lets its new reader unsubscribe the original one.
Unsubscribe. A GET only shows a button, because mail scanners open links. A POST, the one RFC 8058 clients send, unsubscribes. In a test mail the POST is logged and takes nobody off a list.
Bounces. A hard bounce or a spam complaint suppresses the address on every list. A soft bounce does so when a second mail bounces.
Sign-ups. Browser forms are allowed by exact origin (CORS), server routes by a per-form key, sites on Cloudflare over a service binding. Each list's consent id must match the config, a honeypot field catches form bots, and requests are rate-limited per client (per /64 for IPv6). The answer is the same for a new, known or suppressed address. Without double opt-in, a sign-up never lifts a suppression and never changes an existing contact.
Assistants (MCP). OAuth 2.1 with PKCE S256 only, tokens bound to the
/mcpresource, rotating refresh tokens, revocation per client, and least-privilege scopes (stats.read,subscribers.read,drafts.write,test.send). Sending to a list is never a scope: a person approves each send on a page that shows the list, the subject, the count, the text part, every link and anything hidden; the approval works once, for ten minutes. Every tool call is audited. Tokens, codes and secrets are stored only as hashes.Admins. Sign-in for the dashboard, the consent page and approvals goes through Cloudflare Access; Utskick checks the Access JWT itself and allows only the addresses in
admins. The development stand-in works only when both the configured and the requested host are local.Pages. Every reader and admin page is server-rendered without scripts. The dashboard and approval pages carry a strict Content-Security-Policy and cannot be framed; the web archive is served in a CSP sandbox.
Limits on abuse. At most one open per mail, and one click per mail and link, are logged an hour; request bodies are read only up to a small limit.
Local stand-ins stay local.
SES_ENDPOINT(the SES mock) is ignored unless the public URL is a local host, and the CLI refuses to send the admin token over plain HTTP to anything but localhost.
Report a vulnerability as described in SECURITY.md.
Privacy
Leaving a list deletes the address from it, and the contact once it is on no list. A SHA-256 hash of the address stays on the suppression list, so an old import cannot sign the person up again; it is a pseudonym, not anonymous.
Opens and clicks are stored as "delivery N, link M, at time T", without IP address or user agent. Lists without
opensget no pixel. In the EU, an open pixel may need consent (EDPB Guidelines 2/2023); say so where people sign up, or turn it off.Pages, logs, stats, the dashboard and the audit log show no address or name.
D1 and R2 can be created in Cloudflare's EU jurisdiction, and SES runs in the AWS region you choose.
Limits
One SES
SendEmailcall per recipient, at up to 14 a second: about 2.5 minutes for 2,000 recipients.No double opt-in yet (docs/signup.md describes the plan). Until then, someone who left a list cannot rejoin through a form.
Issues are HTML. Mailchimp's conditional merge tags (
*|IF:…|*) are not supported.
License
GNU Affero General Public License v3.0. Copyright Sourceful Labs AB and contributors. If you run a modified Utskick for others, the AGPL asks you to offer them its source.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read lists, contacts, campaign stats; add contacts; draft, schedule and send campaigns and SMS.
Inspect outreach delivery, campaigns, leads and pipeline; prepare sequence drafts. Read-only.
Read audiences, members, campaigns and reports; add, update, tag and archive subscribers.
Manage DialMyCalls contacts and groups; read broadcasts; send text and voice broadcasts on approval.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceEnables assistants to read account data (tunnels, usage, metrics, tokens, invoices, domains, notifications, cloud apps) and start/stop tunnels via the tunnels.io API and CLI.MIT

FirstTouch MCPofficial
AlicenseNot gradedqualityDmaintenanceConnects AI assistants to your workspace so you can build audiences from connected HubSpot lists, author and publish outreach flows, enroll selected contacts, and review team tasks in natural language. Every email and call action requires human approval before it executes.Apache 2.0
Arsel MCP Serverofficial
AlicenseBqualityBmaintenanceEnables AI agents to read and manage contacts, lists, tags, events, templates, campaigns and message logs in an Arsel organization, and to draft email, SMS, push and in-app campaigns for review. It is read-and-draft only, so agents can create and update records but cannot send, schedule or delete anything.5947 npmMIT- AlicenseNot gradedqualityBmaintenanceEnables users to search, read, and create ecommerce email marketing assets such as campaigns, automations, popups, and landing pages, manage sending-domain setup, and with confirmation schedule/send campaigns or publish/pause assets.MIT