@chrischall/myatriumhealth-mcp 0.4.3 → 0.4.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 +86 -23
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Angi — find home-service pros by trade and city, read ratings, reviews and pro profiles. Angi serves its pages only to a real browser, so requests route through the user's own angi.com tab via the fetchproxy browser extension, reusing their existing session. The trade/city taxonomy is read from Angi's public sitemaps directly.",
|
|
10
|
-
"version": "0.4.
|
|
10
|
+
"version": "0.4.4"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "MyAtriumHealth",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Read MyAtriumHealth (Epic MyChart) records — test results, medications, allergies, immunizations, health issues, visits — by relaying requests through the user's signed-in browser tab via the fetchproxy bridge.",
|
|
18
|
-
"version": "0.4.
|
|
18
|
+
"version": "0.4.4",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myatriumhealth-mcp",
|
|
3
3
|
"displayName": "MyAtriumHealth",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.4",
|
|
5
5
|
"description": "MyAtriumHealth (Epic MyChart) access for Claude — test results, medications, allergies, immunizations, visits",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/dist/bundle.js
CHANGED
|
@@ -9763,7 +9763,7 @@ var require_websocket = __commonJS({
|
|
|
9763
9763
|
var http = __require("http");
|
|
9764
9764
|
var net = __require("net");
|
|
9765
9765
|
var tls = __require("tls");
|
|
9766
|
-
var { randomBytes:
|
|
9766
|
+
var { randomBytes: randomBytes3, createHash } = __require("crypto");
|
|
9767
9767
|
var { Duplex, Readable } = __require("stream");
|
|
9768
9768
|
var { URL: URL2 } = __require("url");
|
|
9769
9769
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -10301,7 +10301,7 @@ var require_websocket = __commonJS({
|
|
|
10301
10301
|
}
|
|
10302
10302
|
}
|
|
10303
10303
|
const defaultPort = isSecure ? 443 : 80;
|
|
10304
|
-
const key =
|
|
10304
|
+
const key = randomBytes3(16).toString("base64");
|
|
10305
10305
|
const request = isSecure ? https.request : http.request;
|
|
10306
10306
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
10307
10307
|
let perMessageDeflate;
|
|
@@ -40115,7 +40115,7 @@ function parseChallengeContext(html) {
|
|
|
40115
40115
|
if (flag("AllowSMS", tfs)) channels.push("sms");
|
|
40116
40116
|
if (flag("AllowEmail", tfs)) channels.push("email");
|
|
40117
40117
|
if (flag("AllowTotp", tfs)) channels.push("totp");
|
|
40118
|
-
const
|
|
40118
|
+
const rm2 = /RememberMeSettings\s*:\s*\{([\s\S]{0,300}?)\}/.exec(html)?.[1] ?? "";
|
|
40119
40119
|
const workflow = /Workflow\s*:\s*(\d+)/.exec(tfs)?.[1];
|
|
40120
40120
|
const email3 = str("DisplayEmail");
|
|
40121
40121
|
const phone = str("DisplayPhone");
|
|
@@ -40124,8 +40124,8 @@ function parseChallengeContext(html) {
|
|
|
40124
40124
|
...email3 !== void 0 ? { displayEmail: email3 } : {},
|
|
40125
40125
|
...phone !== void 0 ? { displayPhone: phone } : {},
|
|
40126
40126
|
...workflow !== void 0 ? { workflow: Number(workflow) } : {},
|
|
40127
|
-
rememberMeEnabled: flag("Enabled",
|
|
40128
|
-
enrollDeviceTracking: flag("EnrollDeviceTracking",
|
|
40127
|
+
rememberMeEnabled: flag("Enabled", rm2),
|
|
40128
|
+
enrollDeviceTracking: flag("EnrollDeviceTracking", rm2)
|
|
40129
40129
|
};
|
|
40130
40130
|
}
|
|
40131
40131
|
var b64 = (s) => Buffer.from(s, "utf8").toString("base64");
|
|
@@ -42534,9 +42534,29 @@ var SessionState = class {
|
|
|
42534
42534
|
outboundSeq = 0;
|
|
42535
42535
|
lastInboundSeq = 0;
|
|
42536
42536
|
inflightInbound = /* @__PURE__ */ new Set();
|
|
42537
|
+
saturationWarned = false;
|
|
42537
42538
|
constructor(sessionKey) {
|
|
42538
42539
|
this.sessionKey = sessionKey;
|
|
42539
42540
|
}
|
|
42541
|
+
/**
|
|
42542
|
+
* Whether the caller holding this claim verdict should log saturation now.
|
|
42543
|
+
* True only on the TRANSITION into it: a line per dropped frame turned a
|
|
42544
|
+
* flood — or a set that never drains — into a log flood amplifying the very
|
|
42545
|
+
* condition it reported (#376). An `'ok'` claim re-arms the latch, since it
|
|
42546
|
+
* is the one signal every caller sees that the set has drained below the
|
|
42547
|
+
* bound; a replay needs no capacity and leaves it alone. Latched per
|
|
42548
|
+
* session, so a renegotiated one starts armed. A set hovering at the bound
|
|
42549
|
+
* can still alternate ok/saturated, but that logs at the rate frames finish
|
|
42550
|
+
* opening, not the rate they arrive.
|
|
42551
|
+
*/
|
|
42552
|
+
saturationWarningDue(claim2) {
|
|
42553
|
+
if (claim2 === "ok")
|
|
42554
|
+
this.saturationWarned = false;
|
|
42555
|
+
if (claim2 !== "saturated" || this.saturationWarned)
|
|
42556
|
+
return false;
|
|
42557
|
+
this.saturationWarned = true;
|
|
42558
|
+
return true;
|
|
42559
|
+
}
|
|
42540
42560
|
nextOutboundSeq() {
|
|
42541
42561
|
this.outboundSeq += 1;
|
|
42542
42562
|
return this.outboundSeq;
|
|
@@ -42547,19 +42567,21 @@ var SessionState = class {
|
|
|
42547
42567
|
* the first `await` of the receive path, or a duplicate frame read in the
|
|
42548
42568
|
* same pass will be judged against a counter neither frame has moved yet.
|
|
42549
42569
|
*
|
|
42550
|
-
*
|
|
42551
|
-
* duplicate of one still in flight
|
|
42552
|
-
*
|
|
42570
|
+
* Anything but `'ok'` means "not this frame's to process": `'replay'` for
|
|
42571
|
+
* a spent seq or a duplicate of one still in flight, `'saturated'` when the
|
|
42572
|
+
* in-flight bound is full. Replay is checked first, since refusing a stale
|
|
42573
|
+
* seq needs no capacity. Every `'ok'` MUST be answered by exactly one
|
|
42574
|
+
* {@link commitInboundSeq} or {@link releaseInboundSeq}.
|
|
42553
42575
|
*/
|
|
42554
42576
|
claimInboundSeq(seq) {
|
|
42555
42577
|
if (seq <= this.lastInboundSeq)
|
|
42556
|
-
return
|
|
42578
|
+
return "replay";
|
|
42557
42579
|
if (this.inflightInbound.has(seq))
|
|
42558
|
-
return
|
|
42580
|
+
return "replay";
|
|
42559
42581
|
if (this.inflightInbound.size >= MAX_INFLIGHT_INBOUND_SEQS)
|
|
42560
|
-
return
|
|
42582
|
+
return "saturated";
|
|
42561
42583
|
this.inflightInbound.add(seq);
|
|
42562
|
-
return
|
|
42584
|
+
return "ok";
|
|
42563
42585
|
}
|
|
42564
42586
|
/**
|
|
42565
42587
|
* Give a claim back without spending the seq — for a frame that did not
|
|
@@ -42654,11 +42676,12 @@ async function awaitSessionReady(ready, opts) {
|
|
|
42654
42676
|
}
|
|
42655
42677
|
|
|
42656
42678
|
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
42657
|
-
import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
42679
|
+
import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
42658
42680
|
import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
42659
42681
|
|
|
42660
42682
|
// node_modules/@fetchproxy/server/dist/identity.js
|
|
42661
|
-
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
42683
|
+
import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink } from "node:fs/promises";
|
|
42684
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
42662
42685
|
import { isAbsolute, join as join2 } from "node:path";
|
|
42663
42686
|
import { homedir as homedir2 } from "node:os";
|
|
42664
42687
|
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
@@ -42714,16 +42737,46 @@ function parseIdentity(text2) {
|
|
|
42714
42737
|
createdAt: j.createdAt
|
|
42715
42738
|
};
|
|
42716
42739
|
}
|
|
42740
|
+
async function openIdentityDir(dir) {
|
|
42741
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
42742
|
+
try {
|
|
42743
|
+
await chmod(dir, 448);
|
|
42744
|
+
} catch (e) {
|
|
42745
|
+
const code = e.code;
|
|
42746
|
+
if (code !== "EROFS" && code !== "EPERM")
|
|
42747
|
+
throw e;
|
|
42748
|
+
}
|
|
42749
|
+
}
|
|
42750
|
+
var STAGING_BYTES = 6;
|
|
42751
|
+
async function sweepStagingFiles(dir, serverName) {
|
|
42752
|
+
const base = safeIdentityFileBase(serverName).replace(/[.]/g, "\\.");
|
|
42753
|
+
const staging = new RegExp(`^${base}\\.json\\.[0-9a-f]{${STAGING_BYTES * 2}}\\.tmp$`);
|
|
42754
|
+
let names;
|
|
42755
|
+
try {
|
|
42756
|
+
names = await readdir(dir);
|
|
42757
|
+
} catch {
|
|
42758
|
+
return;
|
|
42759
|
+
}
|
|
42760
|
+
await Promise.all(names.filter((name) => staging.test(name)).map((name) => unlink(join2(dir, name)).catch(() => void 0)));
|
|
42761
|
+
}
|
|
42717
42762
|
async function writeIdentityFile(dir, serverName, id) {
|
|
42718
42763
|
const path = identityFilePath(dir, serverName);
|
|
42719
|
-
await
|
|
42720
|
-
await
|
|
42721
|
-
|
|
42764
|
+
await openIdentityDir(dir);
|
|
42765
|
+
await sweepStagingFiles(dir, serverName);
|
|
42766
|
+
const tmp = `${path}.${randomBytes2(STAGING_BYTES).toString("hex")}.tmp`;
|
|
42767
|
+
try {
|
|
42768
|
+
await writeFile(tmp, serializeIdentity(id), { mode: 384, flag: "wx" });
|
|
42769
|
+
await chmod(tmp, 384);
|
|
42770
|
+
await rename(tmp, path);
|
|
42771
|
+
} catch (e) {
|
|
42772
|
+
await rm(tmp, { force: true });
|
|
42773
|
+
throw e;
|
|
42774
|
+
}
|
|
42722
42775
|
return path;
|
|
42723
42776
|
}
|
|
42724
42777
|
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
42725
42778
|
const path = identityFilePath(dir, serverName);
|
|
42726
|
-
await
|
|
42779
|
+
await openIdentityDir(dir);
|
|
42727
42780
|
try {
|
|
42728
42781
|
return parseIdentity(await readFile(path, "utf8"));
|
|
42729
42782
|
} catch (e) {
|
|
@@ -42836,7 +42889,7 @@ async function writeExtensionPin(serverName, pin, dir = defaultTrustDir()) {
|
|
|
42836
42889
|
const tmp = `${path}.tmp`;
|
|
42837
42890
|
await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
|
|
42838
42891
|
await chmod2(tmp, 384);
|
|
42839
|
-
await
|
|
42892
|
+
await rename2(tmp, path);
|
|
42840
42893
|
}
|
|
42841
42894
|
|
|
42842
42895
|
// node_modules/@fetchproxy/server/dist/host.js
|
|
@@ -43191,7 +43244,11 @@ async function startHost(opts) {
|
|
|
43191
43244
|
const session = ownSession;
|
|
43192
43245
|
if (!session)
|
|
43193
43246
|
return;
|
|
43194
|
-
|
|
43247
|
+
const claim2 = session.claimInboundSeq(frame.seq);
|
|
43248
|
+
if (session.saturationWarningDue(claim2)) {
|
|
43249
|
+
console.warn(`[fetchproxy] ${opts.ownServerName}: dropped an inbound frame (seq ${frame.seq}) unread \u2014 too many frames from the extension are still being opened (inbound claims saturated). Not a replay. Further drops are not logged until the in-flight set drains.`);
|
|
43250
|
+
}
|
|
43251
|
+
if (claim2 !== "ok")
|
|
43195
43252
|
return;
|
|
43196
43253
|
let inner;
|
|
43197
43254
|
try {
|
|
@@ -43320,7 +43377,9 @@ async function startHost(opts) {
|
|
|
43320
43377
|
// node_modules/@fetchproxy/server/dist/peer.js
|
|
43321
43378
|
var enc5 = new TextEncoder();
|
|
43322
43379
|
async function startPeer(opts) {
|
|
43323
|
-
const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}
|
|
43380
|
+
const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`, {
|
|
43381
|
+
maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
|
|
43382
|
+
});
|
|
43324
43383
|
await new Promise((resolve2, reject) => {
|
|
43325
43384
|
const onOpen = () => {
|
|
43326
43385
|
cleanup();
|
|
@@ -43558,7 +43617,11 @@ async function startPeer(opts) {
|
|
|
43558
43617
|
const inboundSession = session;
|
|
43559
43618
|
if (!inboundSession)
|
|
43560
43619
|
return;
|
|
43561
|
-
|
|
43620
|
+
const claim2 = inboundSession.claimInboundSeq(frame.seq);
|
|
43621
|
+
if (inboundSession.saturationWarningDue(claim2)) {
|
|
43622
|
+
console.warn(`[fetchproxy] ${opts.serverName}: dropped an inbound frame (seq ${frame.seq}) unread \u2014 too many frames from the extension are still being opened (inbound claims saturated). Not a replay. Further drops are not logged until the in-flight set drains.`);
|
|
43623
|
+
}
|
|
43624
|
+
if (claim2 !== "ok")
|
|
43562
43625
|
return;
|
|
43563
43626
|
let result;
|
|
43564
43627
|
try {
|
|
@@ -51138,7 +51201,7 @@ function registerVisitTools(server, client2, patients2) {
|
|
|
51138
51201
|
}
|
|
51139
51202
|
|
|
51140
51203
|
// src/version.ts
|
|
51141
|
-
var VERSION = "0.4.
|
|
51204
|
+
var VERSION = "0.4.4";
|
|
51142
51205
|
|
|
51143
51206
|
// src/index.ts
|
|
51144
51207
|
var dotenvCandidates = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/myatriumhealth-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "MCP server for MyAtriumHealth (Atrium Health's Epic MyChart patient portal) via the fetchproxy browser bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@chrischall/mcp-utils": "^0.26.1",
|
|
46
|
-
"@fetchproxy/server": "^3.0.
|
|
46
|
+
"@fetchproxy/server": "^3.0.1",
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
48
48
|
"dotenv": "^17.4.2",
|
|
49
49
|
"node-html-parser": "^9.0.3",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/myatriumhealth-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.4.
|
|
9
|
+
"version": "0.4.4",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/myatriumhealth-mcp",
|
|
14
|
-
"version": "0.4.
|
|
14
|
+
"version": "0.4.4",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|