NewZapp MCP server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@NewZapp MCP serverHow did last week's staff newsletter do: opens, clicks, unsubscribes?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
NewZapp MCP server
An MCP server that lets Claude, ChatGPT and other MCP clients work with a NewZapp email marketing and internal communications account: the account and its licence, campaign reports, open heatmaps, contact groups, contact counts and a contact search that withholds personal data by default, and (when enabled) creating and updating contacts. It is built from NewZapp's public API documentation only: the OpenAPI 3.0.1 document "NewZapp API" v3 at https://my.newzapp.co.uk/swagger/v3/swagger.json, which is what the ReDoc page at https://my.newzapp.co.uk/api-docs renders.
Once it's connected, a comms team can ask things like:
"How did last week's staff newsletter do: opens, clicks, unsubscribes?"
"Which of our September campaigns had the best click-to-open rate?"
"When do people actually open our emails? Show me the busiest hours."
"How many contacts in the Managers group have unsubscribed or bounced?"
"How many people opened the newsletter but didn't click?"
With writes enabled: "Add our new starter Nia Jones to All staff, with Department set to Parks."
Tools
Tool | What it does | API calls |
| Account name and company, licence (product, subscriber and user limits, internal comms flag), double opt-in setting, and the custom contact fields defined on the account. The SMTP username is never returned. |
|
| Campaign reports: name, subject, status, send time, groups and topics, recipients selected and sent, unique and total opens and clicks, bounces, failures, unsubscribes, feedback, channel totals and read times, with open, click and click-to-open rates computed from those counts. Passes |
|
| One campaign as NewZapp returns it. The spec gives this response no schema, so the record is passed through an allowlist (see Safety defaults): numbers, true/false values and top-level text by default. For the documented report fields, formatted, use |
|
| NewZapp's "summary of recipient action data" for one campaign, optionally for one group ( |
|
| Opens per hour of the day and day of the week, for one campaign ( |
|
| Contact groups with their contact and suppressed counts, type, tags, automations and segments. Passes |
|
| How many contacts match any of the documented filters: |
|
| Contacts matching the same filters, sorted by |
|
| Creates one contact, optionally in groups and with custom field values. Refuses locally if a group or custom field ID is not on the account, or if a contact with the same email already exists. Only registered when writes are enabled. |
|
| Changes fields of one contact, adds it to groups, sets custom field values, or marks it unsubscribed. Writes only. |
|
Array parameters are sent as repeated keys (TopicIds=7&TopicIds=99), as the GET /api/contacts description says. The Filter model is sent exactly as that description shows it: five parameters per filter, filters[i].condition, .field, .operator, .value and .order, with condition=null on the first filter.
Not covered on purpose: every DELETE endpoint (campaigns, contacts, groups, pages), POST /api/groups, POST /api/custom-fields, POST /abuse-complaint, GET /api/campaigns/{id}/html (the campaign's HTML), the landing page endpoints GET /api/pages and /api/pages/{id}, and GET /api/custom-fields (whose 200 has no schema; the custom field definitions come from GET /api/account, which has one). The API documents no endpoint that sends a campaign, and no tool here sends one.
Related MCP server: Amiqus MCP server
Setup
Requires Node 18 or later.
npm install
npm run buildCreate an API key in NewZapp as the spec describes: click your profile image, go to Account Settings > API Integration and create a key. The server sends it as the X-Api-Key header. NewZapp's help centre says API integration "is not covered under general NewZapp support".
Claude Desktop: add this to claude_desktop_config.json:
{
"mcpServers": {
"newzapp": {
"command": "node",
"args": ["/absolute/path/to/newzapp-mcp/dist/index.js"],
"env": { "NEWZAPP_API_KEY": "your-key" }
}
}
}Claude Code:
claude mcp add newzapp -e NEWZAPP_API_KEY=your-key -- node /absolute/path/to/newzapp-mcp/dist/index.jsVariable | Required | Meaning |
| yes | Your API key, sent as the |
| no |
|
| no | Defaults to |
Safety defaults
Read-only unless
NEWZAPP_ALLOW_WRITES=true. Read tools carry the MCPreadOnlyHintannotation.create_contactis marked non-destructive and non-idempotent;update_contactis marked destructive (it overwrites the fields you name) and idempotent. No tool deletes anything or sends a campaign.Contacts are people. By default
search_contactsreturns only each contact's ID, the unsubscribed, confirmed, suppressed and bounced flags, and the subscribe, unsubscribe, confirm and suppress-until dates. Email address, title, first and last name, company, job title, mobile and telephone numbers, date of birth and postal address are only returned withinclude_contact_details=true. A match on a search term or a Filter still confirms that such a contact exists, even when its details are withheld; the same holds forcount_contacts, which returns a number only.create_contactdoes not echo the email address back (it returns the new ID and groups, and says so if NewZapp stored the address differently).In free text (campaign names, subjects and feedback, group, segment, topic and tag names, group descriptions, the account name and company, NewZapp's error messages) email addresses are replaced with
[email redacted]and phone-number-like sequences with[phone redacted]by default, with an email pattern and a phone-number heuristic (international numbers with + or 00, bracketed UK area codes, and UK numbers starting with 0): international numbers written with+or00, UK numbers with a bracketed area code, and UK-style0…numbers of 9 to 11 digits with spaces, dots or hyphens. Other digit strings starting with0are redacted too, while numeric IDs and timestamps are left alone.include_contact_details=truereturns the text as stored on the tools that have it (list_campaigns,get_campaign,get_campaign_summary,list_contact_groups,search_contacts);get_accountalways redacts.get_campaignandget_campaign_summaryreturn records whose shape NewZapp does not document, and the summary is documented as "recipient action data", so per-recipient rows are the expected case. By default both go through an allowlist: numbers, true/false values and nulls are kept at any depth; text is kept only directly on the top-level record (with emails and phone numbers redacted as above); a list holding anything other than numbers, true/false values and nulls (per-recipient rows under any key name, but also the campaign's groups, topics, feedback and channel totals) is withheld whole; text nested deeper is withheld. On top of that, any key whose name suggests contact, identity or sender data is withheld whatever its value, a number included. Keys are matched in any spelling (emailAddress,email_addressandemailaddressalike) on these words: mail, phone, mobile, fax, address, postcode, postal, birth, name, location, salary, wage, user agent, custom field, job title, sender, author, owner, created/modified/updated by, reply-to, and, as whole words, tel, zip, city, county, country, dob, ip and pay, plus keys named exactly from, to, cc, bcc, user or person. The campaign's own top-levelnameis the one exception. A key naming people (contacts, recipients, subscribers, people, person, members, users) is withheld when it holds a list or object. Amobilekey is withheld when it holds a string or a number (a number there could be a phone number, so even a count by device is withheld), but kept when it holds an object such as the read-time breakdown by device. Everything withheld is listed by path underwithheld_fields(at most 100 paths). Keys that suggest bank, card or payment data (bank, IBAN, sort code, card, card number, PAN, BIN, last4, CVV, payment, account number) or credentials (password, secret, token, API key, SMTP username) are never returned, not even withinclude_contact_details, and are listed undernever_returned.include_contact_details=truereturns the rest as stored. Strings longer than 1,000 characters are cut, and nesting deeper than 20 levels is replaced by a placeholder. The default output can still carry personal data in a top-level text field under an innocuous key (only emails and phone numbers are redacted there) or as a bare number under an innocuous key.Nothing is ever downloaded, and the campaign HTML endpoint is not used.
Tool arguments are checked before any call is made. An unknown argument name is rejected rather than ignored, so a misnamed filter (
emailinstead ofsearchorfilters, say) cannot silently turn a count or a search into one over the whole account. IDs: every path and ID parameter is an int32 in the spec, so IDs must be whole numbers from 1 to 2147483647. Dates must be real dates inYYYY-MM-DDor ISO 8601 date-time form (passed to NewZapp as given, although the spec types them as date-times; see Status). Parameters whose values the spec does not list (Status,OrderBy, grouptype,Device,Client,Condition, Filter operators) accept letters, digits, spaces,_,.and-only; Filter field names and the sort field accept letters, digits and_.Writes never change subscription status except in one direction:
update_contactcan mark a contact unsubscribed and cannot re-subscribe one, andcreate_contactsends no subscription flag.isUnsubscribed: falseis never sent. Neither tool sends the documenteddeletedfield, andsuppressUntilDateis only ever sent back unchanged (next point).create_contactchecks the account's custom fields (GET /api/account) and groups (GET /api/groups) when you pass their IDs, then looks for an existing contact with the same email (the Filter model withemailaddressandcontains, compared exactly and case-insensitively on the results) and refuses to create a duplicate, pointing toupdate_contact.update_contactreads the contact first and sends back the full set of documented profile fields it has, its custom fields and its group IDs, with your changes applied. The spec does not say whetherPUT /api/contacts/{id}replaces the whole contact or only the fields sent, so the body also carriesisUnsubscribed: truewhen the contact is already unsubscribed and its currentsuppressUntilDatewhen it has one. Under either reading, then, the documented profile fields you did not name, the custom fields, the group memberships, an unsubscribe and a suppression date keep their current values (checked against the mock only; see Status). Group membership is only added to: the body carries the existing group IDs plus the new ones. A contact markedisDeletedis refused without a write.NewZapp documents no rate limit (neither the spec nor the "API Integration" help article mentions one). Requests are spaced 250 ms apart (about four per second). A 429 is retried at most twice for any method, including
POST /api/contacts, on the assumption that a rate-limited request was not processed (see Status). The retry waits forRetry-After(whole or fractional seconds, or an HTTP-date; 2 s then 4 s when the header is absent or unreadable). A single wait is capped at 10 seconds: if NewZapp asks for a longer one the request gives up at once and the message says how long to wait. Because one tool call can make many requests (a listing pages up to 40 times,create_contactmakes up to 7), each tool call also has one 40-second budget shared by all its requests, throttle and retry waits included, to stay under the MCP client's default 60-second request timeout: a retry wait that would end past the budget is not started, a request still running when it runs out is aborted, and aPOSTorPUTis not started with less than 10 seconds left (nothing is written). A listing that runs out of budget after at least one page returns the records it has, marked incomplete, with theskipto continue from.502, 503 and 504 are retried the same way for
GETonly; when all three attempts fail the error says the service may be unavailable, without the gateway's HTML. APOSTorPUTis never retried after a gateway error, because it may already have been processed; the error says to check withsearch_contactsbefore repeating it.Paging continues from the first record not returned, even when NewZapp sends a page longer than the
Takeasked for. A 200 whose body is not JSON (a proxy or login page in the way) is reported as an error namingNEWZAPP_BASE_URL, never as an empty list. An empty 200 body is accepted as success (thePUTdocuments no response body).A rejected key (401 or 403) produces a message that says which variable to fix, where the key is created, and to check that the API is enabled on the plan. Error text quoted from NewZapp, and the start of a non-JSON 200 body, is scrubbed of the configured API key and redacted like other free text. That redaction covers email addresses and phone numbers only: a name, date of birth or postcode in a NewZapp error message would be passed through.
Tests
npm testThe suite takes about 70 seconds (about 30 of them in the time-budget check, which waits out real Retry-After delays) and runs offline once spec.json is present (it is downloaded from my.newzapp.co.uk/swagger/v3/swagger.json on the first run when missing).
Checks the spec itself: OpenAPI 3.0.1, 22 operations, no
serversblock, nosecuritySchemes, theX-Api-Keyinstruction ininfo.description, every operation this server calls, the exact query parameter names of each (which the tools pass through), the documented Filter model encoding, that each used operation documents only a 200 response, which of them have no response schema (GET /api/campaigns/{id},GET /api/campaigns/{id}/summary,PUT /api/contacts/{id}), that the spec carries no JSON examples, and that the mock's key (nz-test-key-not-real) is obviously fake and not in the spec. Then validates every fixture record with Ajv against the published component schemas (AccountDTO,CampaignReportDTO,CampaignHeatmapDTO,GroupViewModel,ContactDTO,ContactDetailsDTO), with negative controls showing that undeclared keys, ids above int32 and non-date-times are rejected. One adjustment is made for Ajv:CustomFieldDTO.valueis{"nullable": true}without atype, which Ajv refuses; a schema without atypealready accepts any value including null, so thatnullableis dropped. Every other schema is used as published.Starts a local mock of the API under
/apithat serves the fixtures with theX-Api-Keyheader,Skip/Takepaging over bare JSON arrays, the documented filters it implements (Search,Status,TopicIds,FromDate/ToDateon campaigns;GroupId,Search,Unsubscribed,Bounced,Suppressedand the Filter model onemailaddress containsfor contacts;type,searchValue,tagIdsfor groups; the rest are recorded but do not filter), 401 without the right key, 404 for unknown IDs, and a one-off 429 withRetry-After. The mock's list, record, count, contact-detail and create responses are validated against the documented response schemas. NewZapp documents no response schema and no example for the campaign detail or the campaign summary, so their shape is unconfirmed: the mock serves the campaign'sCampaignReportDTOrecord as its detail (an assumption, checked against that schema once the invented keys are removed) with sender, author, reply-to, a scalarmobile, custom field, HTML and SMTP password keys invented to exercise the pass-through, and a summary whose shape is invented (aggregate counts, per-recipient rows under several key names with lower-case concatenated keys, a phone number stored as a number and addresses in free text, taken from a reviewer's probe that got past an earlier version) and not validated against anything. The status codes and bodies of errors are assumptions too (see Status).Starts the built server and drives it over stdio with the official MCP client: 30 checks (33 in the whole suite) covering tools/list and every tool's annotations, unknown argument names rejected on every tool without a request, the 250 ms spacing between paging requests, every read tool,
Skip/Takepaging across three pages stopping at a short page and across three full pages stopping at an empty fourth,max_resultswith continuation byskip, a page longer than theTakeasked for continued from the first record not returned, an API that ignoresSkipdetected instead of looped, heatmap cells outside the documented ranges ignored, a fractional count refused, every documented filter oflist_campaigns,list_contact_groups,get_campaign_heatmap,get_campaign_summary,count_contactsandsearch_contactspassed through under its documented name (the Filter model asfilters[i].*withcondition=nullfirst), redaction and withholding by default and their return on request (contacts, campaign subjects and feedback, group descriptions, the pass-through records under the allowlist with sender, author, contact and custom field keys and every list of records withheld, the per-recipient probe rows and contact keys in lower-case spellings withheld, amobileobject kept and amobilestring withheld, credentials never returned even on request, long HTML cut),create_contactnot echoing the email, the SMTP username never returned,create_contactandupdate_contactrequest bodies validated against the documented request schemas (CreateOrUpdateContactApiDTO,CreateOrUpdateContactDTO) and asserted field by field, the duplicate and reference checks refusing without a write, an update to an unsubscribed and suppressed contact sendingisUnsubscribed: trueand the suppression date back, a contact markedisDeletedrefused after one read, unsubscribe allowed and re-subscribe refused, the write gate with the variable unset and set tofalse, IDs, dates and parameter values refused before any call, the 404 message, the 401 message after exactly one request, the 403 message, the 429 retry waiting forRetry-Afterin the seconds, fractional-seconds and HTTP-date forms, the 2 s then 4 s fallback when the header is missing or unreadable, giving up at once on a wait above the cap and after three attempts on a persistent 429, a 429 onPOSTretried once, a 502 on aGETretried, aGETfailing three times with 503 reported without the gateway HTML, a 502 onPOSTand a 504 onPUTnever retried, a paged call slowed by a 429 before every page ending inside the 40-second budget with the records read so far and where to continue, a non-JSON 200 reported as an error (an email and phone number at the start of it redacted), an error body echoing the key and an email scrubbed of both, and that every request carriedX-Api-Keyand noAuthorizationheader, used a documented method and path with documented parameter names only, and that every operation the server uses was exercised.
Two time-budget paths are not in the suite, because each needs a request held open for 30 to 40 seconds: aborting a request NewZapp never answers, and refusing to start a POST with less than 10 seconds of budget left. Both were checked once with a throwaway local server (no NewZapp host involved): get_account against a server that never answered returned the budget error after 40.0 s, and create_contact whose duplicate-check GET took 31 s returned "nothing was written" with no POST sent.
Status
This is a working prototype. It has not been run against the live API, because it was built without a NewZapp account (no self-serve trial was found). No request with credentials was made to NewZapp; the only requests to NewZapp's hosts were unauthenticated fetches of public pages: the spec, the API docs page, the help centre, and the pricing page (which refused the request). Everything below should be confirmed on a real account:
The base URL
https://my.newzapp.co.uk, inferred because the spec has noserversblock.What a missing, wrong or unauthorised key gets back. The spec documents no error responses at all; the server treats 401 and 403 as a key problem, and the mock answers 401 with an empty body.
The status codes and bodies of other errors: 404 for an unknown ID and the 400 shape are assumptions (the mock uses ASP.NET Core problem details, which the spec's
text/jsonandapplication/*+jsoncontent types suggest). The server readstitle,detail,message,erroranderrorsfrom whatever comes back.The response shapes of
GET /api/campaigns/{id}andGET /api/campaigns/{id}/summary(no schema, no example). The server does not depend on any field name there. The default output of those two tools is an allowlist (numbers, true/false values, top-level text, minus contact-looking keys), which may withhold more than needed on a real record; check what a real record contains, in particular whether the summary lists recipients and under what keys, and whether any top-level text or bare number identifies a person.Paging: the default and maximum
Take, whetherSkip/Takebehave as offset and page size, and the end of a list. Nothing is documented: the server asks for 50 at a time and stops at an empty page or a page shorter than it asked for, so an API that silently capsTakebelow 50 would end a listing early. The default sort order of campaigns and contacts is not documented either.Parameter name casing. The server sends the names as the spec lists them (
Search,Skip,GroupId, and lower-casefrom,to,id,groupId,type,searchValue,tagIds), while the example in theGET /api/contactsdescription writes them in lower case (?skip=0&take=10&field=emailAddress…). The Filter model is sent in the description's lower-casefilters[i].*form.The Filter model: which field names and operators exist (the documentation shows only
emailaddress,isconfirmedandcontains, and says the full list is in the Contacts filter panel in NewZapp), whether the first filter'scondition=nullis needed as printed, and whetherorworks as a condition. What the separateConditionparameter does is not documented.The values of
StatusandOrderByon campaigns,typeon groups, andDeviceandClienton contacts; none is listed. The fixture values ("Sent", "Draft", "Internal", …) are invented.How
Searchmatches (which fields, and whether several terms must all match), and whatFromDate/ToDate,ModifiedFromDate/ModifiedToDateand the heatmap'sfrom/tocompare against, including time zones and whether the bounds are inclusive.Whether the date parameters (
FromDate,ToDate,ModifiedFromDate,ModifiedToDate, the heatmap'sfromandto,SendDateTime) accept a bareYYYY-MM-DD. The spec types them asformat: date-time; the server passes a date-only value through unchanged, which the tests only check on the wire.The date-time format in responses (the fixtures use
Z-suffixed ISO 8601 because the schemas saydate-time).CampaignReportDTOsemantics: howselected,sent,bouncedandfailedrelate, and whether NewZapp's own open and click rates are computed the way this server computes them (unique opens or clicks divided bysent).POST /api/contacts: whether the API itself rejects or merges a duplicate email, whethergroupIds(or the separategroupId) is what adds a contact to groups, whether a custom field value can be sent as{id, value}withoutnameandtype, howdateOfBirthis stored (sent asYYYY-MM-DDT00:00:00Z), and whether creating a contact on an account with double opt-in switched on sends the person a confirmation email.PUT /api/contacts/{id}: whether it replaces the whole contact or only the fields sent, whethergroupIdsreplaces or adds to the memberships, whetherisUnsubscribed: trueunsubscribes as expected, whether sending an existingsuppressUntilDateback is accepted unchanged, and what the 200 body contains (the mock sends none).The duplicate check in
create_contactrelies on the Filter model withemailaddress contains; if that filter does not work as documented, the check could miss an existing contact.A 429 on
POST /api/contactsis retried on the assumption that a rate-limited request was not processed; confirm NewZapp never creates the contact before answering 429.How many requests per second the API tolerates; nothing is documented, so the 250 ms spacing is a guess on the polite side. How long real requests take is unknown too: a long listing on a slow account can end early at the 40-second budget (it says where to continue).
Going to production
This version runs locally over stdio, with the account holder's own API key. For customers to connect from claude.ai or ChatGPT without handling keys, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by NewZapp, a run of the suite against a real account to settle the points above, and then a listing in the Claude and ChatGPT connector directories.
Licence
MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.
This server cannot be deployed
Maintenance
Related MCP Connectors
- ZapierOAuthcom.zapier.mcp
Zapier MCP connects AI tools like Claude, ChatGPT, and Cursor to over 8,000 apps and 30,000+ actions, enabling AI to perform real-world tasks such as sending messages, searching data, scheduling events, and updating records. It acts as a translator between AI tools and apps, handling authentication, rate limits, and retries automatically, transforming AI from a conversational tool into a functional extension of your business stack.
AXL MCP lets AI assistants create and manage landing pages, courses, email campaigns, CRM records, and marketing workflows inside AXL. Built for growing expert businesses, it turns chat requests into real work across sales, marketing, and course delivery. An AXL account is required. Sign in securely with OAuth 2.1. Website: https://axl.tech/developers/mcp . Setup guide: https://docs.axl.tech/mcp . Watch AXL in 77 seconds: pages, courses, CRM, and automation. Product overview: https://www.youtube.com/watch?v=jlhR9CafIww
Let Claude or ChatGPT search, read and send your WhatsApp messages over MCP. OAuth sign-in.
Read SMS, WhatsApp, email, contacts and audiences from your Bird workspace, plus safe CRM writes.
Related MCP Servers
- AlicenseAqualityCmaintenanceLets Claude, ChatGPT and other MCP clients read a SmartSurvey account's surveys, survey designs, responses, exports and folders, and — when writes are enabled — open or close a survey or send an existing invitation to one named recipient. It runs read-only by default, redacting respondent contact details and phone-number-like text unless contact details are explicitly requested.7MIT
- AlicenseAqualityCmaintenanceEnables Claude, ChatGPT and other MCP clients to read an Amiqus ID account—clients, onboarding records and steps, check results, templates, case status counts and webhooks—and, when writes are enabled, create records.9MIT
- AlicenseNot gradedqualityCmaintenanceEnables Claude, ChatGPT and other MCP clients to read practice-management data including organization, clinicians, diaries, availability, bookings, patients, invoices, payments, staff tasks, services, and locations, and optionally create staff tasks, create bookings, and cancel bookings.MIT
- AlicenseNot gradedqualityCmaintenanceLets MCP clients such as Claude and ChatGPT read a rota and time-and-attendance account, exposing venues, groups, shifts, absences and absence types, time entries, venue events and staff names through read-only tools that never return pay data.MIT