simplepractice-mcp 1.1.0 → 1.1.1
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 +85 -14
- package/dist/tools/auth.js +4 -4
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "SimplePractice Client Portal tools for Claude",
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.1"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "simplepractice",
|
|
14
14
|
"source": "./",
|
|
15
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": "1.1.
|
|
16
|
+
"version": "1.1.1",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Chris Chall",
|
|
19
19
|
"url": "https://github.com/chrischall"
|
package/dist/bundle.js
CHANGED
|
@@ -6,6 +6,20 @@ var __export = (target, all) => {
|
|
|
6
6
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
// node_modules/@chrischall/mcp-utils/dist/cancel/index.js
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
11
|
+
var storage = new AsyncLocalStorage();
|
|
12
|
+
function withCallSignal(signal, fn, request) {
|
|
13
|
+
return signal ? storage.run({ signal, ...request ? { request } : {} }, fn) : fn();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// node_modules/@chrischall/mcp-utils/dist/caller/index.js
|
|
17
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
18
|
+
var storage2 = new AsyncLocalStorage2();
|
|
19
|
+
function withCallerCapabilities(capabilities, fn) {
|
|
20
|
+
return capabilities ? storage2.run(capabilities, fn) : fn();
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
// node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
10
24
|
var __create = Object.create;
|
|
11
25
|
var __defProp2 = Object.defineProperty;
|
|
@@ -34472,17 +34486,73 @@ Hint: ${err.hint}`);
|
|
|
34472
34486
|
}
|
|
34473
34487
|
throw err;
|
|
34474
34488
|
}
|
|
34489
|
+
function mcpRequestFrom(args) {
|
|
34490
|
+
const ctx = args.at(-1);
|
|
34491
|
+
if (typeof ctx !== "object" || ctx === null)
|
|
34492
|
+
return void 0;
|
|
34493
|
+
const req = ctx.mcpReq;
|
|
34494
|
+
if (typeof req !== "object" || req === null)
|
|
34495
|
+
return void 0;
|
|
34496
|
+
return typeof req.notify === "function" ? req : void 0;
|
|
34497
|
+
}
|
|
34498
|
+
function callSignalFrom(args) {
|
|
34499
|
+
const ctx = args.at(-1);
|
|
34500
|
+
if (typeof ctx !== "object" || ctx === null)
|
|
34501
|
+
return void 0;
|
|
34502
|
+
const req = ctx.mcpReq;
|
|
34503
|
+
if (typeof req !== "object" || req === null)
|
|
34504
|
+
return void 0;
|
|
34505
|
+
const signal = req.signal;
|
|
34506
|
+
return signal instanceof AbortSignal ? signal : void 0;
|
|
34507
|
+
}
|
|
34508
|
+
function declaredCapabilitiesFrom(server) {
|
|
34509
|
+
const inner = server.server;
|
|
34510
|
+
if (typeof inner !== "object" || inner === null)
|
|
34511
|
+
return void 0;
|
|
34512
|
+
const read = inner.getClientCapabilities;
|
|
34513
|
+
if (typeof read !== "function")
|
|
34514
|
+
return void 0;
|
|
34515
|
+
try {
|
|
34516
|
+
const declared = read.call(inner);
|
|
34517
|
+
return typeof declared === "object" && declared !== null ? declared : void 0;
|
|
34518
|
+
} catch {
|
|
34519
|
+
return void 0;
|
|
34520
|
+
}
|
|
34521
|
+
}
|
|
34475
34522
|
function surfaceToolHints(server) {
|
|
34476
34523
|
const register2 = server.registerTool.bind(server);
|
|
34477
|
-
server.registerTool = (name, config2, cb) => register2(name, config2, (...args) =>
|
|
34478
|
-
|
|
34479
|
-
|
|
34480
|
-
|
|
34481
|
-
|
|
34482
|
-
|
|
34483
|
-
|
|
34484
|
-
|
|
34485
|
-
|
|
34524
|
+
server.registerTool = (name, config2, cb) => register2(name, config2, (...args) => (
|
|
34525
|
+
// THE CALLER'S CANCELLATION, made ambient for the whole handler
|
|
34526
|
+
// (`cancel/index.ts`). The SDK delivers it and the fleet ignored it:
|
|
34527
|
+
// measured against @modelcontextprotocol/server 2.0.0, a cancelled
|
|
34528
|
+
// call aborts `ctx.mcpReq.signal` with the caller's reason and the
|
|
34529
|
+
// handler runs to completion regardless — so the HTTP request stays
|
|
34530
|
+
// in flight, the child keeps burning metered CPU, and the upstream
|
|
34531
|
+
// keeps being hit for somebody who has gone. claude.ai sent 101
|
|
34532
|
+
// cancellations in the week to 2026-09-20.
|
|
34533
|
+
//
|
|
34534
|
+
// Here because this is the one wrapper every tool already passes
|
|
34535
|
+
// through: threading the signal by hand would mean editing several
|
|
34536
|
+
// hundred handlers and missing exactly the ones nobody edits.
|
|
34537
|
+
withCallSignal(callSignalFrom(args), () => (
|
|
34538
|
+
// WHAT THE CALLER CAN DO, made ambient for the same reason and in
|
|
34539
|
+
// the same place (`caller/index.ts`). Only the 2025-era half needs
|
|
34540
|
+
// the wrapper — a 2026-07-28 request answers for itself off its own
|
|
34541
|
+
// envelope — but a guarded tool that cannot tell whether the caller
|
|
34542
|
+
// can be shown a prompt returns one the SDK then refuses to deliver,
|
|
34543
|
+
// and that refusal reaches the caller as an unexplained protocol
|
|
34544
|
+
// error it had no chance to catch.
|
|
34545
|
+
withCallerCapabilities(declaredCapabilitiesFrom(server), () => {
|
|
34546
|
+
let result;
|
|
34547
|
+
try {
|
|
34548
|
+
result = cb(...args);
|
|
34549
|
+
} catch (err) {
|
|
34550
|
+
return hintResultOrRethrow(err);
|
|
34551
|
+
}
|
|
34552
|
+
return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
|
|
34553
|
+
})
|
|
34554
|
+
), mcpRequestFrom(args))
|
|
34555
|
+
));
|
|
34486
34556
|
}
|
|
34487
34557
|
async function createMcpServer(opts) {
|
|
34488
34558
|
const server = new McpServer({ name: opts.name, version: opts.version }, { supportedProtocolVersions: [...SERVER_PROTOCOL_VERSIONS] });
|
|
@@ -34659,12 +34729,13 @@ function toolAnnotations(opts = {}) {
|
|
|
34659
34729
|
...opts.title !== void 0 ? { title: opts.title } : {},
|
|
34660
34730
|
readOnlyHint: opts.readOnly ?? true,
|
|
34661
34731
|
...opts.idempotent !== void 0 ? { idempotentHint: opts.idempotent } : {},
|
|
34732
|
+
...opts.destructive !== void 0 ? { destructiveHint: opts.destructive } : {},
|
|
34662
34733
|
...opts.openWorld !== void 0 ? { openWorldHint: opts.openWorld } : {}
|
|
34663
34734
|
};
|
|
34664
34735
|
}
|
|
34665
34736
|
|
|
34666
34737
|
// src/version.ts
|
|
34667
|
-
var VERSION = "1.1.
|
|
34738
|
+
var VERSION = "1.1.1";
|
|
34668
34739
|
|
|
34669
34740
|
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
34670
34741
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
|
|
@@ -35251,7 +35322,7 @@ function registerAuthTools(server, client2) {
|
|
|
35251
35322
|
"simplepractice_request_sign_in_link",
|
|
35252
35323
|
{
|
|
35253
35324
|
description: "Ask SimplePractice to email a sign-in link to a Client Portal address. The portal has no password \u2014 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.",
|
|
35254
|
-
annotations: toolAnnotations({ readOnly: false, idempotent: false }),
|
|
35325
|
+
annotations: toolAnnotations({ readOnly: false, idempotent: false, destructive: true }),
|
|
35255
35326
|
inputSchema: external_exports.object({
|
|
35256
35327
|
email: external_exports.string().email().describe("The email address the Client Portal is registered to."),
|
|
35257
35328
|
practice: external_exports.string().min(1).optional().describe(
|
|
@@ -35291,7 +35362,7 @@ function registerAuthTools(server, client2) {
|
|
|
35291
35362
|
"simplepractice_verify_sign_in_token",
|
|
35292
35363
|
{
|
|
35293
35364
|
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.',
|
|
35294
|
-
annotations: toolAnnotations({ readOnly: false, idempotent: false }),
|
|
35365
|
+
annotations: toolAnnotations({ readOnly: false, idempotent: false, destructive: true }),
|
|
35295
35366
|
inputSchema: external_exports.object({
|
|
35296
35367
|
link: external_exports.string().min(1).describe('The sign-in link from the email, or just the token after the "#".')
|
|
35297
35368
|
})
|
|
@@ -35302,7 +35373,7 @@ function registerAuthTools(server, client2) {
|
|
|
35302
35373
|
"simplepractice_verify_sign_in_pin",
|
|
35303
35374
|
{
|
|
35304
35375
|
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.",
|
|
35305
|
-
annotations: toolAnnotations({ readOnly: false, idempotent: false }),
|
|
35376
|
+
annotations: toolAnnotations({ readOnly: false, idempotent: false, destructive: true }),
|
|
35306
35377
|
inputSchema: external_exports.object({
|
|
35307
35378
|
email: external_exports.string().email().describe("The address the PIN was sent to."),
|
|
35308
35379
|
pin: external_exports.string().regex(/^\d{6}$/, "The PIN is exactly 6 digits.")
|
|
@@ -35314,7 +35385,7 @@ function registerAuthTools(server, client2) {
|
|
|
35314
35385
|
"simplepractice_sign_out",
|
|
35315
35386
|
{
|
|
35316
35387
|
description: "Discard the stored Client Portal session from local state.",
|
|
35317
|
-
annotations: toolAnnotations({ readOnly: false, idempotent: true }),
|
|
35388
|
+
annotations: toolAnnotations({ readOnly: false, idempotent: true, destructive: false }),
|
|
35318
35389
|
inputSchema: external_exports.object({})
|
|
35319
35390
|
},
|
|
35320
35391
|
async () => minifiedResult({ signedOut: client2.clearSession() })
|
package/dist/tools/auth.js
CHANGED
|
@@ -34,7 +34,7 @@ export function registerAuthTools(server, client) {
|
|
|
34
34
|
});
|
|
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
|
-
annotations: toolAnnotations({ readOnly: false, idempotent: false }),
|
|
37
|
+
annotations: toolAnnotations({ readOnly: false, idempotent: false, destructive: true }),
|
|
38
38
|
inputSchema: z.object({
|
|
39
39
|
email: z.string().email().describe('The email address the Client Portal is registered to.'),
|
|
40
40
|
practice: z
|
|
@@ -74,7 +74,7 @@ export function registerAuthTools(server, client) {
|
|
|
74
74
|
});
|
|
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
|
-
annotations: toolAnnotations({ readOnly: false, idempotent: false }),
|
|
77
|
+
annotations: toolAnnotations({ readOnly: false, idempotent: false, destructive: true }),
|
|
78
78
|
inputSchema: z.object({
|
|
79
79
|
link: z
|
|
80
80
|
.string()
|
|
@@ -84,7 +84,7 @@ export function registerAuthTools(server, client) {
|
|
|
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
|
-
annotations: toolAnnotations({ readOnly: false, idempotent: false }),
|
|
87
|
+
annotations: toolAnnotations({ readOnly: false, idempotent: false, destructive: true }),
|
|
88
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.'),
|
|
@@ -92,7 +92,7 @@ export function registerAuthTools(server, client) {
|
|
|
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
|
-
annotations: toolAnnotations({ readOnly: false, idempotent: true }),
|
|
95
|
+
annotations: toolAnnotations({ readOnly: false, idempotent: true, destructive: false }),
|
|
96
96
|
inputSchema: z.object({}),
|
|
97
97
|
}, async () => minifiedResult({ signedOut: client.clearSession() }));
|
|
98
98
|
}
|
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 = '1.1.
|
|
5
|
+
export const VERSION = '1.1.1'; // x-release-please-version
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simplepractice-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
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)",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"test:watch": "vitest"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@chrischall/mcp-utils": "^
|
|
37
|
+
"@chrischall/mcp-utils": "^2.2.0",
|
|
38
38
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
39
39
|
"dotenv": "^17.4.2",
|
|
40
40
|
"zod": "^4.6.2"
|
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": "1.1.
|
|
9
|
+
"version": "1.1.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "simplepractice-mcp",
|
|
14
|
-
"version": "1.1.
|
|
14
|
+
"version": "1.1.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|