Skip to main content
Glama
dragosh29

sheepCRM MCP Server

by dragosh29

sheepCRM MCP server

An MCP server that lets Claude, ChatGPT and other MCP clients work with a sheepCRM membership database: search people and organisations, read a contact's summary and membership history, list events with their ticket types and attendance lists, read invoices, find and count segments, and (when enabled) record who attended an event. It is built from sheepCRM's public documentation only: the OpenAPI spec at https://sls-api.sheepcrm.com/.well-known/openapi.yaml (which the vendor describes as a work in progress; its source repository tracks 43 of about 180 endpoints as documented) and, for authentication and throttling, the legacy docs at https://docs.sheepcrm.com/.

Once it's connected, a membership secretary can ask things like:

  • "When does Sam Evans's membership end, and is it set to auto-renew?"

  • "Which events are coming up, and how many tickets are left for the AGM?"

  • "Who attended the AGM, and who was invited but didn't reply?"

  • "How many people are in the 'Members lapsing this month' segment?"

  • "Show me invoice INV-0001: what's been paid and what's still due?"

  • With writes enabled: "Mark Priya Shah as attended at the AGM."

Tools

Tool

What it does

API calls

search_people

sheepCRM's global search for a name or other text. Returns each match's URI, record type and display name; by default only person results are kept (the filter is applied locally; resource: "any" keeps everything).

GET /search/v2/{bucket}?q=

find_person

The exact-details person matcher, with every documented parameter passed through under its documented name (email, first_name, last_name, title, date_of_birth, postal_code, take_first_if_multiple). Adds a note when the details given are not one of the matching rules in the legacy docs. The response shape is not documented, so the result is passed through with personal fields withheld.

GET /search/v2/{bucket}/person

get_person

The summary profile of one person or organisation by URI. Response shape not documented; passed through with personal fields withheld.

GET /api/v2{contact_uri}summary

list_memberships

A contact's full membership history: number, plan, status, start and end dates, days until the end date (computed), renewal and auto-renew flags, lapse reason, fee and whether it is paid. Latest end date first. Accepts a person or an organisation URI; the organisation form is outside the spec (see Status).

GET /api/v2{person_uri}membership/all; for an organisation GET /api/v2/{bucket}/organisation/{uid}/membership/all

get_membership

One member record by uid. Response shape not documented; passed through with personal fields withheld.

GET /api/v2/{bucket}/member/{uid}/detail (see Status)

list_events

Events with dates, location, status, capacity and tickets left: the default window (past 14 days, next 90) or one documented state (all, current, draft, future, past, running). Pages with page/page_size.

GET /events/v2/{bucket}, /events/v2/{bucket}/bookings/{state}

get_event

One event's details and its ticket types with price and availability, or the reduced summary with summary_only.

GET /events/v2/{bucket}/booking/{uid} (or .../summary), .../available_tickets

list_event_attendees

An event's attendance list with each person's status, ticket and guest flag, and counts per status. The status filter is applied locally (the endpoint documents no parameters).

GET /events/v2/{bucket}/booking/{uid}/attendance

get_invoice

One invoice, order or membership invoice: status, date, totals, paid and due, line items, the buyer's name.

GET /invoices/v2/{bucket}/{invoice_type}/{uid}

list_segments

Saved lists (segments): the active ones, all of them including inactive, or the active ones matching a name. Pages with page/page_size.

GET /segments/v2/{bucket}, /segments/v2/{bucket}/all, /segments/v2/{bucket}/find?name=

get_segment_count

How many records a segment holds, with its description.

GET /segments/v2/{bucket}/segment/{uid}/count

set_event_attendance

Sets one person's attendance status at an event (invited, accepted, declined, attended, no-show), sent as the two documented query parameters. Replaces the previous status. Only registered when writes are enabled.

PUT /events/v2/{bucket}/booking/{uid}/attendance?person_uri=&status=

Not covered on purpose: the /internal/* endpoints, segment creation, segment bulk actions (/action/replace, /action/delete), Mailchimp sync, exports and PDF/Excel reports, avatars and images, deleting event orders, rebasing tickets and questions, form responses, the personal and communications detail endpoints (their only purpose is contact data), and the audit log. There are no list_invoices or list_groups tools. The OpenAPI spec has no endpoint that lists invoices or groups. The legacy docs do name some: GET /api/v2/{bucket}/group/, /group/all and /group/{uid}/members/all (the last with an expansions parameter, open_tasks, and a status filter), and a contact's invoices at GET /api/v2/{bucket}/{organisation|person}/{uid}/invoices/summary and .../detail. They document no response shape for any of them, so they were left out rather than built on guesses.

Setup

Requires Node 18 or later.

npm install
npm run build

You need two things from sheepCRM:

  • Your flock (the spec calls it the bucket): the name of your database, the first part of every sheepCRM URI, e.g. example-association in /example-association/person/6305f074683e800f3abe809e/.

  • An API key. The legacy docs say you can set your own by logging into sheepCRM and going to your Profile settings. The server sends it as Authorization: Bearer <key>, as those docs show. They also say a 403 means a problem with the key or with your permissions.

Claude Desktop: add this to claude_desktop_config.json:

{
  "mcpServers": {
    "sheepcrm": {
      "command": "node",
      "args": ["/absolute/path/to/sheepcrm-mcp/dist/index.js"],
      "env": { "SHEEPCRM_FLOCK": "your-flock", "SHEEPCRM_API_KEY": "your-key" }
    }
  }
}

Claude Code:

claude mcp add sheepcrm -e SHEEPCRM_FLOCK=your-flock -e SHEEPCRM_API_KEY=your-key -- node /absolute/path/to/sheepcrm-mcp/dist/index.js

Variable

Required

Meaning

SHEEPCRM_FLOCK

yes

Your database (flock/bucket) name. Letters, digits, - and _ only; the server refuses to start otherwise.

SHEEPCRM_API_KEY

yes

Your API key, sent as a Bearer token.

SHEEPCRM_ALLOW_WRITES

no

true to register set_event_attendance. Off by default.

SHEEPCRM_BASE_URL

no

Defaults to https://sls-api.sheepcrm.com. Used by the tests.

SHEEPCRM_TIME_BUDGET_S

no

Seconds one tool call may spend on requests, retries and waits (2 to 55, default 45). Keep it below your MCP client's request timeout. One test uses 5.

Safety defaults

  • Read-only unless SHEEPCRM_ALLOW_WRITES=true. Read tools carry the MCP readOnlyHint annotation. set_event_attendance is marked destructiveHint: true (it overwrites the previous status) and idempotentHint: true.

  • Members, attendees and buyers are third parties. By default:

    • search results and attendance lists return names and record URIs, not the primary_email, primary_telephone or attendee_email fields;

    • invoices return the buyer's name, not their address, locality, region, postcode, country, email, telephone or VAT number;

    • events do not return the booking and billing contact, the booking organisation or the venue postcode;

    • the three records whose shape sheepCRM does not document (get_person, get_membership, find_person) are passed through with every key whose name suggests contact or personal data withheld and listed under withheld_fields: email, phone, telephone, tel, mobile, fax, address, street, town, city, county, region, postcode, postal code, zip, locality, mailsort, geo, coordinates, lat, lng, lon, latitude, longitude, dob, born, birth, date of birth, death, deceased, date of death, salary, pay, wage, income, NI and national insurance, passport, licence (including driving licence), NHS, IP, user agent, health, medical, SEN, EDI, equal ops, ethnicity, gender, sexuality, disability, religion, emergency, next of kin, pickup (authorised pickup), known as, school, school year, photo, vehicle, Twitter, Facebook, Instagram, Skype, LinkedIn and social. Of the person record shown in the legacy docs' Getting Started example, this withholds the contact, location, identity, health, education, photo and social fields (email, telephone, address_lines, locality, region, postal_code, mailsort_code, geo, date_of_birth, date_of_death, deceased, driving_licence, sen, emergency_contact_details, authorised_pickup, known_as, gender, school, school_year, photo, twitter_username, linkedin_public_profile and the other social usernames), and the tests check each of them. Names, salutation, job title, interests, tags, bio, anniversary, adult and similar fields in that record are not withheld by name (their text is still redacted as below). The key is normalised first (camelCase split, lower-cased) and matched as whole words, so primary_email, dateOfBirth, address_lines and postal_code are caught and description or title are not. Keys are redacted like text too: an object keyed by an email address shows [email redacted] as the key, and the paths under withheld_fields and never_returned use the redacted key. A key that holds personal data under another name (a free-form notes object, say) is not caught by name;

    • every other string that can hold free text (names, titles, descriptions, lapse reasons, text inside undocumented records, error messages) has email addresses replaced by [email redacted], phone-number-like sequences by [phone redacted], UK postcodes by [postcode redacted] and a date introduced by "born", "DOB", "date of birth", "birth date" or "birthday" by [date of birth redacted]. These are heuristics: they do not catch a street address, a health condition in a sentence or a date of birth written without one of those words;

    • include_contact_details: true on a tool returns those fields and that text as stored (except the items below).

  • Never returned, whatever is asked: in membership records payment_method, payment_plan, payment_date, payment_reference, gc_subscription_id, stripe_subscription_id and next_payment_plan; in invoices the organisation's payment_details and payment_detail_notes (bank details), signature, signed_invoice_uri and payment_reference_uri (they give access to the invoice or payment), and links (PDF download links); ticket access_codes; an event's internal notes (dietary requirements, catering, bar, setup, general notes); segment rules and include/exclude lists; and in undocumented records any key whose name suggests bank, card or payment data (bank, IBAN, BIC, SWIFT, sort code, account number, card, PAN, BIN, last4, CVV, CVC, payment, Stripe, GoCardless, gc, mandate, PayPal, direct debit, dd), listed under never_returned. Card numbers that pass the Luhn check and a sort code followed by an 8-digit account number are replaced in all text in every mode.

  • Nothing is downloaded: PDF, Excel, export, avatar and image endpoints are not used.

  • IDs are checked before any call is made: uids must be alphanumeric (the spec describes them as "an alphanumeric unique identifier"; its examples are 24 and 8 hex characters), contact URIs must have the form /flock/type/uid/, belong to SHEEPCRM_FLOCK and be of the right type (person or organisation; person for attendance), and enumerated values (state, invoice_type, attendance status) must be one of the documented values.

  • Rate limits: the OpenAPI spec says nothing about them. The legacy docs' "API Throttling" section says sheepCRM may change its limits at any time, that throttled endpoints return x-rate-limit-limit, x-rate-limit-remaining and x-rate-limit-reset headers, and gives the example 40, 400;window=60, 1000;window=3600 (40 calls remaining, 400 per minute, 1000 per hour). They do not say what status a throttled request gets or whether Retry-After is sent. This server spaces requests 250 ms apart (at most four a second, which is inside 400 a minute but, sustained, would use the example's 1000 an hour in about four minutes) and retries a 429 at most twice for any method. It waits for Retry-After (whole or fractional seconds, or an HTTP-date); without it, for x-rate-limit-reset when that is a plain number of seconds; with neither, 2 s then 4 s. It does not read x-rate-limit-limit or x-rate-limit-remaining. A single wait longer than 10 seconds is not attempted: the call gives up at once and says how long sheepCRM asked to wait. A listing makes at most 10 page requests per tool call (1000 items at 100 per page).

  • Time budget: all the requests, retries and waits of one tool call share a budget of 45 seconds (SHEEPCRM_TIME_BUDGET_S), below the MCP client's default 60-second request timeout. A wait that would run past it is not started, and a request still unanswered when it runs out is abandoned. A listing that has already fetched some pages then returns them with complete: false and a note saying why it stopped and which page to continue from (it does the same when a later page stays rate limited after its retries); any other call returns an error saying the budget ran out. If a PUT .../attendance is abandoned mid-request, the error says it may already have been applied.

  • 502, 503 and 504 are retried the same way for GET only; after three failures the error says the service may be unavailable, without the gateway's HTML. A PUT .../attendance is never retried after a gateway error, because it may already have been applied; the error says to check list_event_attendees first.

  • The legacy docs ask integrations to send an APPLICATION header identifying themselves; the server sends APPLICATION: sheepcrm-mcp/0.1.0.

  • A 200 whose body is not JSON (a proxy or login page in the way) is reported as an error naming SHEEPCRM_BASE_URL, never as an empty list. A paginated response without its documented list (bookings, segments) is reported as an error naming the keys received.

  • A rejected key (401 or 403) produces a message naming SHEEPCRM_API_KEY, where keys come from, SHEEPCRM_FLOCK and the possibility of missing permissions. A 404 says to check the id or URI and mentions that the legacy docs say a 404 can also mean bad credentials. Error text from sheepCRM is redacted like any other text, and the API key is replaced by [redacted] if a response echoes it. Both happen on the whole text before it is shortened for the message, so a key or email address that straddles the cut leaves no fragment.

Tests

npm test

The test suite (27 checks, about 45 seconds; most of it is real retry and time-budget waiting):

  1. Checks the spec (downloaded from sls-api.sheepcrm.com/.well-known/openapi.yaml to spec.yaml on the first run) against what this README says about it: OpenAPI 3.0.0, the sls-api.sheepcrm.com server, no securitySchemes and no security, no documented 200 body for the contact summary, member detail and find-person endpoints, the member-detail path written without the slash after /api/v2, and the documented enums for event state and attendance status. It also checks that the mock's API keys are marked as fake and appear nowhere in the spec. Then it validates every fixture record against the spec's component schemas (SearchResultsResponse, PersonMembershipAll, EventSingle, EventsList, EventAvailableTickets, EventAttendanceList, invoice, single_segment, all, count) with Ajv. The contact summary, member detail and find-person records have no published schema and no published example: their fixtures are shapes assumed for the tests (labelled as such in test/fixtures.mjs) and are not validated against anything; the server treats them as opaque records.

  2. Starts a local mock of the API that serves the fixtures with page/page_size pagination, requires Authorization: Bearer (401 without it, 403 for a wrong key or another flock), answers 404 for unknown ids and 400 for a missing q or name, and answers the first GET .../available_tickets with a 429 and Retry-After: 1. The mock's list, detail, write (UpdatedEventAttendance) and error (error: 400, 401, 403, 404, 429) responses are validated against the spec's schemas. No schema in the spec marks any field as required, so a schema pass only proves the types of the fields present; the documented top-level keys of the list, attendance, membership, write and count responses are asserted explicitly. The spec gives no example error body, so the mock's error texts are placeholders in the documented {error, description, type} shape, and the 404 on the events endpoints (which document only 200, 400 and 401) is an assumption.

  3. Starts the built server and drives it over stdio with the official MCP client: 24 checks covering the tool list and annotations; every read tool; the global search with its local resource filter; every documented find_person parameter passed through under its documented name, and the matching-rule note; withholding of contact, birth, address and health fields (including each withheld field of the person record in the legacy docs' Getting Started example, and birth-date keys named born and dateofbirth in a nested record) and redaction of emails, phone numbers, postcodes and dates of birth in the text of the undocumented records by default and their return on request; an object keyed by an email address redacted in the record and in the withheld_fields path, and one keyed by a card number replaced even on request; bank, card and payment fields absent and card and bank numbers in text replaced even on request; memberships sorted with computed days until the end and no payment fields in either mode; event paging across two pages ending at a short page with requests at least 200 ms apart, a state passed in the path ending at an empty page after a full one, max_results with continuation from the next page; event details without internal notes or ticket access codes, with the booking contact and venue postcode only on request; attendance counts, the local status filter and emails only on request; invoice buyer contact only on request and bank details, signature and PDF links never; the three segment endpoints, the refusal of name with include_inactive, and segment counts; the write gate with the variable unset and set to false; the PUT .../attendance query parameters validated against the spec's parameter schemas, no body, URI normalisation, and the local refusal of an organisation URI or another flock's URI; bad ids and URIs, an event state and attendance statuses outside the documented enums refused before any request; 404 messages; the 429 retry waiting for Retry-After in the seconds, fractional-seconds and HTTP-date forms and the 2 s fallback without the header, x-rate-limit-reset used when Retry-After is absent (and a reset above the cap giving up at once); giving up after three attempts a second apart on a persistent 429, a listing whose second page stays rate limited returning its first page with a note, and giving up at once on a Retry-After above the cap; with a 5-second budget, a listing rate limited on two pages returning its first page before the budget runs out, a single rate-limited call stopping before its next wait would pass the budget, a slow PUT abandoned at the budget with the "may already have been applied" advice, and out-of-range SHEEPCRM_TIME_BUDGET_S values refused at start-up; a 502 retried for a GET, three 503s reported with advice and without HTML, a 502 on the PUT not retried; a 200 with a non-JSON body, and one without the documented segments list, reported as errors; an API key echoed in an error body replaced by [redacted], and no fragment of it left when an echo straddles the point where the quoted text is cut (for a non-JSON 200 and for a 500); that every request carried Bearer <key>, the APPLICATION header and the configured flock, and matched a documented method and path (with the member-detail slash corrected as described under Status, and the organisation form of membership/all as the one named exception); the 403 message for a wrong key; and the server refusing to start with a missing or malformed SHEEPCRM_FLOCK.

Status

This is a working prototype. It has not yet been run against the live API, because it was built without a sheepCRM account (there is no self-serve trial; a test flock has to come from sheepCRM). Everything below comes from the published spec and legacy docs and should be confirmed on a real flock:

  • The shapes of GET /api/v2{contact_uri}summary, GET /api/v2/{bucket}/member/{uid}/detail and GET /search/v2/{bucket}/person. The spec documents no body for these; the mock's records are guesses. The server does not depend on their field names, but its default output is only as safe as the key-name list above, so check what a real record contains (in particular any free-form fields that hold personal data under a neutral name) before relying on the default view.

  • Memberships of an organisation. The spec documents membership/all for a person only ({person_uri}); the legacy docs' Contacts page documents GET /api/v2/{bucket}/{organisation|person}/{uid}/membership/all and the vendor's README route list includes GET /api/v2/{bucket}/organisation/{uid}/membership/all, but neither documents its response for an organisation. The server calls it for organisation URIs and assumes the same PersonMembershipAll shape.

  • The personal field names. The key-name list is checked against the person record in the legacy docs, which is a v1 API example; the v2 summary, member detail and find-person responses may use other names.

  • The member detail path. The spec writes it as /api/v2{bucket}/member/{uid}/detail (no slash after /api/v2), while the vendor's README lists /api/v2/{bucket}/member/{uid}/detail; the server calls the latter.

  • Whether a Bearer API key from Profile settings works on sls-api.sheepcrm.com for every endpoint used. The legacy docs show Bearer keys on api.sheepcrm.com/api/v1 and on the v2 people and segment examples; the spec declares no security scheme at all.

  • The status for a bad key (the spec says 401 for invalid credentials and 403 for missing privileges; the legacy docs say 403 for a problem with the key or permissions, and that 404 is "also returned on bad user credentials"), and the real error body shape.

  • Pagination. The spec documents page and page_size (events: defaults 1 and 250) but no total and no next link. The server asks for 100 per page (fewer when max_results is smaller) and takes a page shorter than that, or an empty one, as the end. If sheepCRM caps page_size below 100 without saying so, listings would stop after the first page; if it ignores page, pages would repeat.

  • Whether GET /events/v2/{bucket}/booking/{uid}/attendance and .../attendance/all differ (the spec describes both as "All the attendees for an event"; the server uses the first), and whether attendance lists are complete for large events (no pagination is documented).

  • GET /search/v2/{bucket}: how many results it returns and whether it pages (nothing is documented; max_results is applied locally), and the full set of resource values.

  • The find_person matching rules, which come from the legacy v1 docs; the spec says the v2 endpoint is the same.

  • The meaning of end_date on memberships (the last day of membership is assumed; days_until_end is computed from it in UTC) and of membership_record_status values (the spec lists no enum).

  • PUT .../attendance for a person who is not yet on the list (whether it adds them or refuses), and its errors.

  • 404s on the events and segments endpoints, which document only 200, 400 and 401.

  • Rate limiting: the real limits, the status of a throttled response, whether Retry-After is sent, and the exact format of x-rate-limit-reset (it is only used when it is a plain number; see Safety defaults).

  • Whether the APPLICATION header, documented for the v1 API, matters on v2.

Going to production

This version runs locally over stdio, with the user's own API key. For membership teams to connect from claude.ai or ChatGPT without handling keys, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by sheepCRM (its OAuth2 client registration is already documented), a run of the test suite against a real flock to settle the points above, formatters for the contact summary and member detail once their shapes are known, 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