Skip to main content
Glama
dragosh29

Engaging Networks MCP server

by dragosh29

Engaging Networks MCP server

An MCP server that lets Claude, ChatGPT and other MCP clients work with an Engaging Networks (ENS) account: campaign pages, supporters, their transaction history and recurring gifts, supporter fields and questions, and marketing automation statistics, and (when enabled) updating a supporter's opt-ins. It is built from Engaging Networks' public developer documentation: the OpenAPI 3.1 document "Engaging Networks Services REST API" v6.5.0 at developer.engagingnetworks.net/api/rest/engagingnetworks.app.json, and the knowledge-base pages on the REST services.

Once it's connected, someone at the organisation can ask things like:

  • "Which donation pages are live right now, and which petitions have we closed?"

  • "Is otto.nv@example.com still opted in to email? Is he a member?"

  • "What has supporter 212200 done with us: gifts, petitions, events? Does he have a monthly gift running?"

  • "Who signed up in the last week?" / "Find supporters called Bob in the UK or US."

  • "How is the welcome journey doing this year: open rate, donations, unsubscribes?"

  • With writes enabled: "Otto asked by phone to stop texts: opt him out of SMS."

Tools

Tool

What it does

API calls

list_pages

Campaign pages of one type (nd donation, pet petition, ev event, dcf data capture, survey, et email to target, mem membership and the other documented types), optionally with one status (new, live, close, tested, block, delete).

GET /page

get_page

One page: type, subtype, status, locale, base URL, template, tracking parameters, attributes.

GET /page/{id}

find_supporter

One supporter by email address or supporter ID: ID, suppression flag, name, and optionally question and opt-in answers and memberships. Every other field only on request (see Safety defaults). The supporter field list is fetched once per session to recognise name fields.

GET /supporter or GET /supporter/{supporterId}, GET /supporter/fields

query_supporters

The API's supporter queries: latestCreated, latestModified (with daysBack 1-32), suppressed, profile (with profileId) and search (with a filter such as firstName:Bob~country:GB||US). Returns supporter IDs with created and modified dates, paged 100 at a time.

GET /supporter/query

get_supporter_transactions

A supporter's history (donations, event tickets, petition signatures, emails to targets, data captures, email broadcasts, peer-to-peer) with page names, dates and statuses, plus their recurring schedules (amount, currency, frequency, status, next payment date). The history list has no amounts; only the recurring schedules do. max_results (default 100) caps each list, with counts and totals.

GET /supporter/{supporterId}/transactions, GET /supporter/{supporterId}/transactions/recurring

list_supporter_fields

The account's supporter fields: name, tag and standard property.

GET /supporter/fields

list_supporter_questions

Questions and opt-ins with their type (OPT, CONF for double opt-in, GEN). At most max_results (default 100), with the total.

GET /supporter/questions

get_supporter_question

How one question is presented per locale: label, field type, answer options or range. At most 50 locales and max_options (default 100) options per locale, with the totals.

GET /supporter/questions/{id}

list_marketing_automations

Marketing automations with status, filtered by dashboard folder (the API defaults to the Home folder) and part of the name. At most max_results (default 100), with the total.

GET /ma

get_marketing_automation

One automation and its statistics (journey starts, open and click rates, actions, donations, objective reached, unsubscribes, SMS delivery rate, jumps), optionally for a range of months.

GET /ma/{id}, GET /ma/{id}/stats

update_supporter_opt_ins

Sets named opt-ins to Y or N for one supporter and changes nothing else. The names are checked against the account's questions first: general questions are refused, and so is setting a double opt-in (CONF) question to Y. Writes only.

GET /supporter/questions, PUT /supporter/{supporterId}

Authentication uses POST /authenticate (see Setup).

Not covered on purpose: page processing (donations, actions and card payments through /page/{id}/process), survey responses (the response schema and the example for GET /page/{id}/survey disagree on its shape), single-transaction detail (GET /supporter/{supporterId}/transactions/{transactionId} takes the payment gateway's transaction ID, which the history list does not return, and answers with card digits and expiry), page components, import formats, creating, updating or deleting supporters beyond opt-ins, bulk suppression, origin sources, migrating or changing recurring gifts, export jobs and their downloads, adding supporters to automations in bulk, the audit log, and the token validation and retirement endpoints.

Related MCP server: NewZapp MCP server

Setup

Requires Node 18 or later.

npm install
npm run build

You need the token of an API User. An administrator of your Engaging Networks account creates the API user in the dashboard, gives it permissions (for example view permission on supporter data), and whitelists the IP address of the machine this server runs on. The server posts that token to /authenticate, receives a session token, and sends it in the ens-auth-token header of every other call. The session token is cached, renewed a minute before its documented expiry, and fetched again once if a call answers 401 "Invalid ens-auth-token".

Claude Desktop: add this to claude_desktop_config.json:

{
  "mcpServers": {
    "engagingnetworks": {
      "command": "node",
      "args": ["/absolute/path/to/engagingnetworks-mcp/dist/index.js"],
      "env": { "ENGAGINGNETWORKS_API_TOKEN": "your-api-user-token", "ENGAGINGNETWORKS_REGION": "ca" }
    }
  }
}

Claude Code:

claude mcp add engagingnetworks -e ENGAGINGNETWORKS_API_TOKEN=your-api-user-token -e ENGAGINGNETWORKS_REGION=ca -- node /absolute/path/to/engagingnetworks-mcp/dist/index.js

Variable

Required

Meaning

ENGAGINGNETWORKS_API_TOKEN

yes

The API User token.

ENGAGINGNETWORKS_REGION

no

The datacentre your account is on, from the spec's server list: ca (Canada / Europe, https://ca.engagingnetworks.app/ens/service, the default), us (https://us.engagingnetworks.app/ens/service) or us2 (https://us2.engagingnetworks.app/ens/service).

ENGAGINGNETWORKS_BASE_URL

no

Overrides the region's base URL. Used by the tests.

ENGAGINGNETWORKS_ALLOW_WRITES

no

true to register update_supporter_opt_ins. Off by default.

Safety defaults

  • Read-only unless ENGAGINGNETWORKS_ALLOW_WRITES=true. Read tools carry the MCP readOnlyHint annotation; update_supporter_opt_ins is marked as a write that is not destructive and is idempotent. There is no tool that sends email, processes a page or a payment, or deletes anything.

  • Supporter records come back with the account's own field names as keys. By default find_supporter returns the supporter ID, the suppression flag, the name fields (title, first, middle and last name, recognised through /supporter/fields by field name or tag), opt-in and question answers when asked for, and memberships when asked for. Every other field (email address, phone numbers, postal addresses, date of birth, appeal code and custom fields) is withheld and only its name is listed; include_contact_details returns them as stored.

  • Card and bank data are never returned, with or without include_contact_details: fields whose standard property is a card holder name, bank account number, routing number, bank account type or password are dropped and only counted, and so is any field whose name or tag, split into words (_, - and . as separators, camelCase split, so ccExpiry reads as "cc Expiry"), contains the word card or cards, credit card, cc followed by num, number, no, exp, expiry, expiration, cvv, cvc, holder or type, expiry or expiration, token, mandate, debit, CVV/CVC, bank, IBAN, BIC, SWIFT, sort code, routing, account number, no or type, PayPal, billing agreement, password, PAR, BIN, or last 4 / last four. This errs towards dropping: a custom "Membership Expiry" field is dropped too (the memberships list carries the term dates). A payment field named in some other way is not recognised by its name. A card number typed into any returned text (13 to 19 digits passing the Luhn check) is replaced by [card number redacted], also on request. Recurring schedules never include the payment gateway's transaction ID (which embeds the processor's customer reference), and the endpoint that answers with card digits and expiry (single-transaction detail) is never called.

  • In free text (question answers of every type, supporter, peer-to-peer and member names, email-to-target targets, recurring-gift change reasons, page names and titles, and Engaging Networks' own error messages) email addresses (also URL-encoded, name%40example.org) become [email redacted], phone-number-like sequences [phone redacted] and postcodes [postcode redacted] by default. Question answers are redacted whatever their type says; the opt-in values Y, N, P and D are unchanged by it. Page names and titles are always redacted (the page tools have no switch). query_supporters returns email addresses only on request, and get_supporter_transactions returns a peer-to-peer fundraiser's email only on request. The free-text redaction is pattern matching, not a guarantee:

    • phone numbers: international numbers written with + or 00; UK numbers with a bracketed area code and UK-style 0… numbers of 9 to 11 digits (other digit strings starting with 0 are redacted too); North American numbers as (613) 555-0142, 613-555-0142, 613.555.0142, 613 555 0142, optionally after 1 (1-800-555-0199). Ten digits written without separators are not recognised.

    • postcodes, in capitals: UK (EC1M 5PX), Canadian (K1A 0A2, K1A0A2), US ZIP+4 anywhere (20500-0003), and a five-digit ZIP only directly after a US state code (DC 20500, Washington, DC 20500; Idaho's ID is left out, since "ID 12345" is usually an identifier). A lone five-digit number is left alone.

    • street addresses and dates typed into free text (for example "24 Sussex Dr" or "born 04/12/1980") are not recognised.

  • The API user token and the session token are scrubbed from any text passed on: the spec's own 401 example for /authenticate echoes the token it was given ("Invalid api key [...]").

  • IDs are checked before any call is made: page, supporter, question, profile and automation IDs must be positive integers (the spec types them all as integers), months must be YYYYMM with the start not after the end, a profile query needs profile_id and a search query needs filter (as the spec says).

  • update_supporter_opt_ins fetches the account's questions first and refuses, without writing anything, a name that is not a question, a general question, a name given twice, or a double opt-in (CONF) question set to Y, which would skip the supporter's own confirmation step. The body is { "questions": { "<opt-in name>": "Y" | "N" } }, the shape of the spec's "Update the supporters contact preferences" example, and holds no other field.

  • Lists the API returns whole are capped: list_pages, list_supporter_questions, list_marketing_automations and both lists of get_supporter_transactions return at most max_results entries (default 100, at most 1000) and say how many there are in total; get_supporter_question returns at most 50 locales and max_options options per locale. query_supporters returns at most max_results supporters and says which start to continue from; if the API returns more rows than were asked for, the continuation starts at the first row that was cut.

  • Rate limits, as documented: 5,000 requests per hour per API User, and 200 page-processing requests per 5 minutes from one IP address (this server does no page processing); beyond these limits requests are blocked. The spec documents running out of the hourly allowance as a 401 with the message "... has exceeded its api limit.", not as a 429: that answer is reported as a used-up allowance, and no new session is requested for it. Requests are spaced 200 ms apart; the hourly allowance is not counted locally, because other integrations using the same API user share it. Each query_supporters page is one request of up to 100 rows.

  • The spec documents no 429. One is still retried at most twice for any method, including the PUT, on the assumption that a rate-limited request was not processed (see Status). The retry waits for Retry-After (whole or fractional seconds, or an HTTP-date; 2 s then 4 s when the header is absent). Each wait is capped at 10 seconds; if a longer wait is asked for the call gives up at once and says how long to wait.

  • 502, 503 and 504 are retried the same way for GET and for POST /authenticate only; when all three attempts fail the error says the service may be unavailable, without the gateway's HTML. The PUT /supporter/{supporterId} is never retried after a gateway error, because it may already have been applied; the error says to check with find_supporter first.

  • A 200 whose body is not JSON (a proxy or a login page in the way) is reported as an error naming the region and base URL settings, never as an empty list; the excerpt goes through the redaction first.

Tests

npm test

The test suite:

  1. Validates every fixture record against the response schemas in the ENS OpenAPI document (pages and page details, supporter fields, questions and question details, supporter records by email and by ID, supporter query rows, transactions, recurring schedules, automations and automation stats). The spec is downloaded from developer.engagingnetworks.net to spec.json on the first run. Three Ajv settings work around defects in the document: unicodeRegExp: false (the locale pattern ^[a-z]{2,3}\-[A-Z]{2}$ is invalid as a Unicode regular expression), validateSchema: false (some schemas give examples as an object) and strict: false (OpenAPI keywords). Two schema defects are handled explicitly and asserted: the supporter schemas' questions[].response pattern rejects Y, N and any punctuation, including the spec's own getSupporterByEmail example and the pattern's own example value, so fixtures are validated against a copy without that one pattern; and the page subType schema rejects the empty string the spec's page examples use, so the fixtures omit subType instead. The transaction list schema is an anyOf of objects with nothing required, so each transaction fixture is also checked for the keys of the spec's example of its type. Peer-to-peer payments (ppay, pacs, pacr) are mapped by the spec's discriminator to transactionP2Pdonation, but every transaction schema's own type enum leaves those values out, and that schema's campaignId and status are oneOfs that any ordinary value matches more than once; the ppay fixture is validated against its properties with those two read as anyOf, and each defect is asserted. Negative controls check that the schemas reject an undocumented page type and recurring frequency.

  2. Starts a local mock of the API under /ens/service that implements POST /authenticate (the API user token as the raw body or as a JSON-quoted string, since the spec can be read either way, answering 401 with the documented "Invalid api key [...]" body, messageId 10000000, for a wrong one), GET and DELETE /authenticate/{ens-auth-token}, the ens-auth-token check with the documented 401 "Invalid ens-auth-token", the documented type and status page filters, the five supporter query types with start/rows paging, daysBack, profileId and filter, the folderId and name automation filters, and the opt-in update. It answers the first GET /ma with a 429. Its authentication, list, record, update and error responses are validated against the documented schemas; the 404 body (the spec documents no 404) is checked against the documented { message } error shape.

  3. Starts the built server and drives it over stdio with the official MCP client: 31 checks covering tool annotations; the first call's POST /authenticate with the raw token body (asserted as the reading this client implements, not as conformance) and JSON content type, then reuse of the session token; list_pages passing type and status exactly, with name and title redaction (UK and North American phone numbers, Canadian postcodes, ZIP codes) and max_results; get_page; find_supporter by email and ID with names only by default, the renamed middle-name field recognised through /supporter/fields, the withheld field names, includeQuestions and includeMemberships sent only when asked, redaction of emails, UK phone numbers and postcodes in answers and names, and of North American phone numbers, Canadian postcodes and ZIP codes in a supporter's name and general answer; answers with an undocumented type (GEN, lower case, none) redacted too; every field returned with include_contact_details except the five card, bank, password and PayPal fields of the spec's example, five custom payment fields (a token, cc_num_last4, a card expiration date, ccExpiry, a direct debit mandate) and a Luhn-valid card number typed into a custom field; unknown emails (404, or 200 without a supporter) as "not found"; query_supporters paging at start 0, 100 and 200 up to the documented total of 253 and stopping, with requests at least 190 ms apart (the 200 ms spacing), continuing from start, a correct continuation when the API returns more rows than asked for, an echoed filter in an error message with its URL-encoded email and phone number redacted, and passing daysBack, profileId and filter through exactly, with the profile and search requirements refused locally; get_supporter_transactions with every documented transaction shape, seconds and milliseconds timestamps, recurring schedules without the gateway reference, the P2P email only on request, max_results capping the recurring schedules too, page names and a North American change reason redacted, and a ppay payment keeping its site ID; the question tools, and the caps on questions, question options and locales, and automations; the automation tools after a 429 retry that waits for Retry-After, with folderId, name, startMonth and endMonth passed through and bad month ranges refused locally; the opt-in update's body validated against the documented PUT /supporter/{supporterId} request schema and its local refusals; invalid IDs and out-of-range max_results / max_options refused before any request, and the 404 message; the session token scrubbed from an error the API echoes it in; four concurrent calls on an expired session sharing one POST /authenticate; a retired session replaced once and the call retried; a short-lived session renewed before it expires; the documented usage-limit 401 on a data call (no new session) and on /authenticate; a persistent 429 giving up after three attempts and a Retry-After above the cap giving up at once; HTTP-date and fractional Retry-After; a GET failing three times with 503, a 502 retried on a GET and on POST /authenticate and never on the PUT; a non-JSON 200 and a 200 from /authenticate without a token; the write gate with the variable unset and set to false; a wrong API token giving an actionable message with the echoed token scrubbed; and that every request used a documented method and path, with the raw token only on POST /authenticate and a mock-issued ens-auth-token on everything else.

The suite runs in about 35 seconds.

Status

This is a working prototype. It has not been run against the live API, because it was built without an Engaging Networks account (the company offers no trial or sandbox that we could find). Everything below is taken from the published spec and knowledge base and should be confirmed on a real account:

  • The body of POST /authenticate. The spec types it as a JSON string under application/json; the knowledge base says to put "the token in the body". This server sends the token as the raw body, without JSON quotes. If the API expects a JSON-encoded string ("..."), that is a one-line change. The mock accepts both forms, so the tests do not settle this.

  • The session lifetime: expires is documented in milliseconds (example 3,600,000, one hour), and an expired or retired session is assumed to answer 401 "Invalid ens-auth-token".

  • Which datacentre a UK account is on. The default region is ca (the first server in the spec, labelled "Canada / Europe").

  • What GET /supporter?email= answers for an address that is not on the account, and what any endpoint answers for an unknown ID: the spec documents only 200 and 401 (and a 204 for single-transaction detail). The server treats a 404, or a 200 without supporterId, as "not found".

  • Whether supporter record keys are the account's field names or its tags. The spec's examples use names such as "Email Address" and "First Name", which are both in its fields example; the server matches either.

  • The real values in questions[].response (the spec's example is Y, while its schema's pattern forbids it), and whether general answers are returned in full. Every answer goes through the redaction whatever its type, so this does not affect what is withheld.

  • Peer-to-peer payment transactions (ppay, pacs, pacr): the spec's discriminator maps them to a schema with siteId, but the type enums leave them out; the server reads site_id from any transaction that has one.

  • Whether a real page list carries subType: "" for pages without a subtype, as the spec's examples do; the server treats an empty subtype as none.

  • The start parameter of GET /supporter/query. The parameter's example is 0 and the server treats it as the 0-based index of the first row, but the response example shows "start": 1 for a one-row result. If it is 1-based, the paging offset is one row out. Also to confirm: what rows above 100 does, whether a page can hold more rows than asked for (handled, but not observed), which query types honour daysBack, and the sort order of each query type.

  • The filter syntax of search queries is passed on exactly as given; which field names it accepts (the example uses firstName and country) is not documented beyond that example.

  • GET /supporter/questions/{id}: whether {id} is the question's id or its questionId (the list returns both; the detail example uses id).

  • GET /ma without folderId: documented as defaulting to the Home folder, so automations in other folders need folder_id. Whether the name filter is case-sensitive is not documented; the mock matches case-insensitively.

  • createdDate in the transaction list: the spec's examples mix seconds (1519918699) and milliseconds (1424408400000); values below 10^11 are read as seconds. createdOn (for example "01/03/2018") is passed on as a string because the day/month order is not documented.

  • The transaction history list carries no amounts; single-gift amounts are only in the per-transaction detail, which needs the gateway's transaction ID that the list does not return. Confirm whether the list on a real account includes more than the spec shows.

  • PUT /supporter/{supporterId} with only a questions object: that it changes only those opt-ins, that questions are keyed by their dashboard name, and what it answers for an unknown name (the documented 400 "The following fields are not present in the account" belongs to POST /supporter).

  • A 429 on the PUT is retried on the assumption that a rate-limited request was not processed; the spec documents no 429 at all.

  • Which API user permissions each endpoint needs, and what a missing permission looks like (a 403 is assumed and reported as a permission problem; it is not documented).

  • The statistics' month range: whether startMonth and endMonth are inclusive and what the defaults are.

find_supporter cannot search by name; query_supporters with type search and a filter on name fields is the documented way.

Going to production

This version runs locally over stdio, with the API user's own token, and the machine it runs on must be whitelisted for that API user. For organisations to connect from claude.ai or ChatGPT without handling tokens, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by Engaging Networks, 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.

Related MCP Connectors

  • Read campaigns, donations, profiles and supporters; record offline donations and upsert users.

  • The HubSpot MCP Server acts as a bridge that enables AI assistants and Large Language Models to securely interact with HubSpot CRM data through natural conversation, without requiring users to understand complex API structures. It provides read-only access to standard CRM objects (contacts, companies, deals, tickets, products, invoices, and more) and their associations, secured via OAuth 2.0, allowing AI agents to perform tasks like summarizing deals, fetching company updates, and looking up record changes.

  • Connect your ads, shop, analytics, social, CRM and finance platforms once, then let Claude, ChatGPT, Cursor or any MCP client read, join and explain your numbers. Public statistics from the World Bank, IMF, Eurostat, OECD, WHO and SEC filings come as context, searchable and chartable from the same tools. Read-only by design, every number carries its source.

  • Read and edit GA4, Search Console and Google Tag Manager from any MCP client. 29 tools.

Related MCP Servers

  • A
    license
    A
    quality
    C
    maintenance
    Lets 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.
    7
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables Claude, ChatGPT and other MCP clients to read NewZapp account details, campaign reports, open heatmaps, contact groups and contact counts, and to search contacts with personal data withheld by default; when writes are enabled, it can also create and update contacts.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables Claude, ChatGPT and other MCP clients to search a membership database's people and organisations, read a contact's summary and membership history, list events with ticket types and attendance lists, read invoices, and find and count segments. When writes are enabled, it also lets clients record who attended an event.
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables MCP clients such as Claude and ChatGPT to read a campsite's supplier account data from the Pitchup.com API — campsites, pitch types, pitches, charge types, prices and stay rules, allocation, extras and arrivals and bookings. When writes are explicitly enabled, it also sets allocation, prices and charge types, with writes off and the sandbox environment used by default.
    14
    MIT