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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +83 -16
- package/dist/client.js +25 -12
- package/dist/version.js +1 -1
- package/package.json +3 -3
- 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.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.
|
|
16
|
+
"version": "1.1.4",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Chris Chall",
|
|
19
19
|
"url": "https://github.com/chrischall"
|
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 = /([?&](?:
|
|
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 =
|
|
34368
|
-
|
|
34369
|
-
|
|
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
|
-
|
|
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 ??
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
34983
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
35181
|
+
this.openStore().add(session);
|
|
35115
35182
|
return session;
|
|
35116
35183
|
}
|
|
35117
35184
|
clearSession() {
|
|
35118
35185
|
const host = this.knownPortalHost();
|
|
35119
|
-
return host ? this.
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
37
|
+
"@chrischall/mcp-utils": "^2.4.0",
|
|
38
38
|
"@modelcontextprotocol/server": "^2.0.0",
|
|
39
39
|
"dotenv": "^18.0.0",
|
|
40
|
-
"zod": "^4.6.
|
|
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.
|
|
9
|
+
"version": "1.1.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "simplepractice-mcp",
|
|
14
|
-
"version": "1.1.
|
|
14
|
+
"version": "1.1.4",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|