Gecko Engage 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., "@Gecko Engage MCP ServerHow many people are booked on the Autumn Open Day?"
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.
Gecko Engage MCP server
An MCP server that lets Claude, ChatGPT and other MCP clients work with a Gecko Engage account, the student recruitment and engagement CRM used by universities and colleges: events and their sessions, bookings (attendances), forms and form responses, contacts (prospective students), labels and campaigns, and (when enabled) registering a contact for an event and adding a label to a contact. It is built from Gecko's public developer documentation: the Engage API OpenAPI 3.0.3 document at docs.geckoengage.com/openapi/engage-openapi.yml and the authentication guide at docs.geckoengage.com/docs/authentication.
Once it's connected, someone in the admissions or recruitment team can ask things like:
"How many people are booked on the Autumn Open Day, and how many are on the waitlist?"
"Which sessions does the open day have, and when?"
"How many responses has the Undergraduate Enquiry form had?"
"Find Priya Shah. Which events has she booked, and did she attend?"
"How did last week's WhatsApp open-day reminder do: delivered, read?"
With writes enabled: "Book Sam Evans onto the Spring Open Day." / "Add the Scholarship interest label to Sam."
Tools
Tool | What it does | API calls |
| The Gecko user the token belongs to, the token's region and expiry times (never the token), and which API scopes it has. |
|
| Events, sessions and session times with schedule, delivery method, capacity, tags (asked for with |
|
| One event with its description as plain text, location, categories, tags, sessions and session times, and counts. |
|
| Bookings for an event and/or a contact, with status, guest count and a count per status. Filters |
|
| Forms with module, group, published, expired and full flags and their number of responses. Filters |
|
| Form submissions with form, contact ID, draft or completed, labels (names, and IDs asked for with |
|
| Find contacts by |
|
| One contact by numeric ID or ULID: name, labels, preferred language, counts; contact fields only on request. |
|
| Labels with their IDs. Filter |
|
| Campaigns with channel, status, schedule and subscribers. Filters |
|
| Delivery and read stats for a WhatsApp broadcast campaign over a date range, in total and per period. The days are sent as |
|
| Books a contact onto an event or its waitlist. Because the endpoint is "create or update", it first looks up the contact's attendances on that event, deleted ones included ( |
|
| Adds an existing label to a contact, keeping their other labels. Writes only, marked destructive (see Safety defaults). |
|
Not covered on purpose: everything else in a 458-operation API, including conversations and chat, calls, messages and sending email or SMS, workflows, imports and exports, files and every download (iCalendar files, Wallet passes, conversation exports), payments and transactions, users, integrations, deleting or merging anything, and the separate Portal API.
Related MCP server: NewZapp MCP server
Setup
Requires Node 18 or later.
npm install
npm run buildYou need an API token for your Gecko account. As the authentication guide describes: ideally sign in as a dedicated API user in an "API Users" group with only the permissions needed (for this server's read tools, viewing events, attendances, forms, responses, contacts, labels and campaigns), go to Security Preferences -> Active Sessions -> Create New API Token, pick the profile and permissions, and copy the Access and Refresh tokens.
How the server uses them, following the guide:
The Access token is sent as
Authorization: Bearer …to the regional API named in itsregionclaim:euishttps://api-eu.geckoform.com,us-eishttps://api-us-e.geckoform.com,caishttps://api-ca.geckoform.com. The server refuses to start if the claim is missing or names another region, unlessGECKO_BASE_URLis set.Access tokens last about a day. When the token's
expclaim is less than a minute away, or the API answers 401, the server callsGET https://account-api.geckoengage.com/tokens/refreshwith the Refresh token as the Bearer token and switches to the new Access, ID and Refresh tokens it gets back. Calls that need a refresh at the same moment share one.Refresh tokens can only be used once, and the new tokens are kept in memory only. After the server has refreshed once, the Refresh token in your configuration is spent: when the server restarts, it cannot renew its tokens, and you need to create a new API token and update both variables. (The refresh token itself lasts about 30 days.) The server logs a warning to stderr each time it refreshes.
Claude Desktop: add this to claude_desktop_config.json:
{
"mcpServers": {
"geckoengage": {
"command": "node",
"args": ["/absolute/path/to/geckoengage-mcp/dist/index.js"],
"env": { "GECKO_ACCESS_TOKEN": "your-access-token", "GECKO_REFRESH_TOKEN": "your-refresh-token" }
}
}
}Claude Code:
claude mcp add geckoengage -e GECKO_ACCESS_TOKEN=your-access-token -e GECKO_REFRESH_TOKEN=your-refresh-token -- node /absolute/path/to/geckoengage-mcp/dist/index.jsVariable | Required | Meaning |
| yes | The Access token, sent as a Bearer token. A value pasted with its |
| recommended | The Refresh token, used once to renew the tokens (see above). Without it the server works until the Access token expires and then says so without making a request. |
| no |
|
| no | Overrides the API host taken from the token's region. Used by the tests. Must not contain a username or password. |
| no | Overrides the refresh endpoint, |
Safety defaults
Read-only unless
GECKO_ALLOW_WRITES=true. Read tools carry the MCPreadOnlyHintannotation. The onlyPOSTa read tool makes isPOST /contacts/search, which the spec documents as a search returning contacts.register_contact_for_eventis annotated as a non-destructive write.add_contact_labelis annotated destructive, because the API has no "add one label" call: the tool reads the contact's labels and sends the whole set back through "replace the contact's labels". A label someone else adds or removes in between can therefore be overwritten. It sends nothing when the contact already has the label, and refuses an unknown label ID.Gecko's contacts are prospective students, many of them under 18. By default a contact, attendee or response is identified by ID and name only. Email addresses (of contacts, attendees, responses and the API user) are only returned when a tool is called with
include_contact_details=true. A contact's field values, which hold things like phone numbers, addresses, dates of birth and schools, are not even requested from the API unlessget_contactis called that way. Form answers are never requested.Never returned, even on request: payment data on bookings (
payment_status,payment_transaction_id,payment_url,payment_response_id); contact fields whose label, tag or type looks like bank, card or payment data (the pattern covers bank, IBAN, BIC, SWIFT, sort code, account number, card, CVV/CVC, payment and expiry; the tests exercise a bank account field and a card number field); and the personal links Gecko issues for a student, which open their portal or booking without signing in (portal_url,qr_url,short_qr_url,events_page,available_auth_methods, and a booking'srsvp,ical,passbookandvideo_page). No file is ever downloaded.In free text (names, event titles, internal titles, countries, tags and descriptions, form names and groups, label names, campaign titles and descriptions, and Gecko's own error messages) email addresses are replaced with
[email redacted]and phone-number-like sequences with[phone redacted]by default. The phone match is a heuristic: it covers international numbers written with+or00(including the+44 (0)7700 …form), UK numbers with a bracketed area code such as(020) 7946 0958, and UK-style0…numbers of 9 to 11 digits with spaces, dots or hyphens between groups. Other digit strings of that shape that start with0are redacted too. Tools that return free text takeinclude_contact_detailsto turn this off. Form names and groups, label names and campaign titles and descriptions are always redacted (those tools have no switch). The tests cover names, an event's internal title, country and description, a form name and group, a label name, campaign descriptions and Gecko's error messages.Tokens never appear in tool output or error messages: every error message is checked against every token the server has held, including the Access, ID and Refresh tokens each refresh returns, and a match is replaced with
[redacted]. From an error response only Gecko's JSON message fields (message,details,messages,errors) are passed on. A non-JSON body, such as a gateway page, is never quoted.IDs are checked before any call is made. Every ID on the endpoints used here is an integer in the spec, so tools take positive whole numbers. The one exception is
get_contact, which, likeGET /contacts/{id}, also takes the contact's 26-character ULID. Stats dates must be realYYYY-MM-DDdates withfromnot afterto, andlist_attendancesneeds an event or a contact ID. Free-text filters are trimmed, and one that is empty or only spaces is refused. A form group that starts with@and contains|is refused, because Gecko would read it as several groups.Gecko documents no rate limit and no 429 response. Requests are spaced 250 ms apart (about four per second). A 429 is retried at most twice for any method, on the assumption that a rate-limited request was not processed, waiting for
Retry-After(whole or fractional seconds, or an HTTP-date; 2 s then 4 s when the header is absent or unreadable). Each wait is capped at 10 seconds, so one request waits at most 20 seconds in all. A tool call that makes several requests (pages, lookups) can still run past the MCP client's default 60-second request timeout. If Gecko asks for a wait longer than the cap, the call gives up at once and the message says how long to wait.502, 503 and 504 are retried the same way for
GETrequests to the API only. When all three attempts fail, the error says the service may be unavailable and to try again in a few minutes. APOSTis never retried after a gateway error, because it may already have been processed; the error says to check with the matching list or get tool first. The token refresh is never retried after a gateway error either: the Refresh token is single-use, and the refresh may already have happened.A 200 whose body is not JSON (a proxy or a login page in the way) is reported as an error naming
GECKO_BASE_URL, never as an empty list. A 401 says which variables to fix and where tokens come from, a spent or expired Refresh token says to create a new API token, a 403 says to check the token's scopes withwhoami, and a 404 includes Gecko's message.Lists are paged with the documented
pageandper_page(50 per page by default, at most 100; the spec documents no maximum). A call fetches up tomax_pagespages (4 by default) and stops at the documented end,current_pagereachinglast_page, or at an empty page. Pages are always returned whole. When more remain, the result sayscomplete: falsewith thenext_pageto continue from.
Tests
npm testThe test suite:
Validates every fixture record against the component schemas in Gecko's published OpenAPI document (
Events_EventObject,Attendances_Attendance,Contacts_Contactincluding included current values,Forms_Form,Responses_Response,Labels_Label,Campaigns_Campaign,Campaigns_CampaignStatsResponse,Auth_UserSummary,Auth_AuthScope) with Ajv and ajv-formats. OpenAPI 3.0'snullableis rewritten as a JSON Schema union first. A negative control checks that a wrong type is still rejected. The spec is downloaded fromdocs.geckoengage.com/openapi/engage-openapi.ymltospec.yamlon the first run.Starts a local mock of the API and of the refresh endpoint that serves those fixtures with the documented Laravel-style pagination (
current_page,last_page,per_page,total; every page capped at 10 so the suite pages) and the documented filters, including the@a|bmulti-value syntax and the comma-separatedcategory_id. It checks Bearer tokens (a 401 in the documented shape for a token it did not issue), refreshes single-use Refresh tokens, returns 404s with the spec's example messages, and answers the firstGET /formswith a 429. It follows the documented defaults the tools depend on: eventtagsonly withevent_rfields=tags, responselabel_idsonly withresponse_rfields=label_ids, responses to application-module forms only withmodule=application, and soft-deleted attendances only withtrashed=1. The stats range echoes thefromandtoit received. The mock's list, detail, write and error responses (200, 400, 401, 404) are validated against the response schemas the spec names for each operation. The request bodies it accepts are validated against the documented request schemas (the spec's ownPOST /attendancesexample among them). The refresh endpoint is not in the spec: its response is checked against a schema written from the guide's example refresh response, which the schema also accepts.Starts the built server and drives it over stdio with the official MCP client: 30 checks (32 in the whole suite, about 50 seconds). They cover:
tools/list and annotations, and every tool.
Each documented filter passed through exactly; paging 1, 2, 3 to
last_pagewith requests at least about 250 ms apart; stopping at the default of 4 pages and continuing fromnext_page; an empty page ending a list; the search body repeated on every page; application-module responses left out withoutmodule=applicationand returned with it; the stats range sent with its end-of-day time.Names without contact details by default and emails on request; payment fields and personal links never returned; contact fields not requested by default, and returned on request except the bank and card fields; emails and phone numbers in names, an internal title, a country, a description, a form name and group, a label name and campaign descriptions (including the
+44 (0), bracketed,00-prefixed and dot-separated phone forms) redacted.The search and the two write bodies validated against the documented request schemas, with every key a documented property; an existing booking, and a soft-deleted one, refused without a
POST; an existing label and an unknown label causing noPOST.IDs, a
per_pageabove 100, impossible dates, reversed ranges, blank keywords and a group in the multi-value syntax refused before any request; keywords trimmed; the 404 and 403 messages.The 429 retry in the seconds, fractional-seconds and HTTP-date forms and the 2 s then 4 s fallback without the header; giving up after three attempts, and at once above the cap; a wait of exactly the 10 s cap honoured; a 502 retried for
GET; three 503s reported without the gateway page; a 502 onPOSTnever retried and a 429 onPOSTretried once; a non-JSON 200 reported as an error; error bodies in all three documented shapes (message/details,messages,errors) passed on redacted, including a 400 that echoes the token and a contact's email and phone.Tokens:
A token near expiry refreshed before the call with the Refresh token as Bearer, the new token reused, the warning written to stderr without any token, and an error echoing the new tokens scrubbed.
A spent Refresh token after a "restart" refused with instructions and no API call.
A revoked token refreshed once and the call retried; revoked again on the same server, the second refresh uses the Refresh token the first one returned, never the spent one.
A 401 that persists after a refresh reported after exactly one refresh.
A 429 from the refresh endpoint retried with the same Refresh token.
Three concurrent calls sharing one refresh.
An expired Access token without a Refresh token, and an expired Refresh token, reported without a request.
A wrong token (pasted with
Bearer) giving an actionable 401.A 502 from the refresh endpoint not retried.
The region-to-host mapping.
Start-up refused for a missing or unknown region, an unreadable token, no token, or a username and password in
GECKO_BASE_URLorGECKO_TOKEN_URL(without repeating the password).
The write gate with the variable unset and
false.That every request carried a token the mock issued (or the one deliberately wrong token) to a documented method and path with only documented query parameters, that the refresh used a Refresh token, and that the only
POSTs were the search and the two writes.
Test tokens are unsigned JWT-shaped values built at run time from JSON, with markers like gecko-test-access-1-not-real. No token-like string is stored in the repository.
Status
This is a working prototype. It has not yet been run against the live API, because it was built without a Gecko account: no public trial or sandbox was found, and Gecko sells through demos. Everything below is taken from the published spec and authentication guide and should be confirmed on a real account:
Tokens. That the Access token carries
expand aregionclaim with the valueseu,us-eorca, as the guide says.Token refresh. That
GET /tokens/refreshanswers withAccessToken,IdTokenandRefreshTokenas in the guide's example; the example'sExpiresInlooks like a Unix time rather than a duration, and the server ignores it and readsexpfrom the token instead. Also what status and body it returns for a spent or expired Refresh token (undocumented; the mock uses 401 with amessage).Expired tokens. That an expired Access token gets a 401 from the API; the spec documents 401 for "authentication is required or the supplied token is invalid".
Paging. The largest
per_pagethe API accepts and what it does above it (the spec gives a minimum of 1 and no maximum; the server asks for 50 by default and at most 100). Thatlast_pageis present on every list, as the collection schemas require.Sort order. The default sort order of every list; the server returns whatever order the API uses.
Filter semantics. How
keywordon events, forms, labels and campaigns, andresponse_keywordon responses, match. WhetheremailonPOST /contacts/searchis an exact or partial match (the spec types it as a string without saying). Whether severalstatus,type,delivery_method,module,form_id,labelandcontact_idvalues really combine as "any of" with the@a|bsyntax, whilecategory_idis comma-separated as documented.Counts. That
counts=attendances,responseson events,counts=responseson forms,counts=subscriberson campaigns andcounts=attendances,responseson contacts addattendances_count,responses_countandsubscribers_countfields, as the schemas and the events list example suggest. Whether a form's response count includes responses thatGET /responsesleaves out by default (application-module and quarantined ones); the mock counts them all.Extra fields. That
event_rfields=tagsaddstagsto events andresponse_rfields=label_idsaddslabel_idsto responses, as the parameter descriptions say.Contact fields. The shape of
current_valueswithinclude=current_values.field: the server shows each value'ssafetext (falling back tovalue) under its field'slabel, as in the spec's example. The bank and card filter works on field labels, tags and types. Whether an account's real field names are caught by it needs checking, and a field named differently would be returned withinclude_contact_details.POST /attendances. What "create or update" does with a soft-deleted attendance (the tool refuses in that case, so this is not exercised). Whether a booking made through the API sends the event's confirmation email or runs its workflows (not documented). Thatstatus: "yes"creates a Registered (10) attendance and"waitlist"a Waitlisted (50) one. The capacity and guest-count errors, which the spec documents only by the example "Guest count must not be negative.".POST /contacts/{id}/replace_labels. That it accepts a plain list of existing label IDs; the spec says "Items may be existing label IDs or label objects".GET /campaigns/{id}/stats. What it answers for a campaign that is not a WhatsApp broadcast; the spec documents the endpoint for WhatsApp only. Thatfromandtosent asYYYY-MM-DD HH:MM:SSstrings are accepted ("a parseable date/time string"), and in which timezone they are read (the example response carries atimezone); if it is UTC, the edges of the range can be off by the account's UTC offset.Error wording. The wording of Gecko's error messages, and whether any of them echo request data; the server redacts and scrubs them regardless.
Rate limits. How many requests per second the API tolerates; nothing is documented, so the 250 ms spacing is a guess on the polite side.
Going to production
This version runs locally over stdio with a token the user creates by hand. It keeps rotated tokens in memory, so a restart needs a new API token. For admissions teams to connect from claude.ai or ChatGPT without handling tokens, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by Gecko, so each user's own permissions apply. Tokens would be stored in a secret store between refreshes, as the guide suggests. After that comes a listing in the Claude and ChatGPT connector directories. Further write tools (updating a booking's status after check-in, working with conversations) can follow once they can be tested on a real account.
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
Talk to your live-events CRM (campaigns, analytics, paid ads, segments) in Claude and ChatGPT.
Create forms, read submissions, and build invitations from Claude, ChatGPT, or any MCP client.
Let Claude or ChatGPT search, read and send your WhatsApp messages over MCP. OAuth sign-in.
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.
Related MCP Servers
- 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 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
- AlicenseNot gradedqualityCmaintenanceEnables 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
- AlicenseNot gradedqualityCmaintenanceLets MCP clients such as Claude and ChatGPT read a hospitality guest CRM: the venues in an organisation, guest profiles with their tags and orders, deals, and which guest owns a Wi-Fi device. When writes are explicitly enabled it can also add a tag to a guest.8 npmMIT