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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "SimplePractice Client Portal tools for Claude",
9
- "version": "1.1.0"
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.0",
16
+ "version": "1.1.1",
17
17
  "author": {
18
18
  "name": "Chris Chall",
19
19
  "url": "https://github.com/chrischall"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simplepractice",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "SimplePractice Client Portal — appointments, billing, documents, and announcements",
5
5
  "author": {
6
6
  "name": "Chris Chall",
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
- let result;
34479
- try {
34480
- result = cb(...args);
34481
- } catch (err) {
34482
- return hintResultOrRethrow(err);
34483
- }
34484
- return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
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.0";
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() })
@@ -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.0'; // x-release-please-version
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.0",
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": "^1.0.0",
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.0",
9
+ "version": "1.1.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "simplepractice-mcp",
14
- "version": "1.1.0",
14
+ "version": "1.1.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },