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-Idheader. - Server name:
supporter-club. - Protocol versions:
2025-11-25,2025-06-18,2025-03-26and2024-11-05. If the client asks for another version, the server answers with2025-11-25. - The server offers tools only.
| Method | Behaviour |
|---|---|
POST /mcp | Send 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 /mcp | 405 with {"error":"Method not allowed"}. The server opens no event stream. |
DELETE /mcp | 200 with {"success":true}. |
Transport errors on POST:
| Status | Body | Cause |
|---|---|---|
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.
| Situation | Response |
|---|---|
| No token, or the token is unknown, expired or revoked | 401 Unauthorized, empty body, WWW-Authenticate: Bearer realm="Supporter Club", error="invalid_token" |
| Token issued for a different organisation | Same 401 as above |
| Token approved by a user who no longer belongs to this organisation | 403 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:
| Bucket | What counts | Limit per minute |
|---|---|---|
| Tool calls | JSON-RPC tools/call | 60 |
| Everything else | All other requests to /mcp: initialize, tools/list, notifications, GET, DELETE | 600 |
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 toget_*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.
| Record | url 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.
| Argument | Type | Required | Notes |
|---|---|---|---|
query | string | Yes | Must not be blank. |
limit | integer | No | 1 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.
| Argument | Type | Required | Notes |
|---|---|---|---|
id | string | Yes | A 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.
| Argument | Type | Required | Notes |
|---|---|---|---|
cursor | string | No | From the previous page. |
limit | integer | No | 1 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.
| Argument | Type | Required | Notes |
|---|---|---|---|
id | string | Yes | The club's id. |
Club fields:
| Field | Description |
|---|---|
id | Entity reference. |
name | The club's name. |
slug | The club's address segment. |
state | draft, created or archived. |
published | true while the club is published. |
membership_fee_cents | The club's membership fee, in minor units. |
default_currency_code | The club's default currency. |
created_at | When 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.
| Argument | Type | Required | Notes |
|---|---|---|---|
club | string | Yes | The club's id. |
ambassador | boolean | No | true returns ambassadors only, false returns supporters only. Leave it out for both. |
state | string | No | draft, pending, active, expired or dormant (an inactive ambassador). The tool's own description also mentions cancelled, but that value is refused. |
cursor | string | No | From the previous page. |
limit | integer | No | 1 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.
| Argument | Type | Required | Notes |
|---|---|---|---|
id | string | Yes | The membership's id. |
Membership fields:
| Field | Description |
|---|---|
id | Entity reference. |
club | Entity reference to the club. |
contact | Entity reference to the donor. No name or email address. |
state | draft, pending, active, expired or dormant. |
ambassador | true for an ambassador, false for a supporter. |
created_at | When the membership was created. |
cancelled_at | When 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.
| Argument | Type | Required | Notes |
|---|---|---|---|
club | string | No | A club's id. Only that club's donations. |
since | string | No | RFC 3339 date-time with a time zone, for example 2026-09-01T00:00:00Z. Includes donations created at or after it. |
until | string | No | RFC 3339 date-time with a time zone, for example 2026-10-01T00:00:00Z. Includes donations created before it (not at it). |
sort | string | No | created_at_desc (default) or amount_desc. |
cursor | string | No | From the previous page. |
limit | integer | No | 0 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
succeededare listed or counted. summary.currencyis the organisation's default currency.summary.countcounts every matching donation.summary.sum_centsadds up only the matching donations in the default currency, so donations in other currencies are counted but not summed.- With the default
sortofcreated_at_desc, donations come in creation order, oldest first, despite the name. Page through them withcursor. - With
sort: "amount_desc", donations come largest first andnext_cursoris alwaysnull. Uselimitto get the top N. Paging is available only withcreated_at_desc. - With
limit: 0,itemsis empty andnext_cursorisnull. - An unknown
clubreturnsnot_foundwithids.club_id. Asinceoruntilvalue 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.
| Argument | Type | Required | Notes |
|---|---|---|---|
id | string | Yes | The donation's id. |
Donation fields:
| Field | Description |
|---|---|
id | Entity reference. |
club | Entity reference to the club. Left out if the donation has no club. |
contact | Entity reference to the donor. |
membership | Entity reference to the membership. Left out if there is none. |
amount_cents | Amount in minor units. |
currency | ISO currency code. |
state | The donation's state, for example succeeded, pending or failed. |
created_at | When 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" }
}
}
}code | When |
|---|---|
permission_denied | The token lacks the tool's scope. ids.required_scopes lists the missing scopes. |
not_found | No record with that id in this organisation. ids holds the id you sent (club_id, membership_id, donation_id or page_id). |
validation_error | A 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
| Code | Message | When |
|---|---|---|
-32602 | Invalid params | Unknown 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: …"). |
-32601 | Method not found | The JSON-RPC method isn't supported. |
-32603 | Internal error | The tool failed. This happens if you send an argument the tool doesn't list (every tool except list_donations). |
-32000 | See Rate limits | Over 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.