Supporter Club
API & Integrations

Connecting an AI agent (MCP)

Connect an MCP client to your organisation, authenticate it, stay within the rate limits, and use each tool.

Supporter Club runs a Model Context Protocol (MCP) server, so an AI agent such as an MCP-compatible desktop or command-line assistant can read your organisation's clubs, memberships and donations, and the Supporter Club documentation. Every tool is read-only. Donors appear only as references that link to the admin. No names, email addresses or phone numbers are returned. An agent needs an OAuth access token. It gets one either when a user signed in to your organisation approves its request on a consent screen (Authorize or Deny, for the scopes the agent asked for), or by requesting a token with its own client credentials, with no consent screen. Administrators can revoke an integration's tokens on the Integrations screen; the integration's client credentials keep working after that. The rest of this page is a reference for the developer connecting the agent.

Endpoint

The MCP server is at /mcp on your organisation's own Supporter Club address: the same host your admin and log-in page use. Supporter Club works out the organisation from the host name. The examples use https://your-organisation.example/mcp.

  • Transport: Streamable HTTP, stateless. There are no sessions and no Mcp-Session-Id header.
  • Server name: supporter-club.
  • Protocol versions: 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. If the client asks for another version, the server answers with 2025-11-25.
  • The server offers tools only.
MethodBehaviour
POST /mcpSend one JSON-RPC message. Content-Type must be application/json, and Accept must include both application/json and text/event-stream. Requests get a JSON response (not an event stream). Notifications get 202 Accepted.
GET /mcp405 with {"error":"Method not allowed"}. The server opens no event stream.
DELETE /mcp200 with {"success":true}.

Transport errors on POST:

StatusBodyCause
406{"error":"Not Acceptable: Accept header must include application/json and text/event-stream"}Accept header is missing a type.
415{"error":"Unsupported Media Type: Content-Type must be application/json"}Wrong Content-Type.
400{"error":"Invalid JSON"}The body isn't valid JSON.

Authentication

Every request to /mcp needs an OAuth access token issued for this organisation, sent as Authorization: Bearer <token>. Get one with the authorization code flow or with client credentials. See Integrations, OAuth and scopes.

An MCP client that supports OAuth discovery needs only the server URL. It finds the authorization server at /.well-known/oauth-protected-resource, then registers itself and runs the authorization code flow with PKCE.

SituationResponse
No token, or the token is unknown, expired or revoked401 Unauthorized, empty body, WWW-Authenticate: Bearer realm="Supporter Club", error="invalid_token"
Token issued for a different organisationSame 401 as above
Token approved by a user who no longer belongs to this organisation403 Forbidden, empty body
Token with no user (client credentials)Accepted. Tools behave the same as for a token a user approved.

Each tool needs a scope. A token without it can still call the tool, but gets a permission_denied tool error back. tools/list always lists all ten tools, whatever the token's scopes.

Rate limits

Limits apply to the whole organisation, across all tokens, in fixed one-minute windows that start on the minute:

BucketWhat countsLimit per minute
Tool callsJSON-RPC tools/call60
Everything elseAll other requests to /mcp: initialize, tools/list, notifications, GET, DELETE600

Requests are counted only after the token has been accepted. Every counted response carries:

  • X-RateLimit-Limit: 60 or 600, for the bucket the request counted against;
  • X-RateLimit-Remaining: requests left in this window;
  • X-RateLimit-Reset: Unix time, in seconds, when the window ends.

Over the limit, the server returns 429 Too Many Requests with Retry-After (seconds until the window ends, at least 1) and this JSON-RPC error:

{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32000,
    "message": "The caller has exceeded its rate budget for this tool. Retry after the cooldown indicated by the server.",
    "data": { "code": "rate_limited" }
  }
}

Making calls

Initialize

POST /mcp HTTP/1.1
Host: your-organisation.example
Authorization: Bearer Yt7dP0sQe3Lm9VxR2cWbN8fHk5uJa1gZo6iTq4nEw0A
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"example-client","version":"1.0.0"}}}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": true },
      "prompts": { "listChanged": true },
      "resources": { "listChanged": true },
      "logging": {}
    },
    "serverInfo": { "name": "supporter-club", "version": "0.1.0" }
  }
}

Call a tool

POST /mcp HTTP/1.1
Host: your-organisation.example
Authorization: Bearer Yt7dP0sQe3Lm9VxR2cWbN8fHk5uJa1gZo6iTq4nEw0A
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_club","arguments":{"id":"Xk9pQa"}}}

A successful result has the data twice: as JSON text in content, and as an object in structuredContent.

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [{ "type": "text", "text": "{\"created_at\":\"2026-03-14T10:22:31Z\", ...}" }],
    "isError": false,
    "structuredContent": {
      "created_at": "2026-03-14T10:22:31Z",
      "default_currency_code": "GBP",
      "id": { "id": "Xk9pQa", "url": "https://your-organisation.example/admin/clubs/Xk9pQa" },
      "membership_fee_cents": 1000,
      "name": "Night Run 2026",
      "published": true,
      "slug": "night-run-2026",
      "state": "created"
    }
  }
}

The examples below show only structuredContent.

Conventions

Entity references. Records are identified by an object with two fields:

  • id: a short opaque string. Pass it to get_* tools and as filters.
  • url: the record's page in the Supporter Club admin, on your organisation's host. Opening it needs an admin sign-in. This is the only route to a donor's identity.
Recordurl path
Organisation/admin/organisation
Club/admin/clubs/<id>
Membership/admin/clubs/<club id>/memberships/<id>
Donor (contact)/admin/contacts/<id>
Donation/admin/donations/<id>

Field order and empty fields. Record fields are sorted alphabetically. A field with no value is left out, not sent as null.

Timestamps are ISO 8601 in UTC, for example 2026-03-14T10:22:31Z. Amounts are integers in the currency's minor unit (for example pence), with ISO currency codes in capitals.

Pagination. list_clubs, list_memberships and list_donations return items and next_cursor. Pass next_cursor back as cursor to get the next page. next_cursor is null on the last page. Pages are in creation order, oldest first, unless a tool says otherwise. limit defaults to 25, and the most is 100. Treat the cursor as opaque. No total count is returned.

Tools

list_documentation_sections

Lists the sections of the Supporter Club platform documentation and the page ids in each. Scope: read:documentation. No arguments.

{
  "sections": [
    {
      "title": "Guides",
      "blurb": "A set of useful guides and tutorials on using the Supporter Club platform.",
      "page_ids": ["guides/contact-segments", "guides/merge-tags", "guides/mcp-scopes"]
    }
  ]
}

Each section has title, blurb and page_ids. The example shows one section.

search_documentation

Case-insensitive substring search across the documentation pages. Scope: read:documentation.

ArgumentTypeRequiredNotes
querystringYesMust not be blank.
limitintegerNo1 to 50. Default 10.

Example for query: "consent screen":

{
  "results": [
    {
      "page_id": "guides/mcp-scopes",
      "title": "Connecting an AI agent (MCP scopes)",
      "section": "Guides",
      "snippet": "...asked to pick which scopes the agent may request. The same scopes appear on the consent screen when you (or a teammate) authorise the agent for the first time. Connecting fro..."
    }
  ]
}

Results come in documentation order. snippet is plain text, up to 80 characters either side of the first match, with ... where it was cut. A blank query returns a validation_error: "The query argument must be a non-empty string."

get_documentation_page

Returns one documentation page as Markdown. Scope: read:documentation.

ArgumentTypeRequiredNotes
idstringYesA page id from either tool above, for example guides/mcp-scopes.

Returns id, title, section, content (the full Markdown) and last_updated_at. An unknown id returns not_found, with ids.page_id.

get_organisation

Returns the organisation the token belongs to. Scope: read:organisation. No arguments.

{
  "default_currency_code": "GBP",
  "enabled_currency_codes": ["GBP", "EUR"],
  "id": { "id": "Lm3vRt", "url": "https://your-organisation.example/admin/organisation" },
  "name": "Riverside Food Bank",
  "slug": "riverside"
}

list_clubs

Lists the organisation's clubs, oldest first. Scope: read:clubs.

ArgumentTypeRequiredNotes
cursorstringNoFrom the previous page.
limitintegerNo1 to 100. Default 25.
{
  "items": [
    {
      "created_at": "2026-03-14T10:22:31Z",
      "default_currency_code": "GBP",
      "id": { "id": "Xk9pQa", "url": "https://your-organisation.example/admin/clubs/Xk9pQa" },
      "membership_fee_cents": 1000,
      "name": "Night Run 2026",
      "published": true,
      "slug": "night-run-2026",
      "state": "created"
    }
  ],
  "next_cursor": "aWQ6MTI"
}

All clubs are included, whatever their state.

get_club

Returns one club. Scope: read:clubs.

ArgumentTypeRequiredNotes
idstringYesThe club's id.

Club fields:

FieldDescription
idEntity reference.
nameThe club's name.
slugThe club's address segment.
statedraft, created or archived.
publishedtrue while the club is published.
membership_fee_centsThe club's membership fee, in minor units.
default_currency_codeThe club's default currency.
created_atWhen the club was created.

An unknown id, or one from another organisation, returns not_found with ids.club_id.

list_memberships

Lists the memberships in one club, oldest first. Scope: read:memberships.

ArgumentTypeRequiredNotes
clubstringYesThe club's id.
ambassadorbooleanNotrue returns ambassadors only, false returns supporters only. Leave it out for both.
statestringNodraft, pending, active, expired or dormant (an inactive ambassador). The tool's own description also mentions cancelled, but that value is refused.
cursorstringNoFrom the previous page.
limitintegerNo1 to 100. Default 25.
{
  "items": [
    {
      "ambassador": true,
      "club": { "id": "Xk9pQa", "url": "https://your-organisation.example/admin/clubs/Xk9pQa" },
      "contact": { "id": "Vb7nEw", "url": "https://your-organisation.example/admin/contacts/Vb7nEw" },
      "created_at": "2026-04-02T18:05:12Z",
      "id": { "id": "Pq4sDk", "url": "https://your-organisation.example/admin/clubs/Xk9pQa/memberships/Pq4sDk" },
      "state": "active"
    }
  ],
  "next_cursor": null
}

An unknown club returns not_found with ids.club_id.

get_membership

Returns one membership. Scope: read:memberships.

ArgumentTypeRequiredNotes
idstringYesThe membership's id.

Membership fields:

FieldDescription
idEntity reference.
clubEntity reference to the club.
contactEntity reference to the donor. No name or email address.
statedraft, pending, active, expired or dormant.
ambassadortrue for an ambassador, false for a supporter.
created_atWhen the membership was created.
cancelled_atWhen the supporter's recurring donation was cancelled. Left out if it hasn't been.

An unknown id returns not_found with ids.membership_id.

list_donations

Lists succeeded donations, with a summary across every donation that matches the filters (not just this page). Scope: read:donations.

ArgumentTypeRequiredNotes
clubstringNoA club's id. Only that club's donations.
sincestringNoRFC 3339 date-time with a time zone, for example 2026-09-01T00:00:00Z. Includes donations created at or after it.
untilstringNoRFC 3339 date-time with a time zone, for example 2026-10-01T00:00:00Z. Includes donations created before it (not at it).
sortstringNocreated_at_desc (default) or amount_desc.
cursorstringNoFrom the previous page.
limitintegerNo0 to 100. Default 25. 0 returns only the summary.
{
  "items": [
    {
      "amount_cents": 2500,
      "club": { "id": "Xk9pQa", "url": "https://your-organisation.example/admin/clubs/Xk9pQa" },
      "contact": { "id": "Vb7nEw", "url": "https://your-organisation.example/admin/contacts/Vb7nEw" },
      "created_at": "2026-09-12T08:41:03Z",
      "currency": "GBP",
      "id": { "id": "Rt6yHn", "url": "https://your-organisation.example/admin/donations/Rt6yHn" },
      "membership": { "id": "Pq4sDk", "url": "https://your-organisation.example/admin/clubs/Xk9pQa/memberships/Pq4sDk" },
      "state": "succeeded"
    }
  ],
  "summary": { "count": 148, "sum_cents": 412500, "currency": "GBP" },
  "next_cursor": "aWQ6OTg3"
}
  • Only donations with state succeeded are listed or counted.
  • summary.currency is the organisation's default currency. summary.count counts every matching donation. summary.sum_cents adds up only the matching donations in the default currency, so donations in other currencies are counted but not summed.
  • With the default sort of created_at_desc, donations come in creation order, oldest first, despite the name. Page through them with cursor.
  • With sort: "amount_desc", donations come largest first and next_cursor is always null. Use limit to get the top N. Paging is available only with created_at_desc.
  • With limit: 0, items is empty and next_cursor is null.
  • An unknown club returns not_found with ids.club_id. A since or until value that isn't an RFC 3339 date-time is refused with JSON-RPC error -32602 (see JSON-RPC errors).

get_donation

Returns one donation, whatever its state. Scope: read:donations.

ArgumentTypeRequiredNotes
idstringYesThe donation's id.

Donation fields:

FieldDescription
idEntity reference.
clubEntity reference to the club. Left out if the donation has no club.
contactEntity reference to the donor.
membershipEntity reference to the membership. Left out if there is none.
amount_centsAmount in minor units.
currencyISO currency code.
stateThe donation's state, for example succeeded, pending or failed.
created_atWhen the donation was created.

An unknown id returns not_found with ids.donation_id.

Errors

Tool errors

A tool that can't do what was asked returns a normal JSON-RPC result with isError: true. structuredContent has a stable code, a message, and sometimes ids naming what was involved:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "The caller's token is missing the read:donations scope required by this tool." }],
    "isError": true,
    "structuredContent": {
      "code": "permission_denied",
      "message": "The caller's token is missing the read:donations scope required by this tool.",
      "ids": { "required_scopes": "read:donations" }
    }
  }
}
codeWhen
permission_deniedThe token lacks the tool's scope. ids.required_scopes lists the missing scopes.
not_foundNo record with that id in this organisation. ids holds the id you sent (club_id, membership_id, donation_id or page_id).
validation_errorA blank search query, or a cursor that isn't valid ("The supplied cursor is not valid: …"). A since/until value that passes the date-time check but still can't be read also returns this ("since / until must be valid iso8601 timestamps: …").

JSON-RPC errors

CodeMessageWhen
-32602Invalid paramsUnknown tool (data: "Tool not found: …"), a missing required argument (data: "Missing required arguments: …"), or an argument with the wrong type, format or range, for example limit: 500, state: "cancelled" or a since that isn't an RFC 3339 date-time (data starts "Invalid arguments: …").
-32601Method not foundThe JSON-RPC method isn't supported.
-32603Internal errorThe tool failed. This happens if you send an argument the tool doesn't list (every tool except list_donations).
-32000See Rate limitsOver the rate limit (HTTP 429).

HTTP 500

If any value in a record tool's result looks like an email address or phone number, for example a club name that contains one, the server refuses to return it. The record tools are get_organisation, list_clubs, get_club, list_memberships, get_membership, list_donations and get_donation. The documentation tools don't make this check. The request fails with HTTP 500 and no JSON-RPC response. The whole call fails, so a list tool returns none of the records on that page, not only the one that matched.

On this page