simplepractice-mcp 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +26 -0
- package/.claude-plugin/plugin.json +13 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/auth.js +79 -0
- package/dist/bundle.js +32350 -0
- package/dist/client.js +177 -0
- package/dist/config.js +58 -0
- package/dist/index.js +26 -0
- package/dist/jsonapi.js +91 -0
- package/dist/tools/account.js +48 -0
- package/dist/tools/appointments.js +54 -0
- package/dist/tools/auth.js +67 -0
- package/dist/tools/billing.js +88 -0
- package/dist/tools/documents.js +94 -0
- package/dist/version.js +5 -0
- package/package.json +48 -0
- package/server.json +34 -0
- package/skills/simplepractice/SKILL.md +74 -0
- package/skills/simplepractice-fpx/SKILL.md +260 -0
- package/skills/simplepractice-fpx/references/requests.md +398 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "simplepractice",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Chris Chall",
|
|
5
|
+
"url": "https://github.com/chrischall"
|
|
6
|
+
},
|
|
7
|
+
"metadata": {
|
|
8
|
+
"description": "SimplePractice Client Portal tools for Claude",
|
|
9
|
+
"version": "0.0.0"
|
|
10
|
+
},
|
|
11
|
+
"plugins": [
|
|
12
|
+
{
|
|
13
|
+
"name": "simplepractice",
|
|
14
|
+
"source": "./",
|
|
15
|
+
"description": "Read a SimplePractice Client Portal — appointments, invoices and superbills, documents to sign, and practice announcements. Signs in with the portal's own passwordless emailed link; requests go straight to the portal's JSON:API over your own session.",
|
|
16
|
+
"version": "0.0.0",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Chris Chall",
|
|
19
|
+
"url": "https://github.com/chrischall"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/chrischall/simplepractice-mcp",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"keywords": ["simplepractice", "client-portal", "therapy", "appointments", "billing"]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "simplepractice",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "SimplePractice Client Portal — appointments, billing, documents, and announcements",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Chris Chall",
|
|
7
|
+
"url": "https://github.com/chrischall"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/chrischall/simplepractice-mcp",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"mcpServers": "./.mcp.json",
|
|
12
|
+
"skills": "./skills/"
|
|
13
|
+
}
|
package/.mcp.json
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Hall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# simplepractice-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for the **SimplePractice Client Portal** — the side a practice's
|
|
4
|
+
*clients* log into, not the clinician side. Appointments, billing, paperwork,
|
|
5
|
+
and announcements, read over the portal's own JSON:API.
|
|
6
|
+
|
|
7
|
+
> Developed and maintained by AI (Claude Code). Use at your own discretion.
|
|
8
|
+
|
|
9
|
+
## What it reads
|
|
10
|
+
|
|
11
|
+
| Tool | What it gives you |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `simplepractice_get_account` | practice, current client, every client this login covers, cancellation policy, feature permissions |
|
|
14
|
+
| `simplepractice_list_appointments` | scheduled or requested appointments, with clinician and location |
|
|
15
|
+
| `simplepractice_list_billing_items` | invoices · statements · superbills · receipts · account history |
|
|
16
|
+
| `simplepractice_get_billing_overview` | balance due and per-category counts |
|
|
17
|
+
| `simplepractice_list_payment_methods` | saved cards — brand, last four, expiry |
|
|
18
|
+
| `simplepractice_list_document_requests` | paperwork sent to you, with an outstanding-only filter |
|
|
19
|
+
| `simplepractice_get_document_request` | one request in full, with its questions and answers |
|
|
20
|
+
| `simplepractice_list_documents` | files the practice has shared |
|
|
21
|
+
| `simplepractice_list_announcements` | practice announcements, with unread counts |
|
|
22
|
+
| `simplepractice_session_status` · `_request_sign_in_link` · `_verify_sign_in_token` · `_verify_sign_in_pin` · `_sign_out` | sign-in |
|
|
23
|
+
|
|
24
|
+
Everything is read-only. Cancelling, signing, and paying happen in the portal.
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install -g simplepractice-mcp
|
|
30
|
+
export SIMPLEPRACTICE_PRACTICE=achievebalancetherapy # or the full host
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`SIMPLEPRACTICE_PRACTICE` is the practice's portal address — the slug or the
|
|
34
|
+
whole `<practice>.clientsecure.me` host from the link your provider emailed.
|
|
35
|
+
|
|
36
|
+
| Variable | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `SIMPLEPRACTICE_PRACTICE` | **required** — portal slug or host |
|
|
39
|
+
| `SIMPLEPRACTICE_SESSION_FILE` | optional — session path (default `~/.simplepractice-mcp/session.json`, written `0600`) |
|
|
40
|
+
|
|
41
|
+
## Signing in
|
|
42
|
+
|
|
43
|
+
The Client Portal has **no password**. SimplePractice emails a one-time link
|
|
44
|
+
(or a 6-digit PIN); you trade it for a session cookie:
|
|
45
|
+
|
|
46
|
+
1. `simplepractice_request_sign_in_link { email, confirm: true }`
|
|
47
|
+
2. Open the email, copy the link.
|
|
48
|
+
3. `simplepractice_verify_sign_in_token { link }` — pass the whole link; the
|
|
49
|
+
token is its `#` fragment and the tool extracts it.
|
|
50
|
+
|
|
51
|
+
Links are single-use and last 24 hours. The request endpoint is rate-limited
|
|
52
|
+
per address **and** per IP, which is why sending is confirm-gated — a retry
|
|
53
|
+
loop locks you out of the only way in. There is no refresh token: when the
|
|
54
|
+
session lapses, you sign in again.
|
|
55
|
+
|
|
56
|
+
Because that flow needs nothing but HTTP and your inbox, this server has no
|
|
57
|
+
browser dependency and can run anywhere.
|
|
58
|
+
|
|
59
|
+
## Without the server
|
|
60
|
+
|
|
61
|
+
`skills/simplepractice-fpx` does the same reads with `curl`, either signing in
|
|
62
|
+
by magic link or lifting the session cookie from a browser tab with
|
|
63
|
+
[`fpx`](https://www.npmjs.com/package/@fetchproxy/cli).
|
|
64
|
+
|
|
65
|
+
## Notes from building this
|
|
66
|
+
|
|
67
|
+
The portal is an Ember app that ships **public sourcemaps**, so its models,
|
|
68
|
+
adapters and routes are readable directly — `docs/SIMPLEPRACTICE-API.md`
|
|
69
|
+
records the endpoints and the traps, all confirmed against a live portal:
|
|
70
|
+
|
|
71
|
+
- The SPA catch-all answers **HTTP 200 with `text/html`** for any path the API
|
|
72
|
+
does not define. `/cards` and `/client-billing-overviews` look like working,
|
|
73
|
+
empty endpoints and are not endpoints at all — both are `include`
|
|
74
|
+
relationships of `/clients/<id>`.
|
|
75
|
+
- `hasDocumentPdf`, a card's `isDefault`, and the client's `permissions` blob
|
|
76
|
+
are all **strings**, not booleans or objects.
|
|
77
|
+
- Billing pages by *cursor* (`page[before]` = a row's `cursorId`), appointments
|
|
78
|
+
page by *number*. The two are not interchangeable.
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
npm install
|
|
84
|
+
npm run build
|
|
85
|
+
npm test # 151 tests
|
|
86
|
+
npm run test:coverage # 100% enforced
|
|
87
|
+
npm run typecheck # vitest does not run tsc — this does
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { CookieJar, McpToolError } from '@chrischall/mcp-utils';
|
|
2
|
+
/** The Rails/Devise session cookie the portal authenticates with. */
|
|
3
|
+
export const SESSION_COOKIE = 'simplepractice-session';
|
|
4
|
+
/**
|
|
5
|
+
* Ask SimplePractice to email a sign-in link.
|
|
6
|
+
*
|
|
7
|
+
* The response always reports a 24-hour lifetime and always succeeds for a
|
|
8
|
+
* well-formed address — deliberately, so that it cannot be used to probe
|
|
9
|
+
* whether an email has an account. A 202 is therefore NOT evidence the address
|
|
10
|
+
* is registered, and the tool description says so.
|
|
11
|
+
*/
|
|
12
|
+
export async function requestSignInLink(client, email) {
|
|
13
|
+
const { document } = await client.request('/sign-in-tokens', {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
anonymous: true,
|
|
16
|
+
body: {
|
|
17
|
+
data: { type: 'sign-in-tokens', attributes: { email, expiresIn: '15 minutes' } },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const data = Array.isArray(document.data) ? document.data[0] : document.data;
|
|
21
|
+
const expiresIn = data?.attributes?.expiresIn ?? '24 hours';
|
|
22
|
+
return { expiresIn };
|
|
23
|
+
}
|
|
24
|
+
function extractToken(raw) {
|
|
25
|
+
const trimmed = raw.trim();
|
|
26
|
+
// Users paste the whole emailed link about as often as just the token, and
|
|
27
|
+
// the token is the URL FRAGMENT — a browser never sends it, so there is no
|
|
28
|
+
// way to recover it from anything but the link text itself.
|
|
29
|
+
const hash = trimmed.indexOf('#');
|
|
30
|
+
// A link with NO fragment is not a token-bearing link: the token lives only
|
|
31
|
+
// in the fragment, so accepting the bare URL here would POST the whole URL
|
|
32
|
+
// as if it were the token and report a confusing upstream rejection.
|
|
33
|
+
if (hash < 0 && /^https?:\/\//i.test(trimmed)) {
|
|
34
|
+
throw new McpToolError('That sign-in link has no token in it.', {
|
|
35
|
+
hint: 'The token is the part after the "#". Copy the link straight out of the email — some mail clients drop the fragment when they rewrite links, in which case open the email in a browser and copy the address from the address bar.',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const token = hash >= 0 ? trimmed.slice(hash + 1) : trimmed;
|
|
39
|
+
if (!token || /\s/.test(token) || token.includes('/')) {
|
|
40
|
+
throw new McpToolError('That does not look like a sign-in token.', {
|
|
41
|
+
hint: 'Paste either the whole link from the email or just the part after the "#". The token is the URL fragment — following the link in a browser will not reveal it to anything else.',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return token;
|
|
45
|
+
}
|
|
46
|
+
async function establishSession(client, attributes) {
|
|
47
|
+
const path = `/sessions/${attributes.type}`;
|
|
48
|
+
const { document, setCookie } = await client.request(path, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
anonymous: true,
|
|
51
|
+
body: { data: { type: 'sessions', attributes } },
|
|
52
|
+
});
|
|
53
|
+
const data = Array.isArray(document.data) ? document.data[0] : document.data;
|
|
54
|
+
const status = data?.meta?.status ?? 'unknown';
|
|
55
|
+
if (status !== 'verified') {
|
|
56
|
+
throw new McpToolError(`SimplePractice did not accept the sign-in: ${status}.`, {
|
|
57
|
+
hint: status === 'expired'
|
|
58
|
+
? 'Sign-in links last 24 hours. Request a new one.'
|
|
59
|
+
: 'Sign-in tokens and PINs are single-use. Request a new one rather than reusing the last.',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const jar = new CookieJar();
|
|
63
|
+
jar.absorb(setCookie);
|
|
64
|
+
const cookie = jar.get(SESSION_COOKIE);
|
|
65
|
+
if (!cookie) {
|
|
66
|
+
throw new McpToolError('Sign-in verified but no session cookie came back.', {
|
|
67
|
+
hint: `Expected a ${SESSION_COOKIE} cookie on the response. If SimplePractice has renamed it, simplepractice-mcp needs updating.`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
client.saveSession(`${SESSION_COOKIE}=${cookie}`);
|
|
71
|
+
return { status, signedIn: true };
|
|
72
|
+
}
|
|
73
|
+
export function verifySignInToken(client, linkOrToken) {
|
|
74
|
+
return establishSession(client, { type: 'token', token: extractToken(linkOrToken) });
|
|
75
|
+
}
|
|
76
|
+
export function verifySignInPin(client, email, pin) {
|
|
77
|
+
return establishSession(client, { type: 'pin', email, pin });
|
|
78
|
+
}
|
|
79
|
+
export { extractToken as _extractToken };
|