simplepractice-mcp
# simplepractice-mcp
MCP server for the **SimplePractice Client Portal** — the side a practice's
*clients* log into, not the clinician side. Appointments, billing, paperwork,
and announcements, read over the portal's own JSON:API.
> Developed and maintained by AI (Claude Code). Use at your own discretion.
## What it reads
| Tool | What it gives you |
|---|---|
| `simplepractice_get_account` | practice, current client, every client this login covers, cancellation policy, feature permissions |
| `simplepractice_list_appointments` | scheduled or requested appointments, with clinician and location |
| `simplepractice_list_billing_items` | invoices · statements · superbills · receipts · account history |
| `simplepractice_get_billing_overview` | balance due and per-category counts |
| `simplepractice_list_payment_methods` | saved cards — brand, last four, expiry |
| `simplepractice_list_document_requests` | paperwork sent to you, with an outstanding-only filter |
| `simplepractice_get_document_request` | one request in full, with its questions and answers |
| `simplepractice_list_documents` | files the practice has shared |
| `simplepractice_list_announcements` | practice announcements, with unread counts |
| `simplepractice_session_status` · `_request_sign_in_link` · `_verify_sign_in_token` · `_verify_sign_in_pin` · `_sign_out` | sign-in |
| `simplepractice_healthcheck` | Verify credentials and upstream reachability; reports failures as data, not exceptions |
Everything is read-only. Cancelling, signing, and paying happen in the portal.
The reads that answer with a SimplePractice record rather than a projection —
appointments, billing items, the billing overview, one document request,
announcements — take a `view`. It defaults to `compact`, which returns the slim
projection where this server has one and otherwise drops logo and avatar URLs a
model cannot see; `view: "full"` returns the record untouched.
`simplepractice_list_documents` deliberately takes none: what it returns is the
file reference, and a shared scan is a `.jpg`.
## Setup
```sh
npm install -g simplepractice-mcp
```
There is nothing to configure. The practice comes from your sign-in link.
| Variable | |
|---|---|
| `SIMPLEPRACTICE_PRACTICE` | optional — pins the server to one practice (slug or host) |
| `SIMPLEPRACTICE_SESSION_FILE` | optional — session path (default `~/.simplepractice-mcp/session.json`, written `0600`) |
## Signing in
The Client Portal has **no password**. SimplePractice emails a one-time link
(or a 6-digit PIN); you trade it for a session cookie:
1. Open the email your provider sent, copy the link.
2. `simplepractice_verify_sign_in_token { link }` — pass the **whole** link.
The link is `https://<practice>.clientsecure.me/sign-in/token#<TOKEN>`, so one
paste carries both halves of what the server needs: the token is the `#`
fragment, and the host names the practice. Nothing is hardcoded, and the
stored session remembers the practice for every later run —
`simplepractice_session_status` reports which practice is in play and whether
it came from a link, the environment variable, or the saved session.
To have a fresh link sent rather than using one you already have, name the
practice once:
```
simplepractice_request_sign_in_link { email, practice: "achievebalancetherapy" }
```
Sending asks you to confirm first (see [Confirmations](#confirmations)).
`practice` can be omitted whenever the server already knows the practice —
from an earlier sign-in, or from `SIMPLEPRACTICE_PRACTICE`.
Two sign-in links name no practice, and fall back to whichever one is already
known: the mobile-app variant SimplePractice sends
(`https://clientsecure.me/client-portal-api/sign-in/token#<TOKEN>`, pointed at
the bare apex), and a bare token pasted without its link. A link on any host
outside `*.clientsecure.me` is never adopted — the token is not sent there.
Links are single-use — replaying one answers
`401 "Authorization has already been used or expired"` — and last 24 hours. The
request endpoint is rate-limited per address **and** per IP, which is why
sending asks for confirmation first: a retry loop locks you out of the only way
in. There is no refresh token; when the session lapses, you sign in again.
The whole chain is verified end to end against a live portal — request, the
emailed link, the exchange returning `verified` plus a session cookie, and an
authenticated read with that new session.
Because that flow needs nothing but HTTP and your inbox, this server has no
browser dependency and can run anywhere.
## Confirmations
`simplepractice_request_sign_in_link` sends a real email, so it asks you to
confirm first. On a client that can show a confirmation prompt (Claude Code) you
get the prompt, unless `MCP_CONFIRM_ELICITATION=off`. On one that cannot (claude.ai, Claude Desktop), the first call
sends nothing and returns a preview — the address, the practice — plus a
`confirmToken`; only a repeat call with that token, and the same arguments,
sends. A token works once, and a changed address or practice is refused.
| variable | default | |
|---|---|---|
| `MCP_CONFIRM_MODE` | `ask-user` | What a write does on a client that cannot show a confirmation prompt (claude.ai, Claude Desktop). `ask-user`: two steps — the first call does nothing and returns a preview plus a token, and the model must get your approval in chat before calling again with it. `auto`: the same two steps, but the model may use the token after reviewing the preview itself. `refuse`: writes are refused on such clients. A client that can show prompts (Claude Code) always gets the real prompt, unless `MCP_CONFIRM_ELICITATION=off`. An unrecognised value is treated as `refuse`. |
| `MCP_CONFIRM_ELICITATION` | `on` | `off` never shows a confirmation prompt, so every client gets the `MCP_CONFIRM_MODE` path. Set it for a client that claims to support prompts but never shows one (the write hangs — opencode 2.0.x). Any other value stays `on`, with a warning on stderr. |
| `MCP_CONFIRM_TTL_SECONDS` | `600` | How long a token stays valid. |
| `MCP_CONFIRM_SECRET` | random per process | Signing key; set it only if tokens must survive a server restart. On mcp-host the host supplies a stable per-child key (`MCP_HOST_CONFIRM_SECRET`) and spent tokens are recorded under `MCP_DATA_DIR`, so an approval survives an idle restart. |
## Without the server
`skills/simplepractice-fpx` does the same reads with `curl`, either signing in
by magic link or lifting the session cookie from a browser tab with
[`fpx`](https://www.npmjs.com/package/@fetchproxy/cli).
## Notes from building this
The portal is an Ember app that ships **public sourcemaps**, so its models,
adapters and routes are readable directly — `docs/SIMPLEPRACTICE-API.md`
records the endpoints and the traps, all confirmed against a live portal:
- The SPA catch-all answers **HTTP 200 with `text/html`** for any path the API
does not define. `/cards` and `/client-billing-overviews` look like working,
empty endpoints and are not endpoints at all — both are `include`
relationships of `/clients/<id>`.
- `hasDocumentPdf`, a card's `isDefault`, and the client's `permissions` blob
are all **strings**, not booleans or objects.
- Billing pages by *cursor* (`page[before]` = a row's `cursorId`), appointments
page by *number*. The two are not interchangeable.
## Development
```sh
npm install
npm run build
npm test # 214 tests
npm run test:coverage # 100% enforced
npm run typecheck # vitest does not run tsc — this does
```
## License
MIT
TDQS
Scored across 15 tools
Most tools target clearly distinct resources and actions, but list_documents vs list_document_requests could be confused by name, and healthcheck vs session_status both feel like status checks. Descriptions are strong enough to resolve these in practice.
All tools share the simplepractice_ prefix and mostly follow a verb_noun pattern like list_*, get_*, and verify_sign_in_*. healthcheck and session_status break the pattern slightly, but overall naming is readable and predictable.
15 tools is at the upper edge of the ideal range, but each tool covers a meaningful portal area: authentication, account, appointments, billing, documents, document requests, and announcements. There is no obvious redundancy.
The toolset provides solid read-only coverage of Client Portal workflows, including auth lifecycle, account details, and list/get operations across major data categories. It lacks write/submit actions like completing a document request or paying an invoice, and some lists have no detail getter, but those are workable gaps rather than fatal ones.