simplepractice-mcp 0.4.1 → 1.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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +55438 -29706
- package/dist/tools/account.js +2 -1
- package/dist/tools/appointments.js +2 -2
- package/dist/tools/auth.js +8 -8
- package/dist/tools/billing.js +4 -4
- package/dist/tools/documents.js +8 -8
- package/dist/version.js +1 -1
- package/package.json +8 -7
- package/server.json +2 -2
package/dist/tools/account.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { minifiedResult, toolAnnotations } from '@chrischall/mcp-utils';
|
|
2
3
|
import { parseJsonString } from '../jsonapi.js';
|
|
3
4
|
/**
|
|
@@ -14,7 +15,7 @@ export function registerAccountTools(server, client) {
|
|
|
14
15
|
server.registerTool('simplepractice_get_account', {
|
|
15
16
|
description: 'The practice, the signed-in client, and every client this login can see. One portal login is a "client access" and may cover more than one client — a parent seeing two children, say — so clients is always a list.',
|
|
16
17
|
annotations: toolAnnotations({ readOnly: true }),
|
|
17
|
-
inputSchema: {},
|
|
18
|
+
inputSchema: z.object({}),
|
|
18
19
|
}, async () => {
|
|
19
20
|
const { records } = await client.list('/environment', {
|
|
20
21
|
include: 'currentPractice,currentClient,currentClientOptions,currentClientAccess',
|
|
@@ -25,7 +25,7 @@ export function registerAppointmentTools(server, client) {
|
|
|
25
25
|
server.registerTool('simplepractice_list_appointments', {
|
|
26
26
|
description: 'Appointments from the Client Portal. status "scheduled" returns confirmed/upcoming ones; "requested" returns those still awaiting the practice\'s confirmation. Pages by number.',
|
|
27
27
|
annotations: toolAnnotations({ readOnly: true }),
|
|
28
|
-
inputSchema: {
|
|
28
|
+
inputSchema: z.object({
|
|
29
29
|
status: z
|
|
30
30
|
.enum(['scheduled', 'requested'])
|
|
31
31
|
.default('scheduled')
|
|
@@ -33,7 +33,7 @@ export function registerAppointmentTools(server, client) {
|
|
|
33
33
|
page: z.number().int().positive().default(1),
|
|
34
34
|
pageSize: z.number().int().positive().max(PAGE_SIZE_MAX).default(PAGE_SIZE_MAX),
|
|
35
35
|
view: viewArg(),
|
|
36
|
-
},
|
|
36
|
+
}),
|
|
37
37
|
}, async ({ status, page, pageSize, view }) => {
|
|
38
38
|
const { records } = await client.list('/appointments', {
|
|
39
39
|
include: 'clinician,office,client',
|
package/dist/tools/auth.js
CHANGED
|
@@ -14,7 +14,7 @@ export function registerAuthTools(server, client) {
|
|
|
14
14
|
server.registerTool('simplepractice_session_status', {
|
|
15
15
|
description: 'Report whether this server holds a Client Portal session, for which practice, and how that practice was determined (from a sign-in link, from SIMPLEPRACTICE_PRACTICE, or remembered from the stored session). Reads local state only — makes no network call.',
|
|
16
16
|
annotations: toolAnnotations({ readOnly: true }),
|
|
17
|
-
inputSchema: {},
|
|
17
|
+
inputSchema: z.object({}),
|
|
18
18
|
}, async () => {
|
|
19
19
|
const host = client.knownPortalHost();
|
|
20
20
|
const session = client.getSession();
|
|
@@ -35,7 +35,7 @@ export function registerAuthTools(server, client) {
|
|
|
35
35
|
server.registerTool('simplepractice_request_sign_in_link', {
|
|
36
36
|
description: 'Ask SimplePractice to email a sign-in link to a Client Portal address. The portal has no password — this is how you sign in. Sends a real email and is rate-limited per email address AND per IP, so it requires confirm:true. A success does not prove the address has an account: the API answers identically for unknown addresses by design.',
|
|
37
37
|
annotations: toolAnnotations({ readOnly: false, idempotent: false }),
|
|
38
|
-
inputSchema: {
|
|
38
|
+
inputSchema: z.object({
|
|
39
39
|
email: z.string().email().describe('The email address the Client Portal is registered to.'),
|
|
40
40
|
practice: z
|
|
41
41
|
.string()
|
|
@@ -43,7 +43,7 @@ export function registerAuthTools(server, client) {
|
|
|
43
43
|
.optional()
|
|
44
44
|
.describe('The practice whose portal to sign in to — the slug ("achievebalancetherapy"), the host, or the portal URL. Only needed when this server does not know the practice yet; signing in with an emailed link teaches it, and it then remembers.'),
|
|
45
45
|
confirm: schemaConfirm,
|
|
46
|
-
},
|
|
46
|
+
}),
|
|
47
47
|
}, async ({ email, practice, confirm }) => {
|
|
48
48
|
if (!confirm) {
|
|
49
49
|
return minifiedResult({
|
|
@@ -75,24 +75,24 @@ export function registerAuthTools(server, client) {
|
|
|
75
75
|
server.registerTool('simplepractice_verify_sign_in_token', {
|
|
76
76
|
description: 'Exchange an emailed sign-in link (or the token in it) for a Client Portal session. Accepts the whole link or just the part after the "#". Prefer passing the WHOLE link: its address names the practice, so no practice has to be configured, and this server remembers it afterwards. Tokens are single-use and last 24 hours.',
|
|
77
77
|
annotations: toolAnnotations({ readOnly: false, idempotent: false }),
|
|
78
|
-
inputSchema: {
|
|
78
|
+
inputSchema: z.object({
|
|
79
79
|
link: z
|
|
80
80
|
.string()
|
|
81
81
|
.min(1)
|
|
82
82
|
.describe('The sign-in link from the email, or just the token after the "#".'),
|
|
83
|
-
},
|
|
83
|
+
}),
|
|
84
84
|
}, async ({ link }) => minifiedResult(await verifySignInToken(client, link)));
|
|
85
85
|
server.registerTool('simplepractice_verify_sign_in_pin', {
|
|
86
86
|
description: 'Exchange a 6-digit Client Portal sign-in PIN for a session, for practices that email a code instead of a link. Single-use.',
|
|
87
87
|
annotations: toolAnnotations({ readOnly: false, idempotent: false }),
|
|
88
|
-
inputSchema: {
|
|
88
|
+
inputSchema: z.object({
|
|
89
89
|
email: z.string().email().describe('The address the PIN was sent to.'),
|
|
90
90
|
pin: z.string().regex(/^\d{6}$/, 'The PIN is exactly 6 digits.'),
|
|
91
|
-
},
|
|
91
|
+
}),
|
|
92
92
|
}, async ({ email, pin }) => minifiedResult(await verifySignInPin(client, email, pin)));
|
|
93
93
|
server.registerTool('simplepractice_sign_out', {
|
|
94
94
|
description: 'Discard the stored Client Portal session from local state.',
|
|
95
95
|
annotations: toolAnnotations({ readOnly: false, idempotent: true }),
|
|
96
|
-
inputSchema: {},
|
|
96
|
+
inputSchema: z.object({}),
|
|
97
97
|
}, async () => minifiedResult({ signedOut: client.clearSession() }));
|
|
98
98
|
}
|
package/dist/tools/billing.js
CHANGED
|
@@ -34,7 +34,7 @@ export function registerBillingTools(server, client) {
|
|
|
34
34
|
server.registerTool('simplepractice_list_billing_items', {
|
|
35
35
|
description: 'Invoices, statements, superbills, receipts, or account history from the Client Portal. An empty list is a real answer — many practices bill entirely outside the portal. Pages by cursor: pass the returned nextCursor as "before".',
|
|
36
36
|
annotations: toolAnnotations({ readOnly: true }),
|
|
37
|
-
inputSchema: {
|
|
37
|
+
inputSchema: z.object({
|
|
38
38
|
kind: z
|
|
39
39
|
.enum(['invoice', 'statement', 'superbill', 'receipt', 'account-history'])
|
|
40
40
|
.default('invoice'),
|
|
@@ -44,7 +44,7 @@ export function registerBillingTools(server, client) {
|
|
|
44
44
|
.describe('Cursor for the next page — the nextCursor from a previous call.'),
|
|
45
45
|
pageSize: z.number().int().positive().max(PAGE_SIZE_MAX).default(PAGE_SIZE_MAX),
|
|
46
46
|
view: viewArg(),
|
|
47
|
-
},
|
|
47
|
+
}),
|
|
48
48
|
},
|
|
49
49
|
// `view` is destructured off, never forwarded: `client.list` turns whatever
|
|
50
50
|
// it is handed into a JSON:API query string, and a stray `view=compact`
|
|
@@ -74,7 +74,7 @@ export function registerBillingTools(server, client) {
|
|
|
74
74
|
server.registerTool('simplepractice_get_billing_overview', {
|
|
75
75
|
description: 'Balance due and per-category counts for the Client Portal account. Cheaper than paging the billing collections just to find out whether anything is there.',
|
|
76
76
|
annotations: toolAnnotations({ readOnly: true }),
|
|
77
|
-
inputSchema: { view: viewArg() },
|
|
77
|
+
inputSchema: z.object({ view: viewArg() }),
|
|
78
78
|
}, async ({ view }) => {
|
|
79
79
|
const overview = await loadClientRelationship(client, 'clientBillingOverview');
|
|
80
80
|
// Also un-projected: the overview is whatever `clientBillingOverview`
|
|
@@ -86,7 +86,7 @@ export function registerBillingTools(server, client) {
|
|
|
86
86
|
server.registerTool('simplepractice_list_payment_methods', {
|
|
87
87
|
description: 'Payment methods saved to the Client Portal — brand, last four digits, and expiry. No full card numbers.',
|
|
88
88
|
annotations: toolAnnotations({ readOnly: true }),
|
|
89
|
-
inputSchema: {},
|
|
89
|
+
inputSchema: z.object({}),
|
|
90
90
|
},
|
|
91
91
|
// No `view`: the response below IS a projection, hand-written down to five
|
|
92
92
|
// fields with knowledge of what a card record holds. Running the blind rung
|
package/dist/tools/documents.js
CHANGED
|
@@ -9,7 +9,7 @@ export function registerDocumentTools(server, client) {
|
|
|
9
9
|
server.registerTool('simplepractice_list_document_requests', {
|
|
10
10
|
description: 'Paperwork the practice has sent — consents, questionnaires, contact and insurance forms, Good Faith Estimates, shared files. Use outstandingOnly to see just what still needs the client\'s attention.',
|
|
11
11
|
annotations: toolAnnotations({ readOnly: true }),
|
|
12
|
-
inputSchema: {
|
|
12
|
+
inputSchema: z.object({
|
|
13
13
|
outstandingOnly: z
|
|
14
14
|
.boolean()
|
|
15
15
|
.default(false)
|
|
@@ -19,7 +19,7 @@ export function registerDocumentTools(server, client) {
|
|
|
19
19
|
.boolean()
|
|
20
20
|
.default(false)
|
|
21
21
|
.describe('Include the full document body/questions. Off by default — these are long.'),
|
|
22
|
-
},
|
|
22
|
+
}),
|
|
23
23
|
},
|
|
24
24
|
// No `view`: `items` below is a hand-written projection, and `includeBody`
|
|
25
25
|
// is a field the caller explicitly asked for. A blind rung run over that
|
|
@@ -61,10 +61,10 @@ export function registerDocumentTools(server, client) {
|
|
|
61
61
|
server.registerTool('simplepractice_get_document_request', {
|
|
62
62
|
description: 'One document request in full, including its body or its questions and the answers already given.',
|
|
63
63
|
annotations: toolAnnotations({ readOnly: true }),
|
|
64
|
-
inputSchema: {
|
|
64
|
+
inputSchema: z.object({
|
|
65
65
|
id: z.string().min(1).describe('The document request id.'),
|
|
66
66
|
view: viewArg(),
|
|
67
|
-
},
|
|
67
|
+
}),
|
|
68
68
|
},
|
|
69
69
|
// `view` is destructured off rather than passed on: the id is the only part
|
|
70
70
|
// of this input that may reach the request path.
|
|
@@ -86,9 +86,9 @@ export function registerDocumentTools(server, client) {
|
|
|
86
86
|
server.registerTool('simplepractice_list_documents', {
|
|
87
87
|
description: 'Files the practice has shared through the Client Portal.',
|
|
88
88
|
annotations: toolAnnotations({ readOnly: true }),
|
|
89
|
-
inputSchema: {
|
|
89
|
+
inputSchema: z.object({
|
|
90
90
|
pageSize: z.number().int().positive().max(PAGE_SIZE_MAX).default(PAGE_SIZE_MAX),
|
|
91
|
-
},
|
|
91
|
+
}),
|
|
92
92
|
},
|
|
93
93
|
// No `view`, and this one is the exception worth stating: the PRODUCT of
|
|
94
94
|
// this tool is the file references themselves. A practice that shares a
|
|
@@ -102,10 +102,10 @@ export function registerDocumentTools(server, client) {
|
|
|
102
102
|
server.registerTool('simplepractice_list_announcements', {
|
|
103
103
|
description: 'Announcements the practice has posted to the Client Portal. readAt is null on unread ones.',
|
|
104
104
|
annotations: toolAnnotations({ readOnly: true }),
|
|
105
|
-
inputSchema: {
|
|
105
|
+
inputSchema: z.object({
|
|
106
106
|
pageSize: z.number().int().positive().max(PAGE_SIZE_MAX).default(PAGE_SIZE_MAX),
|
|
107
107
|
view: viewArg(),
|
|
108
|
-
},
|
|
108
|
+
}),
|
|
109
109
|
}, async ({ pageSize, view }) => {
|
|
110
110
|
const { records } = await client.list('/announcements', { page: { size: pageSize } });
|
|
111
111
|
// Verbatim upstream records again. An announcement is text the practice
|
package/dist/version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Single source of truth for the server version. release-please rewrites the
|
|
3
3
|
* literal below; every other file imports VERSION rather than repeating it.
|
|
4
4
|
*/
|
|
5
|
-
export const VERSION = '0.
|
|
5
|
+
export const VERSION = '1.0.0'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simplepractice-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"mcpName": "io.github.chrischall/simplepractice-mcp",
|
|
6
6
|
"description": "SimplePractice Client Portal MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsc && npm run bundle",
|
|
29
|
-
"bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from \"module\"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js",
|
|
29
|
+
"bundle": "esbuild src/index.ts --bundle --alias:zod/v4=./node_modules/zod/v4/index.cjs --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from \"module\"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js",
|
|
30
30
|
"dev": "node --env-file=.env dist/index.js",
|
|
31
31
|
"typecheck": "tsc --noEmit",
|
|
32
32
|
"test": "npm run typecheck && vitest run",
|
|
@@ -34,17 +34,18 @@
|
|
|
34
34
|
"test:watch": "vitest"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@chrischall/mcp-utils": "^0.
|
|
38
|
-
"@modelcontextprotocol/
|
|
37
|
+
"@chrischall/mcp-utils": "^0.28.0",
|
|
38
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
39
39
|
"dotenv": "^17.4.2",
|
|
40
|
-
"zod": "^4.
|
|
40
|
+
"zod": "^4.6.2"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
43
44
|
"@types/node": "^26.0.0",
|
|
44
|
-
"@vitest/coverage-v8": "^
|
|
45
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
45
46
|
"esbuild": "^0.28.0",
|
|
46
47
|
"typescript": "^7.0.2",
|
|
47
|
-
"vitest": "^
|
|
48
|
+
"vitest": "^5.0.0",
|
|
48
49
|
"yaml": "^2.9.0"
|
|
49
50
|
}
|
|
50
51
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/simplepractice-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "1.0.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "simplepractice-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "1.0.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|