Carebit MCP server
by dragosh29
README.md
# Carebit MCP server
An [MCP](https://modelcontextprotocol.io) server that lets Claude, ChatGPT and other MCP clients work with a Carebit private-practice Organization: clinicians and their diaries, bookable slots, bookings, patients, invoices, payments, the staff task queue, services and locations, and (when enabled) creating staff tasks, creating bookings and cancelling bookings. It is built from Carebit's public developer documentation: the OpenAPI 3.1 document at `carebit.dev/openapi.json` and the guides on authentication, testing, pagination and filtering, idempotency and retries, validation errors and rate limits.
Once it's connected, someone at the practice can ask things like:
- "What does Dr Ada Example have on tomorrow, and are there gaps?"
- "When is the next free new-patient slot with Dr Example?"
- "Which invoices are overdue, and how much is outstanding?"
- "What was refunded this month?"
- "What's still open in the task queue for Jo?"
- With writes enabled: "Add a task for Kim to chase the insurer by Friday." / "Book Priya Shah a follow-up with Dr Example at 9 on the 12th." / "Cancel that booking, it was made in error."
## Tools
| Tool | What it does | API calls |
|---|---|---|
| `get_organization` | The Organization the credential belongs to (name, type, contact details, address, currency, time zone) and, by default, the token's granted scopes and developer project. | `GET /v1/organization`, `GET /v1/token` |
| `list_clinicians` | Clinicians with display name, title and specialty. Practice email addresses only on request. | `GET /v1/clinicians` |
| `get_clinician` | One clinician. | `GET /v1/clinicians/{id}` |
| `get_clinician_agenda` | A clinician's diary for up to 45 days: bookings, availability and unavailability intervals in time order. Patient names are shown; contact details and the booking's free-text information only on request. | `GET /v1/clinician_agenda` |
| `find_availability` | Bookable slots for a clinician and service variant: every slot in a date range (up to 45 days), or the next slot from a date (up to 8 months ahead). | `GET /v1/availability_slots` or `GET /v1/next_availability_slot` |
| `list_bookings` | Bookings in a time window (30 days, or 90 with `patient_id`), or recall bookings by status without a window, with the documented filters `clinician_id`, `patient_id`, `status`, `updated_since`. | `GET /v1/bookings` |
| `get_booking` | One booking: status, times, clinician, patient, service and variants, location, payor, recall and cancellation details. | `GET /v1/bookings/{id}` |
| `search_patients` | Connected patients with the documented exact-match filters `first_name`, `last_name`, `date_of_birth`, `email`, `phone_number` and `ids[]` (up to 25). The API has no partial-name search. | `GET /v1/patients` |
| `get_patient` | One patient. | `GET /v1/patients/{id}` |
| `list_invoices` | Invoices with status, totals, outstanding and paid amounts and line items; filters `booking_id`, `patient_id`. | `GET /v1/invoices` |
| `get_invoice` | One invoice with its line items. | `GET /v1/invoices/{id}` |
| `list_payments` | Payments with status, amount, method type, payor type and refunds; filters `patient_id`, `paid_at_from`, `paid_at_to`. | `GET /v1/payments` |
| `list_human_tasks` | The staff task queue, newest first; filters `patient_id`, `staff_member_id`, `document_id`, `is_completed`. | `GET /v1/human_tasks` |
| `list_services` | Services with duration, tax rate and their variants (the `service_variant_id` that availability and booking need); filter `is_bookable_online`. Names and descriptions go through the redaction unless asked. | `GET /v1/services` |
| `list_locations` | The Organization's locations with addresses. | `GET /v1/locations` |
| `create_human_task` | Adds a task to the queue with content, due date, urgency, optional patient, document and staff-member assignees. Writes only. | `POST /v1/human_tasks` |
| `create_booking` | Creates a diary booking for a patient, service and variant at a UTC start time, optionally with clinician, location, end time, status, remote flag and information fields. `notify_patient` defaults to `false` here (the API's own default is `true`). Recall bookings are not supported. Writes only. | `POST /v1/bookings` |
| `cancel_booking` | Cancels a booking with an optional documented reason code and note. Writes only, marked destructive. | `POST /v1/bookings/{booking_id}/cancellations` |
Not covered on purpose: staff members, payors, insurance companies and alternative payors, payment methods (stored cards), creating payments and refunds, creating invoices and quotes, patient creation and updates, patient connections, leads, letters, notes, photos, test results, digital forms, booking forms, lists, recall programmes, availability periods, reports, expirable files, transmissions, webhooks, `PATCH /v1/bookings/{id}` and `PATCH /v1/human_tasks/{id}`, and the organization search. The test suite asserts that the stored-card, payor, note, letter, test-result and report endpoints are never called.
## Setup
Requires Node 18 or later.
```bash
npm install
npm run build
```
You need an API credential for your Carebit Organization. As the authentication guide documents: sign in to Carebit as a staff member, go to **Settings > Developer platform**, create a project with the scopes you need, and create an API credential (a `client_id` and a `client_secret`) for it. The server exchanges them for a Bearer access token at `POST /oauth/token` (OAuth 2.0 client credentials, form-encoded), which the guide's example says lasts an hour (`expires_in: 3600`).
The read tools need these scopes on the project: `organization.read`, `clinicians.read`, `clinician_agenda.read`, `availability_slots.read`, `bookings.read`, `patients.read`, `invoices.read`, `payments.read`, `human_tasks.read`, `services.read`, `locations.read`. The write tools need `human_tasks.create`, `bookings.create` and `bookings.cancel`. A tool whose scope is missing reports which one.
**Claude Desktop:** add this to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"carebit": {
"command": "node",
"args": ["/absolute/path/to/carebit-mcp/dist/index.js"],
"env": { "CAREBIT_CLIENT_ID": "your-client-id", "CAREBIT_CLIENT_SECRET": "your-client-secret" }
}
}
}
```
**Claude Code:**
```bash
claude mcp add carebit -e CAREBIT_CLIENT_ID=your-client-id -e CAREBIT_CLIENT_SECRET=your-client-secret -- node /absolute/path/to/carebit-mcp/dist/index.js
```
| Variable | Required | Meaning |
|---|---|---|
| `CAREBIT_CLIENT_ID` | yes | The API credential's client ID, sent in the token request body. |
| `CAREBIT_CLIENT_SECRET` | yes | The API credential's client secret, sent in the token request body. Never logged or included in an error message. |
| `CAREBIT_SCOPE` | no | Space-separated scopes to request on the token, e.g. `bookings.read clinicians.read`. When unset the token receives the project's full scope set, as the guide documents. Every scope named must be on the project or the token endpoint answers `400 invalid_scope`. |
| `CAREBIT_ALLOW_WRITES` | no | `true` to register `create_human_task`, `create_booking` and `cancel_booking`. Off by default. |
| `CAREBIT_BASE_URL` | no | Defaults to `https://api.carebit.co`. Used by the tests. |
## Safety defaults
- Read-only unless `CAREBIT_ALLOW_WRITES=true`. Read tools carry the MCP `readOnlyHint` annotation; `cancel_booking` is marked destructive; the two create tools are not.
- Patient records are third-party health data. By default a patient's `email`, `phone_number`, `phone`, `mobile` (and their country codes), `date_of_birth`, `nhs_number`, `address_line_1`, `address_line_2`, `city`, `county`, `postcode`, `country_code`, `internal_patient_id` and `patient_portal_add_payment_method_url` are not returned; a clinician's `email` is not returned; a payor's name-and-address fields, `insurance_policy_number`, `insurance_authorization_code`, policy dates and `notes` are not returned; an invoice's `payment_url` (the Patient Portal page where that invoice can be paid) is not returned; and a booking's `information_for_patient` and `information_for_staff_members` (free text that may hold clinical detail) are withheld, with two booleans saying whether they exist. Patient names, sex and the Organization's own `patient_number` are returned, as are the Organization's own and its locations' contact details. In every other free-text field (names, specialties, task content, cancellation notes, invoice notes and line-item titles, payment and refund notes, service names and descriptions and variant descriptions, a booking's service name, and Carebit's own error messages) email addresses are replaced with `[email redacted]`, phone-number-like sequences with `[phone redacted]`, 10-digit NHS-number-shaped groups with `[number redacted]` and UK postcodes with `[postcode redacted]`. `include_contact_details=true` (on every read tool except `get_organization`, `find_availability` and `list_locations`, which return no personal data) returns all of it as stored, with one exception: a payment card number typed into free text (13 to 19 digits, optionally grouped by spaces or hyphens, that pass the Luhn check) is replaced with `[card number redacted]` whether or not contact details were requested, in every field this server returns, including the booking information and payor notes that are otherwise passed through as stored. That check catches full card numbers, not fragments: `card ending 4242` cannot be told from an ordinary number and is left alone, and one arbitrary 13-to-19-digit string in ten passes Luhn and is redacted too. The phone match is a heuristic: it covers international numbers written with `+` or `00` (including the `+44 (0)7700 …` form), UK numbers with a bracketed area code such as `(020) 7946 0958`, and UK-style `0…` numbers of 9 to 11 digits with spaces, dots or hyphens between groups; other digit strings that happen to start with `0` are redacted too, and any 10-digit group written `nnn nnn nnnn` or unbroken is redacted whether or not it is an NHS number, while UUIDs, timestamps and hyphenated references such as `INV-1001` or `POL-0001-000123` are left alone. Dates inside free text are not redacted.
- Stored cards are never read: `GET /v1/patients/{id}/payment_methods` is not called, a payment's `payment_method_id` is never returned, a full card number typed into any free text is redacted as above, and there are no payment, refund or invoice-creation tools. No file is ever downloaded.
- The access token is held in memory only, refreshed a minute before its documented expiry (or after half its lifetime when Carebit issues one shorter than two minutes, so it is still reused rather than fetched before every call), and fetched afresh once when a call answers 401; it never appears in logs or error messages, and neither does the client secret: should Carebit ever echo either in an error message, the value is replaced with `[redacted]` before the message is passed on. The refresh token Carebit issues alongside is not used: refresh tokens rotate on every use and would have to be stored, while a client-credentials grant can simply be repeated (the token endpoint allows 10 requests per minute per `client_id`). Only a JSON error message from the token endpoint is ever passed on, never its raw body; a non-JSON 200 from it is reported by its first 60 characters, redacted before they are cut.
- Every write carries an `Idempotency-Key` header holding a fresh version-4 UUID, as the idempotency guide requires on every POST create; the same key is kept across retries of that operation. A `409 idempotency_conflict` (the same key still in flight) is retried after `Retry-After` (1 second when absent, as documented) with the same key, at most twice. The same key is sent again on a `409 idempotency_conflict` retry, on a 429 retry and on the single retry after a token refresh (401), as the rate-limits guide requires for any write retry. The code reports a response that carries `Idempotency-Replayed: true` as "already created (replayed)", but the tests never reach the replay path, because the mock stores nothing for a request it answered 409, 429 or 401 (see Status).
- IDs are checked before any call is made: every identifier must look like a UUID (the spec types them all as RFC 4122 UUIDs). Dates take `YYYY-MM-DD` and real calendar dates only; times take `YYYY-MM-DDTHH:MM:SSZ` (UTC, the only form the API documents) or a bare date, which becomes the start of that UTC day, or its end for an upper bound. Offsets such as `+01:00` and forms such as `05/10/2026` are refused locally. The documented limits are enforced before a request: a diary or slot range of at most 45 days inclusive, a bookings window of at most 30 days (90 with `patient_id`), no window with a recall status and a window with every other status including `did_not_attend`. `create_booking` refuses an end before the start; `cancel_booking` only accepts the documented reason codes.
- Rate limits, from the rate-limits guide: 120 requests and 30 writes per minute per access token, 240 and 60 per developer project, 300 and 90 per Organization, 10 patient creates per token, 10 `/oauth/token` requests per minute per IP and per `client_id`, all on a rolling 60-second window at the Cloudflare edge; every 429 carries `Retry-After: 60`. Requests are spaced 500 ms apart, which keeps one server at the per-token ceiling; the project and Organization limits are shared with other integrations and are not modelled. A 429 is retried at most twice for any method, on the assumption that a request the edge rejected 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 a tool call stays under the MCP client's default 60-second request timeout: with Carebit's documented `Retry-After: 60` the call therefore gives up at once and the message says to wait 60 seconds. The cap is per request, not per tool call: a list can make up to 20 requests in one call.
- 502, 503 and 504 are retried the same way for `GET` and for `POST /oauth/token` only; when all three attempts fail the error says the service may be unavailable and to try again in a few minutes, without the gateway's HTML. A 500 and a connection failure are reported at once, without a retry, although the idempotency guide says to retry every 5xx and network failure. A `POST` is never retried after a gateway error, even though the idempotency guide says a retry with the same key is safe: this prototype does not repeat a write on the strength of documentation it could not test, and the error says to check with the matching list tool before repeating it (see Going to production).
- A 200 whose body is not JSON (a proxy or a login page in the way) is reported as an error naming `CAREBIT_BASE_URL`, never as an empty list or an empty record. Every body excerpt in an error message (60 characters of a non-JSON 200, 300 of a non-JSON error body) is redacted before it is cut, so a contact detail that straddles the cut cannot leak in part. A list page that says `has_more: true` without a usable `next_cursor` (the guide says the two always agree) is reported as incomplete with a note, not as the whole collection.
- Rejected client credentials produce a message that says which variables to fix and where credentials come from; a 401 that survives a token refresh says to check the credential and project; a 403 names the scope the endpoint needs; a 404 says the resource does not exist or is outside the Organization; a 422 passes on Carebit's message, code and, when several fields fail, every field-level error; and every error message, from the token endpoint included, carries the `Carebit-Developer-Platform-API-Request-Id` for support tickets whenever the response had the header (a gateway page from the edge may not carry it).
## Tests
```bash
npm test
```
The test suite:
1. Validates every fixture record against the component schemas in Carebit's published OpenAPI 3.1 document (`Organization`, `Token`, `Clinician`, `Location`, `Service`, `Patient`, `Booking`, `ClinicianAgendaItem`, `AvailabilitySlot`, `Invoice`, `Payment`, `HumanTask`) with Ajv 2020 and ajv-formats, including a negative control (a non-UUID id and an undeclared key are rejected, since every resource schema sets `additionalProperties: false`). It also checks that every scope the mock's project holds is one the spec defines and that the mock's client ID and secret are obviously fake values that do not appear in the spec. The spec is downloaded from `carebit.dev/openapi.json` to `spec.json` on the first run.
2. Starts a local mock of the API: `POST /oauth/token` with the documented form-encoded client-credentials grant (answering the `OAuthTokenResponse` shape with a refresh token and scope, `401 invalid_client` for wrong credentials, `400 invalid_scope` for a scope not on the project), `GET /v1/token`, a 401 for any API call without a token the mock issued, a 403 for a token without the endpoint's scope, the endpoints used here with the documented list envelope and cursor pagination (`limit` 1 to 100 with a 422 above, `cursor` from `next_cursor`, `has_more` false and `next_cursor` null on the last page; every page capped at four records so the suite pages), the documented filters (the bookings window rules, recall statuses without a window, exact patient filters including `ids[]`, the `paid_at` window omitting null `paid_at`, `is_completed`, `is_bookable_online`), the `Idempotency-Key` contract on writes (`400 idempotency_key_required`, replay with `Idempotency-Replayed: true`, `422 idempotency_key_reused`, an injectable `409 idempotency_conflict` with `Retry-After: 1`), the documented error envelope with field-level `errors`, 404s for unknown ids, a 422 for cancelling a cancelled booking, injected failures on any endpoint, and a one-off 429 on the first `GET /v1/services`. The second check validates the mock's token, list, record, write and error responses against the documented response schemas.
3. Starts the built server and drives it over stdio with the official MCP client: 30 checks (32 in the whole suite, 124 requests, about 70 seconds) covering every tool, tool annotations, the token fetched with the documented form body (no `scope` unless `CAREBIT_SCOPE` is set, and that value when it is) before the first API call and reused as a Bearer header, a revoked token refreshed once with the call retried, a token near expiry refreshed before it expires, a 60-second token reused rather than fetched before every call, cursor pagination with `limit` 100 following `next_cursor` to `has_more: false` and continuing from a returned cursor with the original filters on every page, every documented filter passed through exactly (`clinician_ids[]`, `start_date`/`end_date`, `clinician_id`/`service_variant_id`/`from_date`, `start_time_from`/`start_time_to` with bare dates converted, `clinician_id`, `patient_id`, `status`, `updated_since`, `first_name`, `last_name`, `date_of_birth`, `email`, `phone_number`, `ids[]` repeated, `booking_id`, `paid_at_from`/`paid_at_to`, `staff_member_id`, `document_id`, `is_completed`, `is_bookable_online`), the documented window and range limits refused before any request (a 31-day calendar month given as bare dates refused with the two exact windows to use instead, 30 bare days and an exact 30-day span accepted), redaction of contact details, NHS number, date of birth, address, third-party identifier, payment-method link, policy number, payor address, invoice payment link, stored-card id and the booking's free text by default and their return on request, and of emails, phone numbers, an NHS-shaped number and a UK postcode typed into specialties, task content, cancellation notes, invoice notes and payment notes, service names and descriptions, variant descriptions, line-item titles and a booking's service name redacted by default and returned as stored on request, full card numbers (Visa and Amex shapes, unbroken, spaced and hyphenated) redacted in task content, booking information, payor notes and the record a create tool echoes whether or not contact details were requested while a non-Luhn digit string and `ending 4242` stay, the three write bodies validated against the spec's request schemas (`POST /v1/bookings` through its `allOf`/`oneOf` diary branch) with a version-4 UUID `Idempotency-Key` that changes per call and is kept across a 409 retry, a 429 retry and the retry after a token refresh, the 422 for an already-cancelled booking passed on with Carebit's message and code, the 429 retry waiting for `Retry-After` in the seconds, fractional-seconds and HTTP-date forms and falling back to 2 s without the header, giving up after three attempts on a persistent 429 and at once on the documented `Retry-After: 60` with the request id in the message, a 502 retried for `GET` and never for `POST`, a `GET` failing three times with 503 reported with advice and without the gateway HTML (and with the request id and Carebit's message when the 503 carried them), a 200 with a non-JSON body reported as an error, body excerpts redacted before they are cut (an email address straddling the 60- and 300-character cuts), a list page with `has_more: true` and no cursor reported as incomplete, token-endpoint errors (400, 403, a non-JSON 200) carrying the request id and never the client secret even when the message echoes it, the write gate with the variable unset and set to `false`, the 401 for wrong client credentials naming the variables without echoing the secret, the 403 naming the missing scope, and that every request either carried the documented form body to `/oauth/token` with no `Authorization` header or carried a Bearer token the mock had actually minted (membership in the mock's issued set, not just the shape) to a documented method and path, that every `POST` carried an `Idempotency-Key`, that the secret never travelled in a query string, and that the stored-card, payor, note, letter, test-result, report and revoke endpoints were never called.
## Status
This is a working prototype. It has **not yet been run against the live API**, because it was built without a Carebit Organization or API credential (Carebit has no sandbox; the testing guide says to test against production with an example patient, and a trial Organization is provided after a booked demo). Everything below is taken from the published spec and guides and should be confirmed on a real account:
- The token endpoint: that client credentials sent as body fields (rather than HTTP Basic, which the spec also allows) are accepted, the exact status and `error.code` for wrong credentials (the spec documents 401; the mock's `invalid_client` code is the OAuth 2.0 convention), whether the API answers 401 for an expired token (the server refreshes a minute before the documented expiry and once on a 401 either way), and whether repeating the client-credentials grant every hour instead of using the refresh token has any side effect (the guide documents both grants; refresh tokens rotate on use).
- Error bodies: the spec documents the envelope and the idempotency, scope and rate-limit codes; the codes the mock uses for 401, 403 and 404 (`invalid_token`, `insufficient_scope`, `resource_missing`) are placeholders. The server switches on `idempotency_conflict` only and passes every other message through with contact details redacted.
- Pagination: the mock's cursor is opaque base64 and the server treats `next_cursor` as opaque, as documented; that the real cursor survives being sent back with the original filters and `limit` is assumed. `starting_after` is not used. What a `limit` between the page's actual size and 100 returns on a real account is not tested beyond the mock's cap.
- `GET /v1/clinician_agenda` and `GET /v1/availability_slots` answer the list envelope but document no `limit` or `cursor` parameter; the server makes one request and reports `has_more: true` as a note. Whether the real endpoints ever page, and how many items a 45-day diary holds, is unknown.
- The bookings window: whether `start_time_to` is compared inclusively as documented, what the API answers for a window of exactly 30 or 90 days, whether 30 days and 23:59:59 counts as exceeding 30 days (the server assumes it does, so a 31-day calendar month given as bare dates is refused with the two exact windows to use instead), and whether `patient_id` alone (without a window) is accepted for non-recall statuses (the server requires the window, as the description says).
- `GET /v1/next_availability_slot`: the 404 for "no slot within 8 months" cannot be told apart from an unknown clinician or variant by status alone; the server's message says both.
- The exact-match patient filters: whether `first_name` and `last_name` really ignore case and whether `phone_number` matching a national `07700 900123` against a stored E.164 number works as the parameter description says; the server sends the value as given.
- `POST /v1/bookings`: which `status` values are accepted on creation (the schema lists `arrived`, `confirmed`, `did_not_attend`, `unconfirmed`, `awaiting_recall`; the tool offers `unconfirmed` and `confirmed`), whether omitting `end_time` derives it from the service duration as the mock does, what the API's default `status` is, and whether `notify_patient: false` really suppresses every patient email. The mock answers 201; the spec documents 201.
- `POST /v1/bookings/{booking_id}/cancellations`: whether the Organization requires `cancellation_reason` (the tool leaves it optional and passes on the 422), which statuses can be cancelled, and that an attendance penalty invoice is applied as the description says.
- `POST /v1/human_tasks`: the API default of `is_remindable` when omitted (the mock uses `true`), and whether an unknown `assignee_id` answers 404 as the mock does.
- Retries: a 429 is retried on the assumption that a rate-limited request was not processed at the edge (with the same `Idempotency-Key` on a write), and a 409 `idempotency_conflict` is retried with the same key as the guide says; neither has been observed on a live account. A write is never retried after a 5xx although the guide says a retry with the same key is safe, and a 500 or a network failure is never retried for any method.
- The `Idempotency-Replayed` header is read from the response and would be reported as "already created (replayed)"; no test reaches that path (the mock stores nothing for a write it answered 409, 429 or 401), and whether a replay of a create answers 201 (the guide says the original status) is untested.
- Rate limits: the 500 ms spacing is derived from the documented 120 requests per minute per token and has not been measured against the edge; the write limit of 30 per minute is not separately throttled. The token refresh margin (a minute, or half the lifetime of a token shorter than two minutes) assumes Carebit's `expires_in` is accurate.
- The `Carebit-Developer-Platform-API-Request-Id` header is read case-insensitively and included in error messages, from the token endpoint and the retry paths included, whenever the response carried it; its presence on every response, and on token-endpoint and edge (429, 5xx) responses in particular, is as documented and untested.
## Going to production
This version runs locally over stdio, with the practice's own API credential. For practices to connect from claude.ai or ChatGPT without handling credentials, the next step is a remote server (Streamable HTTP) behind OAuth, hosted by Carebit, and then a listing in the Claude and ChatGPT connector directories. A production version, tested on a real Organization, would also retry writes after a 5xx with the same `Idempotency-Key` as the guide documents, use the refresh grant, cover the staff-member, payor, note and letter endpoints, and add the `PATCH` tools for completing tasks and confirming, arriving and rescheduling bookings.
## 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
ActivityMaintained
ResponsivenessNo issues