simplepractice-mcp 1.1.2 → 1.1.4

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.2"
9
+ "version": "1.1.4"
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.2",
16
+ "version": "1.1.4",
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.2",
3
+ "version": "1.1.4",
4
4
  "description": "SimplePractice Client Portal — appointments, billing, documents, and announcements",
5
5
  "author": {
6
6
  "name": "Chris Chall",
package/dist/bundle.js CHANGED
@@ -34362,13 +34362,28 @@ var API_KEY_RE = new RegExp([
34362
34362
  "whsec_[A-Za-z0-9]{16,}\\b"
34363
34363
  // webhook signing secret (Stripe-style)
34364
34364
  ].map((p) => `\\b${p}`).join("|"), "g");
34365
- var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
34365
+ var QUERY_SECRET_RE = /([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
34366
+ var OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
34367
+ var FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
34368
+ var HEADER_SECRET_RE = /(\b(?:x[-_][a-z0-9_-]*?(?:api[-_]?key|token|secret|auth)[a-z0-9_-]*|api[-_]?key)\s*:\s*)[^\s,;"'<>`]+/gi;
34366
34369
  var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
34367
- var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
34368
- var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
34369
- var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
34370
+ var JSON_SECRET_KEYS = [
34371
+ "(?:access|refresh|id|auth|session|bearer|csrf|xsrf)[_-]?token",
34372
+ "client[_-]?secret",
34373
+ "api[_-]?secret",
34374
+ "(?:x[_-])?api[_-]?key",
34375
+ "private[_-]?key",
34376
+ "x[_-][a-z0-9_-]*?(?:api[_-]?key|token|secret|auth)[a-z0-9_-]*",
34377
+ "(?:proxy-)?authorization",
34378
+ "password",
34379
+ "passwd",
34380
+ "secret",
34381
+ "token"
34382
+ ].join("|");
34383
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")(?:[^"\\\\]|\\\\.)*(")`, "gi");
34384
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')(?:[^'\\\\]|\\\\.)*(')`, "gi");
34370
34385
  function redactSecrets(text) {
34371
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
34386
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
34372
34387
  }
34373
34388
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
34374
34389
  const str = text === null || text === void 0 ? "" : String(text);
@@ -34560,13 +34575,47 @@ async function createMcpServer(opts) {
34560
34575
  }
34561
34576
  return server;
34562
34577
  }
34578
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 5e3;
34579
+ var DEFAULT_REPEAT_SIGNAL_GRACE_MS = 500;
34563
34580
  function withGracefulShutdown(target, opts = {}) {
34564
34581
  const shouldExit = opts.exit ?? true;
34582
+ if (opts.timeoutMs !== void 0 && !(Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0)) {
34583
+ throw new RangeError("withGracefulShutdown: timeoutMs must be a finite number greater than 0.");
34584
+ }
34585
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
34586
+ if (opts.repeatSignalGraceMs !== void 0 && !(Number.isFinite(opts.repeatSignalGraceMs) && opts.repeatSignalGraceMs >= 0)) {
34587
+ throw new RangeError("withGracefulShutdown: repeatSignalGraceMs must be a finite number >= 0.");
34588
+ }
34589
+ const repeatGraceMs = opts.repeatSignalGraceMs ?? DEFAULT_REPEAT_SIGNAL_GRACE_MS;
34565
34590
  let shuttingDown = false;
34591
+ let firstSignalAt = 0;
34592
+ let exited = false;
34593
+ const exitOnce = () => {
34594
+ if (exited)
34595
+ return;
34596
+ exited = true;
34597
+ process.exit(0);
34598
+ };
34566
34599
  const handler = (signal) => {
34567
- if (shuttingDown)
34600
+ if (shuttingDown) {
34601
+ if (Date.now() - firstSignalAt < repeatGraceMs)
34602
+ return;
34603
+ if (shouldExit) {
34604
+ console.error(`[mcp-utils] second ${signal} during shutdown \u2014 exiting now`);
34605
+ exitOnce();
34606
+ }
34568
34607
  return;
34608
+ }
34569
34609
  shuttingDown = true;
34610
+ firstSignalAt = Date.now();
34611
+ let timer;
34612
+ if (shouldExit) {
34613
+ timer = setTimeout(() => {
34614
+ console.error(`[mcp-utils] graceful shutdown on ${signal} did not finish in ${timeoutMs}ms \u2014 exiting`);
34615
+ exitOnce();
34616
+ }, timeoutMs);
34617
+ timer.unref?.();
34618
+ }
34570
34619
  void (async () => {
34571
34620
  try {
34572
34621
  if (opts.onSignal)
@@ -34575,8 +34624,10 @@ function withGracefulShutdown(target, opts = {}) {
34575
34624
  } catch (err) {
34576
34625
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34577
34626
  } finally {
34627
+ if (timer !== void 0)
34628
+ clearTimeout(timer);
34578
34629
  if (shouldExit)
34579
- process.exit(0);
34630
+ exitOnce();
34580
34631
  }
34581
34632
  })();
34582
34633
  };
@@ -34718,9 +34769,12 @@ var pageSchema = {
34718
34769
  page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
34719
34770
  };
34720
34771
  function toolAnnotations(opts = {}) {
34772
+ if (opts.readOnly === true && opts.destructive === true) {
34773
+ throw new Error("toolAnnotations: readOnly: true contradicts destructive: true \u2014 a destructive tool is not read-only.");
34774
+ }
34721
34775
  return {
34722
34776
  ...opts.title !== void 0 ? { title: opts.title } : {},
34723
- readOnlyHint: opts.readOnly ?? true,
34777
+ readOnlyHint: opts.readOnly ?? opts.destructive === void 0,
34724
34778
  ...opts.idempotent !== void 0 ? { idempotentHint: opts.idempotent } : {},
34725
34779
  ...opts.destructive !== void 0 ? { destructiveHint: opts.destructive } : {},
34726
34780
  ...opts.openWorld !== void 0 ? { openWorldHint: opts.openWorld } : {}
@@ -34728,7 +34782,7 @@ function toolAnnotations(opts = {}) {
34728
34782
  }
34729
34783
 
34730
34784
  // src/version.ts
34731
- var VERSION = "1.1.2";
34785
+ var VERSION = "1.1.4";
34732
34786
 
34733
34787
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
34734
34788
  import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
@@ -34973,14 +35027,27 @@ function buildQuery(params) {
34973
35027
  return pairs.join("&");
34974
35028
  }
34975
35029
  var SimplePracticeClient = class {
34976
- store;
35030
+ /**
35031
+ * The session store, as it is on disk NOW.
35032
+ *
35033
+ * Another server process (Claude Desktop beside Claude Code) shares the
35034
+ * session file, and `SessionStore` reads it only in its constructor, then
35035
+ * rewrites the whole file from its in-memory Map on every add/remove. Held
35036
+ * for the life of the process, that snapshot would write a session another
35037
+ * process signed out of straight back to disk, or drop one it just created.
35038
+ * So by default every access opens the store afresh; an injected store
35039
+ * (tests) is used as given.
35040
+ */
35041
+ openStore;
34977
35042
  fetchImpl;
34978
35043
  /** A practice learned at runtime — from a sign-in link, or named on a tool call. */
34979
35044
  adoptedHost = null;
34980
35045
  constructor(opts = {}) {
34981
35046
  this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
34982
- this.store = opts.store ?? new SessionStore({
34983
- filePath: sessionFilePath(),
35047
+ const injected = opts.store;
35048
+ const filePath = sessionFilePath();
35049
+ this.openStore = injected ? () => injected : () => new SessionStore({
35050
+ filePath,
34984
35051
  keyOf: (session) => session.host,
34985
35052
  normalizeKey: (key) => key.toLowerCase()
34986
35053
  });
@@ -35025,7 +35092,7 @@ var SimplePracticeClient = class {
35025
35092
  */
35026
35093
  mostRecentSessionHost() {
35027
35094
  let newest = null;
35028
- for (const session of this.store.list()) {
35095
+ for (const session of this.openStore().list()) {
35029
35096
  if (!newest || session.createdAt > newest.createdAt) newest = session;
35030
35097
  }
35031
35098
  return newest?.host ?? null;
@@ -35106,17 +35173,17 @@ var SimplePracticeClient = class {
35106
35173
  }
35107
35174
  getSession() {
35108
35175
  const host = this.knownPortalHost();
35109
- return host ? this.store.get(host) : null;
35176
+ return host ? this.openStore().get(host) : null;
35110
35177
  }
35111
35178
  saveSession(cookie) {
35112
35179
  const host = this.requireConfig();
35113
35180
  const session = { host, cookie, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
35114
- this.store.add(session);
35181
+ this.openStore().add(session);
35115
35182
  return session;
35116
35183
  }
35117
35184
  clearSession() {
35118
35185
  const host = this.knownPortalHost();
35119
- return host ? this.store.remove(host) : false;
35186
+ return host ? this.openStore().remove(host) : false;
35120
35187
  }
35121
35188
  requireSession() {
35122
35189
  const session = this.getSession();
package/dist/client.js CHANGED
@@ -20,19 +20,32 @@ export function buildQuery(params) {
20
20
  return pairs.join('&');
21
21
  }
22
22
  export class SimplePracticeClient {
23
- store;
23
+ /**
24
+ * The session store, as it is on disk NOW.
25
+ *
26
+ * Another server process (Claude Desktop beside Claude Code) shares the
27
+ * session file, and `SessionStore` reads it only in its constructor, then
28
+ * rewrites the whole file from its in-memory Map on every add/remove. Held
29
+ * for the life of the process, that snapshot would write a session another
30
+ * process signed out of straight back to disk, or drop one it just created.
31
+ * So by default every access opens the store afresh; an injected store
32
+ * (tests) is used as given.
33
+ */
34
+ openStore;
24
35
  fetchImpl;
25
36
  /** A practice learned at runtime — from a sign-in link, or named on a tool call. */
26
37
  adoptedHost = null;
27
38
  constructor(opts = {}) {
28
39
  this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
29
- this.store =
30
- opts.store ??
31
- new SessionStore({
32
- filePath: sessionFilePath(),
33
- keyOf: (session) => session.host,
34
- normalizeKey: (key) => key.toLowerCase(),
35
- });
40
+ const injected = opts.store;
41
+ const filePath = sessionFilePath();
42
+ this.openStore = injected
43
+ ? () => injected
44
+ : () => new SessionStore({
45
+ filePath,
46
+ keyOf: (session) => session.host,
47
+ normalizeKey: (key) => key.toLowerCase(),
48
+ });
36
49
  }
37
50
  /**
38
51
  * Which practice this server is talking to, and how it found out.
@@ -76,7 +89,7 @@ export class SimplePracticeClient {
76
89
  */
77
90
  mostRecentSessionHost() {
78
91
  let newest = null;
79
- for (const session of this.store.list()) {
92
+ for (const session of this.openStore().list()) {
80
93
  if (!newest || session.createdAt > newest.createdAt)
81
94
  newest = session;
82
95
  }
@@ -159,12 +172,12 @@ export class SimplePracticeClient {
159
172
  }
160
173
  getSession() {
161
174
  const host = this.knownPortalHost();
162
- return host ? this.store.get(host) : null;
175
+ return host ? this.openStore().get(host) : null;
163
176
  }
164
177
  saveSession(cookie) {
165
178
  const host = this.requireConfig();
166
179
  const session = { host, cookie, createdAt: new Date().toISOString() };
167
- this.store.add(session);
180
+ this.openStore().add(session);
168
181
  return session;
169
182
  }
170
183
  clearSession() {
@@ -172,7 +185,7 @@ export class SimplePracticeClient {
172
185
  // Not knowing the practice is the same outcome as having no session for
173
186
  // it: nothing to sign out of. Throwing would make sign-out the one tool
174
187
  // that fails when it has nothing to do.
175
- return host ? this.store.remove(host) : false;
188
+ return host ? this.openStore().remove(host) : false;
176
189
  }
177
190
  requireSession() {
178
191
  const session = this.getSession();
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.2'; // x-release-please-version
5
+ export const VERSION = '1.1.4'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simplepractice-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
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,10 +34,10 @@
34
34
  "test:watch": "vitest"
35
35
  },
36
36
  "dependencies": {
37
- "@chrischall/mcp-utils": "^2.2.0",
37
+ "@chrischall/mcp-utils": "^2.4.0",
38
38
  "@modelcontextprotocol/server": "^2.0.0",
39
39
  "dotenv": "^18.0.0",
40
- "zod": "^4.6.2"
40
+ "zod": "^4.6.5"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@modelcontextprotocol/client": "^2.0.0",
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.2",
9
+ "version": "1.1.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "simplepractice-mcp",
14
- "version": "1.1.2",
14
+ "version": "1.1.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },