Skip to main content
Glama
dragosh29

Stampede MCP server

by dragosh29

Stampede MCP server

An MCP server that lets Claude, ChatGPT and other MCP clients work with a Stampede hospitality guest CRM: the venues in an organisation, guest profiles with their tags and orders, the organisation's deals, and which guest owns a Wi-Fi device; and (when enabled) tagging a guest. It is built from Stampede's public developer documentation only (the HTML pages under https://stampede.ai/developer). Stampede publishes no OpenAPI or other machine-readable spec: its pages describe each response as a TypeScript-style shape without example values, so the JSON schemas used by the tests were written by hand from those shapes (see Tests).

Once it's connected, someone running a venue group can ask things like:

  • "Which venues are in our organisation, and what are their IDs?"

  • "Find Sam Evans. When did we last see him, and what is he tagged with?"

  • "What has this guest ordered with us?"

  • "Which of our deals are active, when do they expire, and how many codes does each have?"

  • "Whose phone is 3c:22:fb:0a:1b:2c on the guest Wi-Fi?"

  • With writes enabled: "Tag Priya Shah as a Wine Club member."

Tools

Tool

What it does

API calls

list_venues

Venues (serials) in the organisation the API key belongs to: ID, name, organisation ID. Pages with the documented cursor and limit; continue with next_cursor.

GET /v1/venues

get_venue

One venue by its ID (as list_venues returns it), with its branding settings. Stampede documents no single-venue endpoint, so this pages through the venue list (up to max_pages pages of 50, 4 by default, at most 10) until it finds the ID.

GET /v1/venues

search_guests

Guests matching the documented search parameter (passed through as given), or all guests without it: ID, profile ID, name, created and last-interaction dates, tag IDs. Pages with cursor and limit.

GET /v1/guests

get_guest

One guest profile: name, dates, the profile's verified number as sent (its meaning is not documented), and counts of personalisation choices and custom answers; the personal fields only on request (see Safety defaults).

GET /v1/guests/:guest_id

list_guest_tags

A guest's tags. The response is not documented, so records are passed through with the generic withholding below.

GET /v1/guests/:guest_id/tags

list_orders

A guest's orders, with the documented search, limit and cursor. The response is not documented, so records are passed through with the generic withholding below.

GET /v1/guests/:guest_id/order

list_deals

Deals (vouchers such as 10%_OFF): name, description, active flag, expiry, cash value in minor units and currency, days a code stays valid, number of codes. Follows links.next to the last page, at most 10 pages per call; continue with page set to the returned next_page.

GET /v1/deal

wifi_lookup

Which guest owns a device, by MAC address, sent as the documented mac parameter exactly as given: guest ID and name, the email only on request.

GET /v1/guests/wifi

tag_guest

Adds a new or existing tag to a guest with the documented body { "name": ... }. Only registered when writes are enabled.

PUT /v1/guests/:guest_id/tags

Not covered on purpose: creating guests, bookings, orders, Wi-Fi sessions, form submissions and deals (POST /v1/guests, .../booking, .../order, .../wifi, .../form, POST /v1/deal), removing a tag (DELETE .../tags/by-name), voucher-code search and revocation (GET /v1/code/search, whose documented response includes sent_to_profile_data typed any, and POST /v1/code/revoke), and both workflow triggers (POST /v1/workflows/incoming-event, POST /v1/workflows/{workflowId}/workflow-runs).

Related MCP server: Indraft

Setup

Requires Node 18 or later.

npm install
npm run build

You need a client ID and client secret. As the Authentication page documents, they are created in the Stampede dashboard under Marketplace > API Keys > Create New API Key, and a key belongs to one organisation, so create it in the organisation whose venues and guests you want. The server exchanges them for a Bearer token at POST https://global.stampede.ai/oauth/token (OAuth 2.0 client credentials) and sends that token on every call.

Claude Desktop: add this to claude_desktop_config.json:

{
  "mcpServers": {
    "stampede": {
      "command": "node",
      "args": ["/absolute/path/to/stampede-mcp/dist/index.js"],
      "env": { "STAMPEDE_CLIENT_ID": "your-client-id", "STAMPEDE_CLIENT_SECRET": "your-client-secret" }
    }
  }
}

Claude Code:

claude mcp add stampede -e STAMPEDE_CLIENT_ID=your-client-id -e STAMPEDE_CLIENT_SECRET=your-client-secret -- node /absolute/path/to/stampede-mcp/dist/index.js

Variable

Required

Meaning

STAMPEDE_CLIENT_ID

yes

The client ID of your API key.

STAMPEDE_CLIENT_SECRET

yes

Its client secret. Sent only in the body of the token request, never logged.

STAMPEDE_ALLOW_WRITES

no

true to register tag_guest. Off by default.

STAMPEDE_REQUESTS_PER_15_MIN

no

How many API requests this server may make in any 15 minutes, from 1 to 100. Defaults to 100, the documented limit; lower it if other integrations use the same key. A value outside 1 to 100 stops the server at start-up.

STAMPEDE_TOOL_BUDGET_S

no

Seconds one tool call may take, all its requests and waits included. Defaults to 45, under the MCP client's default 60-second timeout. Used by the tests.

STAMPEDE_BASE_URL

no

Defaults to https://global.stampede.ai. Used by the tests.

Safety defaults

  • Read-only unless STAMPEDE_ALLOW_WRITES=true. Read tools carry the MCP readOnlyHint annotation. tag_guest is marked as a write that is not destructive; it is not marked idempotent, because the Tags page does not say what adding a tag the guest already has does.

  • Guests are third parties. By default the tools return a guest's IDs, name, created and last-interaction dates and tag IDs (and, in get_guest, the profile's verified number and how many personalisation choices and custom answers there are), and nothing else about them:

    • search_guests withholds email, phone and the three marketing-consent timestamps (data_opt_in_at, email_opt_in_at, sms_opt_in_at) and lists them under withheld_fields;

    • get_guest also withholds lat, lng, birth_day, birth_month, postcode, country and gender, and withholds the documented organization_registration_personalisation_choice and custom_question_answers lists as a whole (their items are typed any), returning only how many there are;

    • wifi_lookup withholds the owner's email;

    • names are returned, with any email address, phone number or UK postcode typed into them replaced by [email redacted] / [phone redacted] / [postcode redacted];

    • include_contact_details=true returns all of it.

  • The tag and order records are passed through as sent, because Stampede does not document their shape. Any key whose name suggests a guest's personal data (email, phone, telephone, tel, mobile, cell, msisdn, fax, contact, address, addr, street, city, town, county, postcode, post code, postal, zip, dob, birth, lat, lng, gender, ip, user agent, mac, opt in, consent, user profile) is withheld unless include_contact_details=true, and listed under withheld_fields. Any key whose name suggests bank, card or payment data (card, bank, iban, bic, swift, sort code, account number, pan, bin, last4, cvv, cvc, payment, pay method) is dropped whatever the caller asks, and listed under dropped_payment_fields. Keys are matched as whole words after splitting camelCase and snake_case, so customer_email, contact_number, postalCode, delivery_postal_code, address_line_1, card_last4 and payment are caught while venue_ids, in_venue and notes are not; a field that holds personal data under a name without one of those words is not caught. Key names are redacted like values (an email address used as a key is replaced).

  • Free text (venue names, deal names and descriptions, tag and order values, error messages) goes through an email pattern and a phone-number heuristic (international numbers with + or 00, bracketed UK area codes, and UK numbers starting with 0), plus two UK patterns: a heuristic that covers international numbers written with + or 00, UK numbers with a bracketed area code, UK-style 0… numbers of 9 to 11 digits, and UK numbers written as 44 followed by 10 digits without + (447700900123); other digit strings starting with 0 are redacted too. UK postcodes (EH6 6AA, SW1A 1AA, in either case, with or without the space) are replaced with [postcode redacted]; postal codes of other countries are not. A street name or town written into free text (an order note such as "Deliver to 1 Example Street, Edinburgh") is not caught, because there is no reliable pattern for it; the tests show this. Venue and deal text is always redacted (those tools have no include_contact_details switch). Card numbers (13 to 19 digits that pass the Luhn check) are replaced with [card number redacted] everywhere, with or without include_contact_details.

  • Nothing is downloaded: the branding image URLs of a venue are returned as links.

  • Input is checked before any call is made: argument names that a tool does not know are refused (so a typo such as serach cannot turn a search into a full listing that spends the request budget); a venue ID must be 1 to 100 characters (the Venues page types it only as string, and get_venue compares it in memory, never in a URL); guest IDs must be a single path segment of letters, digits, _ and - (the docs type them as strings and give no example) and may not be wifi, since GET /v1/guests/wifi is the Wi-Fi lookup; MAC addresses must be six hex pairs separated by : or -, or twelve hex digits; a tag name is trimmed and must be 1 to 255 characters (255 is the limit the Tags page gives for tag_name when deleting a tag, assumed here for adding one).

  • Rate limits, as documented: the Introduction page says "Users are allowed up to 100 requests every 15 minutes"; the Authentication page says the token endpoint "operates a strict rate limit of 20 requests/minute" and asks integrations to cache the token until it approaches expiry. This server keeps a rolling budget of 100 API requests (or STAMPEDE_REQUESTS_PER_15_MIN) per 15 minutes per server process, retries included. When the budget is spent it waits for a free slot if that takes 10 seconds or less and fits in the tool call's time budget (a case the tests do not exercise), and otherwise refuses the call without sending it, saying how many seconds to wait. The token is cached and refreshed shortly before expires_in runs out (a minute before, or after half its lifetime when it lives less than two minutes), and fetched afresh once if an API call answers 401. Token requests have their own budget of 20 per minute, and all requests are spaced 250 ms apart; neither of those two is exercised by the tests. The budget cannot see requests made by other processes or integrations that use the same key.

  • Tools that page ask for 50 records per page at most and stop at max_results, so a question does not spend the budget by default: search_guests and list_orders return 25 guests or orders unless asked for more. Each call also reads at most a fixed number of pages (10 for list_venues and list_deals, 4 for search_guests and list_orders); when a listing stops early, note says whether it hit max_results, the page cap or the time budget, and how to continue (cursor set to next_cursor, or page set to next_page for deals), or that there is no documented way to.

  • Stampede does not say that limit is honoured. If a page holds more records than were asked for, the whole page is returned (so count can exceed max_results, and note says why): cutting it would make next_cursor, which points after the whole page, skip the records that were cut. A list sent as a bare array (no links, so no cursor) is cut to max_results and marked incomplete.

  • A 429 is retried at most twice for any method, waiting 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 Stampede asks for a longer wait, the call gives up at once and the message says how long to wait. What Stampede's 429 looks like, and whether it sends Retry-After, is not documented.

  • Every tool call has one time budget, 45 seconds by default (STAMPEDE_TOOL_BUDGET_S), shared by all its requests and waits: token requests, API retries and the 401 re-authentication together, so the call ends before the MCP client's default 60-second timeout instead of sending requests after the client has given up. A retry or rate-budget wait that would end past it is not started, and a request still running when it ends is aborted (for PUT .../tags the message then says the tag may already have been added). A listing that has read at least one page when the budget runs out returns those records, marked incomplete, with the cursor of the next page. Aborting a request that is still running at the deadline is not exercised by the tests; the skipped waits and the partial listing are.

  • 502, 503 and 504 are retried the same way for GET (and for the token request, a case the tests do not exercise); when all three attempts fail the error says the service may be unavailable, without the gateway's HTML. PUT .../tags is never retried after a gateway error, because the tag may already have been added; the error says to check with the matching read tool (list_guest_tags) before repeating it.

  • Pages are followed by reading the query parameters of links.next (cursor for venues, guests and orders, page for deals) and requesting the same path again from STAMPEDE_BASE_URL, keeping the original search. The host in links.next is never contacted, so the token only goes to the configured base URL. A links.next that names a different path stops paging and says so.

  • A 200 to a GET whose body is not JSON (a proxy or login page in the way) is reported as an error naming STAMPEDE_BASE_URL, and a list response without a data list as an error naming the keys it had; neither is ever shown as an empty list. PUT .../tags has no documented response, so any 2xx is taken as "tag added" whatever its body (empty, text or JSON), and only a message from a JSON object is passed on.

  • wifi_lookup reads a 404 as "no guest owns this device" only when the 404 body is JSON (from the API); a 404 page from a proxy or a moved route, a 401 that survives a fresh token, and repeated 5xx stay errors.

  • An invalid or expired cursor (or deals page, a case the tests do not exercise) that Stampede answers with 400 comes back as an error that says to call again without it.

  • Errors pass on Stampede's message and errors (and the token endpoint's error and error_description), redacted, never the stack field its documented error body carries. The client secret is replaced with [redacted] if it ever appears in text that is passed on (so is the current access token, a case the tests do not exercise). Wrong credentials produce a message that names STAMPEDE_CLIENT_ID and STAMPEDE_CLIENT_SECRET and where they come from.

Tests

npm test

The suite needs no Stampede account and no network after the first run. It:

  1. Checks the documentation and the schemas written from it. The 12 developer pages are downloaded to docs.html and docs/ on the first run (both gitignored) and parsed: the suite asserts that they list 20 operations, including every method and path this server calls, with the documented query parameters (cursor, limit for venues; search, limit, cursor for guests and orders; mac for the Wi-Fi lookup; none for deals), that the Tags and Order pages document no response for the reads used here, that the two rate-limit sentences and the Form page's venue_id rule (see Status) are still on their pages, and that each response shape copied verbatim into test/documented-examples.mjs (token 200 and 400, venue list, guest list, single guest, the 400 and 409 error bodies, Wi-Fi owner, deal list) is still on its page. Each hand-written schema in test/schemas.mjs must equal the schema that test/shape.mjs derives mechanically from its shape (every key required unless marked ?, no other keys, the documented types and nulls), and the two request-body schemas (token request, tag) must list exactly the documented body fields and required fields. The mock's client ID and secret are checked to be obviously fake values that appear nowhere in the documentation. Every fixture record is then validated against the schemas.

  2. Starts a local mock of https://global.stampede.ai: the token endpoint with the documented JSON body, answering 200 or the documented 400 shape; the /v1 endpoints behind a Bearer token the mock issued; cursor pagination whose links.next points at https://global.stampede.ai/...; deals paginated by page with links and meta; a one-off 429 with Retry-After: 1 on the first GET /v1/venues; a 400 in the documented error shape for a cursor it did not issue; injected answers (JSON, HTML or raw text bodies, any status) on any endpoint; and a record of every request. The mock's token, list, guest, Wi-Fi, deal and 400 responses are validated against the schemas. Stampede documents no example values, so every value in the fixtures is made up, and it documents no 401, 404 or 429 body, so the mock answers those with the documented 409 error shape (401, 404) or a bare message (429). The tag and order records the mock serves are assumptions (see Status).

  3. Starts the built server and drives it over stdio with the official MCP client, 29 checks (32 in the whole suite, about 45 seconds): tool list and annotations; the token request's body and content type, one token reused across calls, a revoked token replaced once, a short-lived token refreshed before it expires; every tool; cursor paging to links.next: null for venues, guests and orders, with continuation by next_cursor for venues and guests and the search kept on later pages; deals paged page=2, page=3 from links.next; search, cursor, limit and mac passed through exactly; the default withholding and its listing, redaction of emails and phone numbers in names, venue names, deal descriptions, tag names and order notes, and their return on request; payment fields dropped and a card number redacted even with include_contact_details; tag_guest sending a body that validates against the documented request body; the write tool absent with STAMPEDE_ALLOW_WRITES unset or false; bad IDs, MAC addresses, cursors, tag names and unknown argument names (serach) refused before any request; get_venue honouring max_pages; the 404 message for an unknown guest, without the mock's stack, and a 422 whose message and errors are passed on with an email and a phone number redacted; the 429 retry for the seconds, fractional-seconds, HTTP-date and missing-header forms, giving up after three attempts and at once for a wait over 10 s; a 502 retried for GET, three 503s reported with advice, a 502 on the PUT not retried; a 200 HTML page and a list without data reported as errors; a links.next to another path stopping the paging; the first server process staying under 100 requests; an undocumented order record with contact_number, postalCode, street, city, msisdn, address_line_1, delivery_postal_code, town and county keys withheld, a UK postcode and a 44… number redacted in its note, in a key and in a 400 error message, and a street and town in the note left as they are (the known gap); a page larger than the limit sent kept whole so that next_cursor skips nothing, and a bare array cut to max_results; the page cap (4 pages of guests, 10 of deals) reported as such with a cursor or next_page to continue, and a max_results already at its maximum not advising to raise it; an invalid cursor answered with 400 reported with advice; wifi_lookup giving found: false only for a JSON 404, and errors for an HTML 404, a 401 after a fresh token and three 503s, with an email in the owner's name redacted; tag_guest succeeding on an empty 200, an empty 201, a text OK and a JSON true; a 3-second tool budget stopping a chain of 2-second 429 waits (list and PUT) inside it, and a listing returning its first page with a cursor when the second runs out of time; wrong credentials giving an actionable message with the secret scrubbed from an echoed error_description; the request budget refusing a call locally, without sending it, once STAMPEDE_REQUESTS_PER_15_MIN is spent, with a value of 500 refused at start-up (and a STAMPEDE_TOOL_BUDGET_S of 0); and, as the last check, that every request made by every server process in the suite carried the token request's documented body or a Bearer token the mock issued, hit a documented method and path, and used only documented query parameters (plus page on GET /v1/deal, read from links.next).

Status

This is a working prototype. It has not been run against the live API, because it was built without a Stampede account: there is no free trial, pricing is quoted on a sales call, and API keys are created inside the dashboard. Everything below comes from the public pages and should be confirmed on a real account:

  • The token request. The server sends client_id, client_secret and grant_type as JSON, as every example on the Authentication page does, and no scope (the page shows "scope": "ALL:ALL" in the response only). Confirm that this works, what expires_in is in practice, and what a wrong secret gets (the page documents a 400 with error and error_description; the server treats 400, 401 and 403 from the token endpoint as a credentials problem).

  • The answer to an expired or revoked token, and to an unknown guest, venue or Wi-Fi device. Nothing is documented: the server treats a 401 as "fetch a new token and retry once", a 404 as "not found", and a 404 with a JSON body from the Wi-Fi lookup as "no guest owns this device"; the mock uses the documented 409 error shape for them.

  • The format of guest IDs (the docs type them as strings with no example), and whether they can contain anything other than letters, digits, _ and -. The Guests and Tags pages list guest_id under "Query Parameters" for GET /v1/guests/:guest_id and GET /v1/guests/:guest_id/tags, while their path templates put it in the path; the server follows the path templates and assumes the label is a slip in the docs.

  • Venue IDs: the Venues page types id only as string; the Form page says its venue_id body field is "exactly 12 alphanumeric characters". Whether the two are the same value is not stated, so get_venue accepts any ID and matches it exactly against the list.

  • The meaning and values of user_profile.verified (typed number on the Guests page, with no explanation). get_guest returns it as sent, and does not read it as "email verified".

  • Pagination: the default and maximum limit for venues, guests and orders, and whether it is honoured at all (not documented; the guest table types limit as "Date", and the server sends a number of at most 50 and keeps an oversized page whole), what links.next contains (the server keeps the original search and limit and overrides them with whatever links.next carries), whether links.first and links.last are null on cursor pages, and whether GET /v1/deal pages by page (the server follows links.next, sends no parameters of its own on a first call because none is documented, and sends page only when continuing from a next_page it read from links.next).

  • What search matches on GET /v1/guests and on GET /v1/guests/:guest_id/order (fields, partial or exact, case). The mock matches names, emails and phone numbers for guests, and any field for orders.

  • The shape of GET /v1/guests/:guest_id/tags, GET /v1/guests/:guest_id/order and PUT /v1/guests/:guest_id/tags, none of which is documented. The mock serves tags as { "data": [{ "id", "name" }] } (and one guest's as a bare array) and orders as { "data": [...], "links": {...} } with the fields of the documented order-creation body (amount, currency, venue_ids, event_id, in_venue) plus id and created_at; the server does not depend on those names, but its default output for these two tools is only as safe as the key-name heuristic above, so check what a real record contains before relying on it.

  • Whether PUT .../tags with a tag the guest already has is a no-op, what it returns, and the longest tag name it accepts.

  • The Wi-Fi lookup: which MAC formats it accepts (the server sends the address exactly as typed, in either case, with : or - or no separators) and whether the match is case-sensitive.

  • The rate limit: whether the 100 requests per 15 minutes are counted per API key, per organisation or per IP address, whether token requests count towards it, and what a 429 looks like (body and Retry-After). The server's budget assumes 100 per key and does not count token requests.

  • The gender values (the table on the Guests page is cut off after "m" and "f"), the units of lat and lng, and whether country is a code or a name.

  • Whether guests whose data_opt_in_at is null (no data opt-in) are returned by GET /v1/guests, and whether the business wants their names shown at all.

get_venue pages through the venue list because no single-venue endpoint is documented; on a large organisation a GET /v1/venues/:id would make it one request.

Going to production

This version runs locally over stdio, with the organisation's own client credentials. For venue groups to connect from claude.ai or ChatGPT without handling keys, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by Stampede, a run of this suite against a real account to settle the points above, and then a listing in the Claude and ChatGPT connector directories. A third-party build would also go through Stampede's "Going Live" review to be listed in the Stampede Marketplace.

Licence

MIT. Built by Alexandru Dragoș (alexandru.dragos96@gmail.com) with an AI agent (Claude) working under his direction.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Connects Claude to Cendyn CRM, enabling real-time retrieval of contacts, purchases, campaigns, and audiences for analysis and decision-making.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to read and write CRM data—companies, contacts, opportunities, tasks, and interactions—through MCP, with OAuth and provenance tracking for every value.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to manage a fully customizable CRM, including contacts, companies, sales pipelines, custom objects and fields, file uploads, and data relationships via MCP over HTTP.
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables 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.
    9
    MIT