@chrischall/myatriumhealth-mcp 0.3.0 → 0.4.0
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/README.md +5 -5
- package/dist/bundle.js +528 -277
- package/package.json +5 -5
- 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.
|
|
10
|
+
"version": "0.4.0"
|
|
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.
|
|
18
|
+
"version": "0.4.0",
|
|
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
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "MyAtriumHealth (Epic MyChart) access for Claude — test results, medications, allergies, immunizations, visits",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/README.md
CHANGED
|
@@ -137,11 +137,11 @@ stated rather than inferred.
|
|
|
137
137
|
| `mah_list_care_team` | Care team providers, internal and external |
|
|
138
138
|
| `mah_list_billing_accounts` | Billing accounts and balances (parsed from HTML) |
|
|
139
139
|
| `mah_get_menu` | Which portal features this account exposes |
|
|
140
|
-
| `mah_healthcheck` | Connection health — bridge status,
|
|
141
|
-
| `mah_auth_status` | Whether a session can be resumed and whether a device token is stored
|
|
142
|
-
| `mah_sign_in` | Sign in server-side; reports verification channels if a code is needed *(
|
|
143
|
-
| `mah_send_verification_code` | Ask the portal to send a code to the account holder *(
|
|
144
|
-
| `mah_verify_code` | Submit the code the user received *(
|
|
140
|
+
| `mah_healthcheck` | Connection health — bridge status when relaying, credential and session status when signing in server-side |
|
|
141
|
+
| `mah_auth_status` | Whether a session can be resumed and whether a device token is stored |
|
|
142
|
+
| `mah_sign_in` | Sign in server-side; reports verification channels if a code is needed *(needs credentials)* |
|
|
143
|
+
| `mah_send_verification_code` | Ask the portal to send a code to the account holder *(needs credentials)* |
|
|
144
|
+
| `mah_verify_code` | Submit the code the user received *(needs credentials)* |
|
|
145
145
|
| `mah_list_patients` | The patients this login can open — the account holder and any proxy subjects |
|
|
146
146
|
| `mah_get_patient_context` | Which patient the readers are serving, confirmed with the portal |
|
|
147
147
|
| `mah_set_active_patient` | Point every reader at one of those patients; survives restarts |
|
package/dist/bundle.js
CHANGED
|
@@ -41365,6 +41365,8 @@ function validateFrame(raw) {
|
|
|
41365
41365
|
return validateEncrypted(raw);
|
|
41366
41366
|
if (t === "pair-pending")
|
|
41367
41367
|
return validatePairPending(raw);
|
|
41368
|
+
if (t === "hello-rejected")
|
|
41369
|
+
return validateHelloRejected(raw);
|
|
41368
41370
|
if (t === "extension-disconnected")
|
|
41369
41371
|
return { type: "extension-disconnected" };
|
|
41370
41372
|
throw new ProtocolError(`unknown frame type: ${String(t)}`);
|
|
@@ -41495,6 +41497,16 @@ function validatePairPending(raw) {
|
|
|
41495
41497
|
}
|
|
41496
41498
|
return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
|
|
41497
41499
|
}
|
|
41500
|
+
function validateHelloRejected(raw) {
|
|
41501
|
+
assertString(raw.mcpId, "hello-rejected.mcpId");
|
|
41502
|
+
if (!isValidMcpId(raw.mcpId))
|
|
41503
|
+
throw new ProtocolError("hello-rejected.mcpId: invalid format");
|
|
41504
|
+
assertString(raw.reason, "hello-rejected.reason");
|
|
41505
|
+
if (raw.reason.length > 200) {
|
|
41506
|
+
throw new ProtocolError("hello-rejected.reason: must be at most 200 characters");
|
|
41507
|
+
}
|
|
41508
|
+
return { type: "hello-rejected", mcpId: raw.mcpId, reason: raw.reason };
|
|
41509
|
+
}
|
|
41498
41510
|
function validateInnerFrame(raw) {
|
|
41499
41511
|
assertObject(raw, "inner");
|
|
41500
41512
|
const t = raw.type;
|
|
@@ -42309,6 +42321,17 @@ var FetchproxySessionNotReadyError = class extends Error {
|
|
|
42309
42321
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
42310
42322
|
}
|
|
42311
42323
|
};
|
|
42324
|
+
var FetchproxyHelloRejectedError = class extends Error {
|
|
42325
|
+
mcpId;
|
|
42326
|
+
reason;
|
|
42327
|
+
constructor(info) {
|
|
42328
|
+
super(`fetchproxy: the extension refused the connection for "${info.mcpId}": ${info.reason}. This is the extension's own reason \u2014 it is not a timeout, and retrying unchanged will be refused the same way.`);
|
|
42329
|
+
this.name = "FetchproxyHelloRejectedError";
|
|
42330
|
+
this.mcpId = info.mcpId;
|
|
42331
|
+
this.reason = info.reason;
|
|
42332
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
42333
|
+
}
|
|
42334
|
+
};
|
|
42312
42335
|
async function awaitSessionReady(ready, opts) {
|
|
42313
42336
|
const ms = opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS;
|
|
42314
42337
|
if (ms <= 0)
|
|
@@ -42334,12 +42357,21 @@ import { join as join3 } from "node:path";
|
|
|
42334
42357
|
|
|
42335
42358
|
// node_modules/@fetchproxy/server/dist/identity.js
|
|
42336
42359
|
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
42337
|
-
import { join as join2 } from "node:path";
|
|
42360
|
+
import { isAbsolute, join as join2 } from "node:path";
|
|
42338
42361
|
import { homedir as homedir2 } from "node:os";
|
|
42339
42362
|
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
42340
42363
|
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
42364
|
+
function envIdentityDir() {
|
|
42365
|
+
const raw = process.env.FETCHPROXY_IDENTITY_DIR;
|
|
42366
|
+
if (raw === void 0)
|
|
42367
|
+
return void 0;
|
|
42368
|
+
const dir = raw.trim();
|
|
42369
|
+
if (dir === "" || !isAbsolute(dir))
|
|
42370
|
+
return void 0;
|
|
42371
|
+
return dir;
|
|
42372
|
+
}
|
|
42341
42373
|
function defaultIdentityDir() {
|
|
42342
|
-
return join2(homedir2(), ".fetchproxy", "identity");
|
|
42374
|
+
return envIdentityDir() ?? join2(homedir2(), ".fetchproxy", "identity");
|
|
42343
42375
|
}
|
|
42344
42376
|
function safeIdentityFileBase(serverName) {
|
|
42345
42377
|
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
@@ -42347,42 +42379,57 @@ function safeIdentityFileBase(serverName) {
|
|
|
42347
42379
|
}
|
|
42348
42380
|
return serverName.replace(/\//g, "_");
|
|
42349
42381
|
}
|
|
42350
|
-
|
|
42351
|
-
|
|
42352
|
-
|
|
42353
|
-
|
|
42354
|
-
try {
|
|
42355
|
-
const raw = await readFile(path, "utf8");
|
|
42356
|
-
const j2 = JSON.parse(raw);
|
|
42357
|
-
return {
|
|
42358
|
-
x25519Priv: fromB64(j2.x25519Priv),
|
|
42359
|
-
x25519Pub: fromB64(j2.x25519Pub),
|
|
42360
|
-
ed25519Priv: fromB64(j2.ed25519Priv),
|
|
42361
|
-
ed25519Pub: fromB64(j2.ed25519Pub),
|
|
42362
|
-
createdAt: j2.createdAt
|
|
42363
|
-
};
|
|
42364
|
-
} catch (e) {
|
|
42365
|
-
if (e.code !== "ENOENT")
|
|
42366
|
-
throw e;
|
|
42367
|
-
}
|
|
42382
|
+
function identityFilePath(dir, serverName) {
|
|
42383
|
+
return join2(dir, `${safeIdentityFileBase(serverName)}.json`);
|
|
42384
|
+
}
|
|
42385
|
+
async function generateIdentity() {
|
|
42368
42386
|
const x = await generateX25519();
|
|
42369
42387
|
const ed = await generateEd25519();
|
|
42370
|
-
|
|
42388
|
+
return {
|
|
42371
42389
|
x25519Priv: x.privateKey,
|
|
42372
42390
|
x25519Pub: x.publicKey,
|
|
42373
42391
|
ed25519Priv: ed.privateKey,
|
|
42374
42392
|
ed25519Pub: ed.publicKey,
|
|
42375
42393
|
createdAt: Date.now()
|
|
42376
42394
|
};
|
|
42377
|
-
|
|
42395
|
+
}
|
|
42396
|
+
function serializeIdentity(id) {
|
|
42397
|
+
return JSON.stringify({
|
|
42378
42398
|
x25519Priv: toB64(id.x25519Priv),
|
|
42379
42399
|
x25519Pub: toB64(id.x25519Pub),
|
|
42380
42400
|
ed25519Priv: toB64(id.ed25519Priv),
|
|
42381
42401
|
ed25519Pub: toB64(id.ed25519Pub),
|
|
42382
42402
|
createdAt: id.createdAt
|
|
42403
|
+
}, null, 2);
|
|
42404
|
+
}
|
|
42405
|
+
function parseIdentity(text2) {
|
|
42406
|
+
const j = JSON.parse(text2);
|
|
42407
|
+
return {
|
|
42408
|
+
x25519Priv: fromB64(j.x25519Priv),
|
|
42409
|
+
x25519Pub: fromB64(j.x25519Pub),
|
|
42410
|
+
ed25519Priv: fromB64(j.ed25519Priv),
|
|
42411
|
+
ed25519Pub: fromB64(j.ed25519Pub),
|
|
42412
|
+
createdAt: j.createdAt
|
|
42383
42413
|
};
|
|
42384
|
-
|
|
42414
|
+
}
|
|
42415
|
+
async function writeIdentityFile(dir, serverName, id) {
|
|
42416
|
+
const path = identityFilePath(dir, serverName);
|
|
42417
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
42418
|
+
await writeFile(path, serializeIdentity(id), { mode: 384 });
|
|
42385
42419
|
await chmod(path, 384);
|
|
42420
|
+
return path;
|
|
42421
|
+
}
|
|
42422
|
+
async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
42423
|
+
const path = identityFilePath(dir, serverName);
|
|
42424
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
42425
|
+
try {
|
|
42426
|
+
return parseIdentity(await readFile(path, "utf8"));
|
|
42427
|
+
} catch (e) {
|
|
42428
|
+
if (e.code !== "ENOENT")
|
|
42429
|
+
throw e;
|
|
42430
|
+
}
|
|
42431
|
+
const id = await generateIdentity();
|
|
42432
|
+
await writeIdentityFile(dir, serverName, id);
|
|
42386
42433
|
return id;
|
|
42387
42434
|
}
|
|
42388
42435
|
|
|
@@ -42486,6 +42533,9 @@ async function startHost(opts) {
|
|
|
42486
42533
|
}
|
|
42487
42534
|
});
|
|
42488
42535
|
const ownHello = await buildServerHello({
|
|
42536
|
+
// 2.6.0: tell the extension it can say WHY it refused a hello, instead of
|
|
42537
|
+
// leaving us to time out and guess.
|
|
42538
|
+
accepts: ["hello-rejected"],
|
|
42489
42539
|
identity: opts.ownIdentity,
|
|
42490
42540
|
mcpId: opts.ownMcpId,
|
|
42491
42541
|
serverName: opts.ownServerName,
|
|
@@ -42697,6 +42747,16 @@ async function startHost(opts) {
|
|
|
42697
42747
|
extensionWs.send(JSON.stringify(frame));
|
|
42698
42748
|
}
|
|
42699
42749
|
}
|
|
42750
|
+
if (frame.type === "hello-rejected" && identified === "extension") {
|
|
42751
|
+
if (frame.mcpId === opts.ownMcpId) {
|
|
42752
|
+
rejectOwnSession(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
|
|
42753
|
+
} else {
|
|
42754
|
+
const slot = peers.get(frame.mcpId);
|
|
42755
|
+
if (slot?.helloFrame.accepts?.includes("hello-rejected")) {
|
|
42756
|
+
slot.ws.send(JSON.stringify(frame));
|
|
42757
|
+
}
|
|
42758
|
+
}
|
|
42759
|
+
}
|
|
42700
42760
|
if (frame.type === "pair-pending" && identified === "extension") {
|
|
42701
42761
|
if (frame.mcpId === opts.ownMcpId) {
|
|
42702
42762
|
ownPendingPairCode = frame.pairCode;
|
|
@@ -42808,7 +42868,8 @@ async function startPeer(opts) {
|
|
|
42808
42868
|
graphqlOps: opts.graphqlOps,
|
|
42809
42869
|
// 2.5.0: let a host ≥2.5.0 tell us when the extension leaves, so
|
|
42810
42870
|
// `extensionConnected()` / `sessionLinked()` can go back to false.
|
|
42811
|
-
|
|
42871
|
+
// 2.6.0: `hello-rejected` for the same reason, from the extension.
|
|
42872
|
+
accepts: ["extension-disconnected", "hello-rejected"]
|
|
42812
42873
|
});
|
|
42813
42874
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
42814
42875
|
ws.send(JSON.stringify(hello));
|
|
@@ -42923,6 +42984,9 @@ async function startPeer(opts) {
|
|
|
42923
42984
|
}
|
|
42924
42985
|
return;
|
|
42925
42986
|
}
|
|
42987
|
+
if (frame.type === "hello-rejected" && frame.mcpId === opts.mcpId) {
|
|
42988
|
+
rejectFirstReady(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
|
|
42989
|
+
}
|
|
42926
42990
|
if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
|
|
42927
42991
|
pendingPairCode = frame.pairCode;
|
|
42928
42992
|
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
@@ -43020,6 +43084,8 @@ function classifyFetchError(error61) {
|
|
|
43020
43084
|
|
|
43021
43085
|
// node_modules/@fetchproxy/server/dist/classify-bridge-error.js
|
|
43022
43086
|
function classifyBridgeError(err) {
|
|
43087
|
+
if (err instanceof FetchproxyHelloRejectedError)
|
|
43088
|
+
return "hello_rejected";
|
|
43023
43089
|
if (err instanceof FetchproxySessionNotReadyError)
|
|
43024
43090
|
return "session_not_ready";
|
|
43025
43091
|
if (err instanceof FetchproxyTimeoutError)
|
|
@@ -43120,11 +43186,20 @@ var FetchproxyNoTabError = class extends FetchproxyHintedError {
|
|
|
43120
43186
|
this.name = "FetchproxyNoTabError";
|
|
43121
43187
|
}
|
|
43122
43188
|
};
|
|
43189
|
+
var FetchproxyTabOpeningError = class extends FetchproxyHintedError {
|
|
43190
|
+
constructor(originalError) {
|
|
43191
|
+
super(originalError, "the extension is opening that tab now \u2014 retry in a few seconds. You do not need to open one yourself, and this is not a version problem.");
|
|
43192
|
+
this.name = "FetchproxyTabOpeningError";
|
|
43193
|
+
}
|
|
43194
|
+
};
|
|
43123
43195
|
var SCOPE_REJECTION = /not in declared/;
|
|
43124
|
-
var NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
|
|
43196
|
+
var NO_TAB_REJECTION = /no tab matching (?!.*(content script loaded|one is still opening))/;
|
|
43197
|
+
var TAB_OPENING_REJECTION = /no tab matching .*one is still opening/;
|
|
43125
43198
|
function protocolErrorFrom(error61) {
|
|
43126
43199
|
if (SCOPE_REJECTION.test(error61))
|
|
43127
43200
|
return new FetchproxyScopeError(error61);
|
|
43201
|
+
if (TAB_OPENING_REJECTION.test(error61))
|
|
43202
|
+
return new FetchproxyTabOpeningError(error61);
|
|
43128
43203
|
if (NO_TAB_REJECTION.test(error61))
|
|
43129
43204
|
return new FetchproxyNoTabError(error61);
|
|
43130
43205
|
return new FetchproxyProtocolError(error61);
|
|
@@ -43872,9 +43947,9 @@ var FetchproxyServer = class {
|
|
|
43872
43947
|
if (opts.subdomain !== void 0)
|
|
43873
43948
|
assertSubdomainLabel(opts.subdomain);
|
|
43874
43949
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
43875
|
-
const
|
|
43950
|
+
const isAbsolute2 = path.startsWith("http://") || path.startsWith("https://");
|
|
43876
43951
|
let host;
|
|
43877
|
-
if (
|
|
43952
|
+
if (isAbsolute2) {
|
|
43878
43953
|
try {
|
|
43879
43954
|
host = new URL(path).host;
|
|
43880
43955
|
} catch {
|
|
@@ -43883,7 +43958,7 @@ var FetchproxyServer = class {
|
|
|
43883
43958
|
} else {
|
|
43884
43959
|
host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
43885
43960
|
}
|
|
43886
|
-
const url2 =
|
|
43961
|
+
const url2 = isAbsolute2 ? path : `https://${host}${path}`;
|
|
43887
43962
|
assertUrlInDomains("request url", url2, this.opts.domains);
|
|
43888
43963
|
let tabUrl = `https://${host}/`;
|
|
43889
43964
|
if (opts.viaTab !== void 0) {
|
|
@@ -44991,6 +45066,8 @@ var CREDENTIAL_ARMS = /* @__PURE__ */ new Set([
|
|
|
44991
45066
|
"ok",
|
|
44992
45067
|
"no_credential",
|
|
44993
45068
|
"credential_rejected",
|
|
45069
|
+
"session_expired",
|
|
45070
|
+
"verification_pending",
|
|
44994
45071
|
"timeout",
|
|
44995
45072
|
"http",
|
|
44996
45073
|
"transport",
|
|
@@ -45007,6 +45084,10 @@ function credentialHint(arm, prefix, hostLabel, source) {
|
|
|
45007
45084
|
return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
|
|
45008
45085
|
case "credential_rejected":
|
|
45009
45086
|
return `${hostLabel} rejected the credential from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
|
|
45087
|
+
case "session_expired":
|
|
45088
|
+
return `The credential from '${source}' is configured, but no session is live \u2014 ${hostLabel} served a sign-in page rather than the data. Sign in again; a cookie-session portal expires these on its own, so this recurs between uses.`;
|
|
45089
|
+
case "verification_pending":
|
|
45090
|
+
return `${hostLabel} is holding the sign-in on a second factor rather than refusing it. Supply the verification code the ACCOUNT HOLDER received \u2014 the credential itself is not the problem, so changing it will not help.`;
|
|
45010
45091
|
case "timeout":
|
|
45011
45092
|
return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
|
|
45012
45093
|
case "http":
|
|
@@ -45017,105 +45098,161 @@ function credentialHint(arm, prefix, hostLabel, source) {
|
|
|
45017
45098
|
return `Unexpected failure \u2014 see error.message.`;
|
|
45018
45099
|
}
|
|
45019
45100
|
}
|
|
45020
|
-
function
|
|
45021
|
-
|
|
45022
|
-
|
|
45023
|
-
|
|
45024
|
-
|
|
45025
|
-
|
|
45026
|
-
|
|
45027
|
-
|
|
45028
|
-
|
|
45029
|
-
|
|
45030
|
-
|
|
45101
|
+
function credentialHealthcheckDescription(hostLabel) {
|
|
45102
|
+
return `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Read-only; never returns the credential itself.`;
|
|
45103
|
+
}
|
|
45104
|
+
async function runCredentialHealthcheck(args) {
|
|
45105
|
+
const { prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
|
|
45106
|
+
const probeUrl = probePath ? `https://${hostLabel}${probePath.startsWith("/") ? "" : "/"}${probePath}` : void 0;
|
|
45107
|
+
let probeStarted = 0;
|
|
45108
|
+
let state;
|
|
45109
|
+
try {
|
|
45110
|
+
state = await resolveCredential();
|
|
45111
|
+
} catch (e) {
|
|
45112
|
+
const classified = classifyThrown?.(e);
|
|
45113
|
+
const result2 = {
|
|
45114
|
+
ok: false,
|
|
45115
|
+
// Still false, and still no source: a classification explains WHY
|
|
45116
|
+
// nothing resolved, it does not invent a credential that did.
|
|
45117
|
+
credential: { source: null, resolved: false },
|
|
45118
|
+
// No `url`: nothing was probed, and naming one implies it was tried.
|
|
45119
|
+
probe: { elapsed_ms: 0 },
|
|
45120
|
+
error: {
|
|
45121
|
+
kind: classified?.kind ?? "no_credential",
|
|
45122
|
+
message: truncateErrorMessage(messageOf(e)),
|
|
45123
|
+
...classified?.detail !== void 0 ? { detail: classified.detail } : {}
|
|
45124
|
+
},
|
|
45125
|
+
// The hint must follow the KIND beside it. Falling back to
|
|
45126
|
+
// `no_credential`'s copy under a classified kind would state a cause
|
|
45127
|
+
// the kind contradicts — the same disagreement this path exists to
|
|
45128
|
+
// remove. So: an inline hint wins; else the classified arm's own
|
|
45129
|
+
// copy (consumer override first); else, for a kind this module has
|
|
45130
|
+
// no copy for, the neutral `unknown` text rather than one that
|
|
45131
|
+
// asserts a cause; else the unclassified `no_credential` default.
|
|
45132
|
+
hint: classified?.hint ?? (isArm(classified?.kind) ? hints?.[classified.kind] ?? credentialHint(classified.kind, prefix, hostLabel, null) : classified !== void 0 ? credentialHint("unknown", prefix, hostLabel, null) : hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null))
|
|
45133
|
+
};
|
|
45134
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
45135
|
+
}
|
|
45136
|
+
const credential = {
|
|
45137
|
+
source: state.source,
|
|
45138
|
+
resolved: state.source !== null,
|
|
45139
|
+
...state.detail !== void 0 ? { detail: state.detail } : {}
|
|
45140
|
+
};
|
|
45141
|
+
if (!credential.resolved) {
|
|
45142
|
+
const result2 = {
|
|
45143
|
+
ok: false,
|
|
45144
|
+
credential,
|
|
45145
|
+
probe: { elapsed_ms: 0 },
|
|
45146
|
+
error: { kind: "no_credential", message: "no credential source resolved" },
|
|
45147
|
+
hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
|
|
45148
|
+
};
|
|
45149
|
+
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
45150
|
+
}
|
|
45151
|
+
let arm = "ok";
|
|
45152
|
+
let error61;
|
|
45153
|
+
let status;
|
|
45154
|
+
let customHint;
|
|
45155
|
+
probeStarted = Date.now();
|
|
45156
|
+
try {
|
|
45157
|
+
await probeFn();
|
|
45158
|
+
} catch (e) {
|
|
45159
|
+
status = statusOf(e);
|
|
45160
|
+
const aborted2 = e instanceof Error && e.name === "AbortError";
|
|
45161
|
+
arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
|
|
45162
|
+
let kind = arm;
|
|
45163
|
+
let detail;
|
|
45164
|
+
const custom2 = classifyThrown?.(e);
|
|
45165
|
+
if (custom2) {
|
|
45166
|
+
kind = custom2.kind;
|
|
45167
|
+
customHint = custom2.hint;
|
|
45168
|
+
detail = custom2.detail;
|
|
45169
|
+
}
|
|
45170
|
+
error61 = {
|
|
45171
|
+
kind,
|
|
45172
|
+
// Redacted AND bounded before it reaches the result: an upstream
|
|
45173
|
+
// failure routinely quotes what it was sent, and a healthcheck is
|
|
45174
|
+
// the tool people paste into a chat when something is broken.
|
|
45175
|
+
message: truncateErrorMessage(messageOf(e)),
|
|
45176
|
+
...detail !== void 0 ? { detail } : {}
|
|
45177
|
+
};
|
|
45178
|
+
}
|
|
45179
|
+
const result = {
|
|
45180
|
+
ok: error61 === void 0,
|
|
45181
|
+
credential,
|
|
45182
|
+
probe: {
|
|
45183
|
+
...probeUrl ? { url: probeUrl } : {},
|
|
45184
|
+
elapsed_ms: Date.now() - probeStarted,
|
|
45185
|
+
...status !== void 0 ? { status } : {}
|
|
45031
45186
|
},
|
|
45032
|
-
|
|
45033
|
-
|
|
45034
|
-
|
|
45035
|
-
|
|
45036
|
-
|
|
45037
|
-
|
|
45038
|
-
|
|
45039
|
-
|
|
45040
|
-
|
|
45041
|
-
|
|
45042
|
-
|
|
45043
|
-
|
|
45044
|
-
|
|
45045
|
-
|
|
45046
|
-
|
|
45047
|
-
|
|
45048
|
-
|
|
45049
|
-
|
|
45050
|
-
|
|
45051
|
-
|
|
45052
|
-
|
|
45053
|
-
|
|
45054
|
-
|
|
45055
|
-
|
|
45056
|
-
|
|
45057
|
-
|
|
45058
|
-
|
|
45059
|
-
|
|
45187
|
+
...error61 ? { error: error61 } : {},
|
|
45188
|
+
hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
|
|
45189
|
+
};
|
|
45190
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
45191
|
+
}
|
|
45192
|
+
var SessionNotLiveError = class extends Error {
|
|
45193
|
+
hostLabel;
|
|
45194
|
+
detail;
|
|
45195
|
+
constructor(hostLabel, detail) {
|
|
45196
|
+
super(`${hostLabel} served a sign-in page rather than the data (${detail}).`);
|
|
45197
|
+
this.hostLabel = hostLabel;
|
|
45198
|
+
this.detail = detail;
|
|
45199
|
+
this.name = "SessionNotLiveError";
|
|
45200
|
+
}
|
|
45201
|
+
};
|
|
45202
|
+
var ProbeHttpError = class extends Error {
|
|
45203
|
+
hostLabel;
|
|
45204
|
+
status;
|
|
45205
|
+
constructor(hostLabel, status) {
|
|
45206
|
+
super(`${hostLabel} answered ${status}.`);
|
|
45207
|
+
this.hostLabel = hostLabel;
|
|
45208
|
+
this.status = status;
|
|
45209
|
+
this.name = "ProbeHttpError";
|
|
45210
|
+
}
|
|
45211
|
+
};
|
|
45212
|
+
function sessionProbe(opts) {
|
|
45213
|
+
const host = opts.hostLabel ?? "the upstream";
|
|
45214
|
+
return async () => {
|
|
45215
|
+
const { status, body } = await opts.request();
|
|
45216
|
+
if (status >= 300 && status < 400) {
|
|
45217
|
+
throw new SessionNotLiveError(host, `redirected with ${status}`);
|
|
45218
|
+
}
|
|
45219
|
+
if (status < 200 || status >= 300)
|
|
45220
|
+
throw new ProbeHttpError(host, status);
|
|
45221
|
+
if (opts.signedOut(body))
|
|
45222
|
+
throw new SessionNotLiveError(host, "sign-in or verification page");
|
|
45223
|
+
return body;
|
|
45224
|
+
};
|
|
45225
|
+
}
|
|
45226
|
+
function sessionClassifier(opts) {
|
|
45227
|
+
const { hostLabel, hints } = opts;
|
|
45228
|
+
const remedies = opts.remedies ?? {};
|
|
45229
|
+
const call = (tool, fallback) => tool !== void 0 ? `Call ${tool}.` : fallback;
|
|
45230
|
+
return (err) => {
|
|
45231
|
+
if (err instanceof ProbeHttpError) {
|
|
45232
|
+
return {
|
|
45233
|
+
kind: "http",
|
|
45234
|
+
hint: hints?.http ?? `${hostLabel} answered ${err.status}. The credential was never judged \u2014 that is an upstream problem, so retry, and if it persists the far side is down.`
|
|
45060
45235
|
};
|
|
45061
|
-
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
45062
45236
|
}
|
|
45063
|
-
|
|
45064
|
-
|
|
45065
|
-
|
|
45066
|
-
|
|
45067
|
-
|
|
45068
|
-
|
|
45069
|
-
const result2 = {
|
|
45070
|
-
ok: false,
|
|
45071
|
-
credential,
|
|
45072
|
-
probe: { elapsed_ms: 0 },
|
|
45073
|
-
error: { kind: "no_credential", message: "no credential source resolved" },
|
|
45074
|
-
hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
|
|
45237
|
+
if (!(err instanceof SessionNotLiveError))
|
|
45238
|
+
return void 0;
|
|
45239
|
+
if (opts.credentialsRejected?.() === true) {
|
|
45240
|
+
return {
|
|
45241
|
+
kind: "credential_rejected",
|
|
45242
|
+
hint: hints?.credential_rejected ?? `${hostLabel} refused this username and password. Correct them, then sign in ` + `again. ${call(remedies.signIn, "")}`.trim() + " Nothing retries for you: repeated failures escalate to a captcha or a lockout."
|
|
45075
45243
|
};
|
|
45076
|
-
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
45077
45244
|
}
|
|
45078
|
-
|
|
45079
|
-
|
|
45080
|
-
|
|
45081
|
-
|
|
45082
|
-
probeStarted = Date.now();
|
|
45083
|
-
try {
|
|
45084
|
-
await probeFn();
|
|
45085
|
-
} catch (e) {
|
|
45086
|
-
status = statusOf(e);
|
|
45087
|
-
const aborted2 = e instanceof Error && e.name === "AbortError";
|
|
45088
|
-
arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
|
|
45089
|
-
let kind = arm;
|
|
45090
|
-
let detail;
|
|
45091
|
-
const custom2 = classifyThrown?.(e);
|
|
45092
|
-
if (custom2) {
|
|
45093
|
-
kind = custom2.kind;
|
|
45094
|
-
customHint = custom2.hint;
|
|
45095
|
-
detail = custom2.detail;
|
|
45096
|
-
}
|
|
45097
|
-
error61 = {
|
|
45098
|
-
kind,
|
|
45099
|
-
// Redacted AND bounded before it reaches the result: an upstream
|
|
45100
|
-
// failure routinely quotes what it was sent, and a healthcheck is
|
|
45101
|
-
// the tool people paste into a chat when something is broken.
|
|
45102
|
-
message: truncateErrorMessage(messageOf(e)),
|
|
45103
|
-
...detail !== void 0 ? { detail } : {}
|
|
45245
|
+
if (opts.verificationPending?.() === true) {
|
|
45246
|
+
return {
|
|
45247
|
+
kind: "verification_pending",
|
|
45248
|
+
hint: hints?.verification_pending ?? `A verification code is outstanding, so ${hostLabel} is holding the sign-in rather than refusing it. ` + (remedies.sendCode !== void 0 && remedies.verifyCode !== void 0 ? `Call ${remedies.sendCode}, then pass the code the ACCOUNT HOLDER receives to ${remedies.verifyCode}.` : "Supply the verification code the ACCOUNT HOLDER received; the credential itself is not the problem, so changing it will not help.")
|
|
45104
45249
|
};
|
|
45105
45250
|
}
|
|
45106
|
-
|
|
45107
|
-
|
|
45108
|
-
|
|
45109
|
-
probe: {
|
|
45110
|
-
...probeUrl ? { url: probeUrl } : {},
|
|
45111
|
-
elapsed_ms: Date.now() - probeStarted,
|
|
45112
|
-
...status !== void 0 ? { status } : {}
|
|
45113
|
-
},
|
|
45114
|
-
...error61 ? { error: error61 } : {},
|
|
45115
|
-
hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
|
|
45251
|
+
return {
|
|
45252
|
+
kind: "session_expired",
|
|
45253
|
+
hint: hints?.session_expired ?? `The credentials are configured but no session is live \u2014 ${hostLabel} sessions are short-lived, so this recurs between uses. ` + call(remedies.signIn, "Sign in again.") + " Expect a verification code, which goes to the account holder."
|
|
45116
45254
|
};
|
|
45117
|
-
|
|
45118
|
-
});
|
|
45255
|
+
};
|
|
45119
45256
|
}
|
|
45120
45257
|
|
|
45121
45258
|
// node_modules/@chrischall/mcp-utils/dist/fetchproxy/index.js
|
|
@@ -45152,9 +45289,9 @@ function createFetchproxyTransport(opts) {
|
|
|
45152
45289
|
async start() {
|
|
45153
45290
|
await server.listen();
|
|
45154
45291
|
if (logListening) {
|
|
45155
|
-
console.error(`[${serverOpts.serverName}:bridge]
|
|
45292
|
+
console.error(`[${serverOpts.serverName}:bridge] ready \u2014 127.0.0.1:${server.bridgeHealth().port} binds on the first request (version=${serverOpts.version})`);
|
|
45156
45293
|
} else if (debug) {
|
|
45157
|
-
console.error(`[${serverOpts.serverName}:bridge]
|
|
45294
|
+
console.error(`[${serverOpts.serverName}:bridge] ready \u2014 binds on the first request (version=${serverOpts.version})`);
|
|
45158
45295
|
}
|
|
45159
45296
|
},
|
|
45160
45297
|
async close() {
|
|
@@ -45246,123 +45383,131 @@ function projectBridgeStatus(health) {
|
|
|
45246
45383
|
} : {}
|
|
45247
45384
|
};
|
|
45248
45385
|
}
|
|
45249
|
-
function
|
|
45250
|
-
|
|
45251
|
-
|
|
45386
|
+
function probeDisplayUrl(hostLabel, probePath) {
|
|
45387
|
+
return `https://${hostLabel}${probePath.startsWith("/") ? "" : "/"}${probePath}`;
|
|
45388
|
+
}
|
|
45389
|
+
function bridgeHealthcheckDescription(hostLabel, probePath) {
|
|
45390
|
+
return `Round-trips a small public ${hostLabel} URL (${probePath}) through the fetchproxy bridge and returns diagnostics: the bridge's role (host/peer/null), port, version, the extension link (linked / pair pending / not attached / never answered), the elapsed round-trip time, and a plain-English hint distinguishing 'bridge never came up' from 'extension not connected' from 'real ${hostLabel}-side problem'. Read-only, no auth required.`;
|
|
45391
|
+
}
|
|
45392
|
+
async function runBridgeHealthcheck(args) {
|
|
45393
|
+
const { prefix, probePath, hostLabel, probeFn, classifyThrown, hints, path } = args;
|
|
45394
|
+
const probeUrl = probeDisplayUrl(hostLabel, probePath);
|
|
45252
45395
|
const resolveTransport = () => typeof args.transport === "function" ? args.transport() : args.transport;
|
|
45396
|
+
let probeBody = "";
|
|
45397
|
+
let thrown;
|
|
45398
|
+
const wrappedProbe = async (p) => {
|
|
45399
|
+
try {
|
|
45400
|
+
probeBody = await probeFn(p);
|
|
45401
|
+
return probeBody;
|
|
45402
|
+
} catch (e) {
|
|
45403
|
+
thrown = e;
|
|
45404
|
+
throw e;
|
|
45405
|
+
}
|
|
45406
|
+
};
|
|
45407
|
+
let ok;
|
|
45408
|
+
let elapsedMs;
|
|
45409
|
+
let bridge2;
|
|
45410
|
+
let rawError;
|
|
45411
|
+
let pathNow;
|
|
45412
|
+
if (path) {
|
|
45413
|
+
const start = Date.now();
|
|
45414
|
+
try {
|
|
45415
|
+
await wrappedProbe(probePath);
|
|
45416
|
+
ok = true;
|
|
45417
|
+
} catch (e) {
|
|
45418
|
+
ok = false;
|
|
45419
|
+
rawError = { kind: classifyBridgeError(e), message: truncateErrorMessage(messageOf(e)) };
|
|
45420
|
+
}
|
|
45421
|
+
elapsedMs = Date.now() - start;
|
|
45422
|
+
pathNow = path();
|
|
45423
|
+
const transport2 = resolveTransport();
|
|
45424
|
+
bridge2 = transport2 ? projectBridgeStatus(transport2.status()) : void 0;
|
|
45425
|
+
} else {
|
|
45426
|
+
const transport2 = resolveTransport();
|
|
45427
|
+
if (!transport2) {
|
|
45428
|
+
throw new Error("bridge healthcheck: transport() returned nothing and no `path` was supplied \u2014 a bridge-only healthcheck needs its bridge.");
|
|
45429
|
+
}
|
|
45430
|
+
const probeResult = await transport2.runProbe(wrappedProbe, probePath);
|
|
45431
|
+
ok = probeResult.ok;
|
|
45432
|
+
elapsedMs = probeResult.elapsed_ms;
|
|
45433
|
+
bridge2 = {
|
|
45434
|
+
...probeResult.bridge,
|
|
45435
|
+
last_extension_message_at: transport2.status().lastExtensionMessageAt
|
|
45436
|
+
};
|
|
45437
|
+
rawError = probeResult.error;
|
|
45438
|
+
}
|
|
45439
|
+
const probe = ok ? { url: probeUrl, elapsed_ms: elapsedMs, status: 200, body_length: probeBody.length } : { url: probeUrl, elapsed_ms: elapsedMs };
|
|
45440
|
+
let error61;
|
|
45441
|
+
let bridgeHint;
|
|
45442
|
+
let customHint;
|
|
45443
|
+
let customDetail;
|
|
45444
|
+
if (rawError) {
|
|
45445
|
+
let kind = rawError.kind === "other" ? "unknown" : rawError.kind;
|
|
45446
|
+
if (thrown instanceof FetchproxySessionNotReadyError) {
|
|
45447
|
+
kind = "session_not_ready";
|
|
45448
|
+
bridgeHint = thrown.hint;
|
|
45449
|
+
} else if (thrown instanceof FetchproxyBridgeDownError) {
|
|
45450
|
+
bridgeHint = thrown.hint;
|
|
45451
|
+
}
|
|
45452
|
+
if (thrown !== void 0 && classifyThrown) {
|
|
45453
|
+
const custom2 = classifyThrown(thrown);
|
|
45454
|
+
if (custom2) {
|
|
45455
|
+
kind = custom2.kind;
|
|
45456
|
+
customHint = custom2.hint;
|
|
45457
|
+
customDetail = custom2.detail;
|
|
45458
|
+
}
|
|
45459
|
+
}
|
|
45460
|
+
error61 = {
|
|
45461
|
+
kind,
|
|
45462
|
+
message: rawError.message,
|
|
45463
|
+
...bridgeHint !== void 0 ? { bridge_hint: bridgeHint } : {},
|
|
45464
|
+
...customDetail !== void 0 ? { detail: customDetail } : {}
|
|
45465
|
+
};
|
|
45466
|
+
}
|
|
45467
|
+
const direct = pathNow ? pathNow.transport === "direct" : bridge2 === void 0;
|
|
45468
|
+
const arm = ok ? "ok" : error61?.kind === "session_not_ready" ? "session_not_ready" : error61?.kind === "bridge_down" ? "bridge_down" : direct ? "direct" : bridge2 === void 0 || bridge2.role === null ? "no_role" : error61?.kind === "timeout" ? "timeout" : error61?.kind === "protocol" || error61?.kind === "http" ? "protocol" : "unknown";
|
|
45469
|
+
const defaultHint = healthcheckHint({
|
|
45470
|
+
ok,
|
|
45471
|
+
role: bridge2?.role ?? null,
|
|
45472
|
+
// The real configured port from bridgeHealth(), never a literal 37149.
|
|
45473
|
+
port: bridge2?.port ?? null,
|
|
45474
|
+
hostLabel,
|
|
45475
|
+
prefix,
|
|
45476
|
+
probePath,
|
|
45477
|
+
errorKind: error61?.kind,
|
|
45478
|
+
bridgeHint: error61?.kind === "bridge_down" ? bridgeHint : void 0,
|
|
45479
|
+
session: {
|
|
45480
|
+
...bridge2?.session_state !== void 0 ? { state: bridge2.session_state } : {},
|
|
45481
|
+
pairCode: bridge2?.pending_pair_code ?? (thrown instanceof FetchproxySessionNotReadyError ? thrown.pairCode : null)
|
|
45482
|
+
},
|
|
45483
|
+
direct
|
|
45484
|
+
});
|
|
45485
|
+
const result = {
|
|
45486
|
+
ok,
|
|
45487
|
+
...bridge2 ? { bridge: bridge2 } : {},
|
|
45488
|
+
...pathNow ? { transport: pathNow } : {},
|
|
45489
|
+
probe,
|
|
45490
|
+
...error61 ? { error: error61 } : {},
|
|
45491
|
+
// Precedence: classifyThrown's hint > per-arm override > default ladder.
|
|
45492
|
+
hint: customHint ?? hints?.[arm] ?? defaultHint
|
|
45493
|
+
};
|
|
45494
|
+
return {
|
|
45495
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
45496
|
+
};
|
|
45497
|
+
}
|
|
45498
|
+
function registerAdaptiveHealthcheckTool(args) {
|
|
45499
|
+
const { server, prefix, hostLabel, usingBridge, bridge: bridge2, credential } = args;
|
|
45253
45500
|
server.registerTool(`${prefix}_healthcheck`, {
|
|
45254
|
-
title: "Verify
|
|
45255
|
-
description: `
|
|
45501
|
+
title: "Verify this server can reach its upstream",
|
|
45502
|
+
description: `Reports which hop is broken when a real tool fails, for whichever path this server is actually using. Relaying through your signed-in browser tab: ${bridgeHealthcheckDescription(hostLabel, bridge2.probePath)} Signing in server-side with configured credentials: ${credentialHealthcheckDescription(hostLabel)}`,
|
|
45256
45503
|
annotations: {
|
|
45257
|
-
title: "Verify
|
|
45504
|
+
title: "Verify this server can reach its upstream",
|
|
45258
45505
|
readOnlyHint: true,
|
|
45259
45506
|
idempotentHint: true,
|
|
45260
45507
|
openWorldHint: true
|
|
45261
45508
|
},
|
|
45262
45509
|
inputSchema: {}
|
|
45263
|
-
}, async () => {
|
|
45264
|
-
let probeBody = "";
|
|
45265
|
-
let thrown;
|
|
45266
|
-
const wrappedProbe = async (p) => {
|
|
45267
|
-
try {
|
|
45268
|
-
probeBody = await probeFn(p);
|
|
45269
|
-
return probeBody;
|
|
45270
|
-
} catch (e) {
|
|
45271
|
-
thrown = e;
|
|
45272
|
-
throw e;
|
|
45273
|
-
}
|
|
45274
|
-
};
|
|
45275
|
-
let ok;
|
|
45276
|
-
let elapsedMs;
|
|
45277
|
-
let bridge2;
|
|
45278
|
-
let rawError;
|
|
45279
|
-
let pathNow;
|
|
45280
|
-
if (path) {
|
|
45281
|
-
const start = Date.now();
|
|
45282
|
-
try {
|
|
45283
|
-
await wrappedProbe(probePath);
|
|
45284
|
-
ok = true;
|
|
45285
|
-
} catch (e) {
|
|
45286
|
-
ok = false;
|
|
45287
|
-
rawError = { kind: classifyBridgeError(e), message: truncateErrorMessage(messageOf(e)) };
|
|
45288
|
-
}
|
|
45289
|
-
elapsedMs = Date.now() - start;
|
|
45290
|
-
pathNow = path();
|
|
45291
|
-
const transport2 = resolveTransport();
|
|
45292
|
-
bridge2 = transport2 ? projectBridgeStatus(transport2.status()) : void 0;
|
|
45293
|
-
} else {
|
|
45294
|
-
const transport2 = resolveTransport();
|
|
45295
|
-
if (!transport2) {
|
|
45296
|
-
throw new Error("registerBridgeHealthcheckTool: transport() returned nothing and no `path` was supplied \u2014 a bridge-only healthcheck needs its bridge.");
|
|
45297
|
-
}
|
|
45298
|
-
const probeResult = await transport2.runProbe(wrappedProbe, probePath);
|
|
45299
|
-
ok = probeResult.ok;
|
|
45300
|
-
elapsedMs = probeResult.elapsed_ms;
|
|
45301
|
-
bridge2 = {
|
|
45302
|
-
...probeResult.bridge,
|
|
45303
|
-
last_extension_message_at: transport2.status().lastExtensionMessageAt
|
|
45304
|
-
};
|
|
45305
|
-
rawError = probeResult.error;
|
|
45306
|
-
}
|
|
45307
|
-
const probe = ok ? { url: probeUrl, elapsed_ms: elapsedMs, status: 200, body_length: probeBody.length } : { url: probeUrl, elapsed_ms: elapsedMs };
|
|
45308
|
-
let error61;
|
|
45309
|
-
let bridgeHint;
|
|
45310
|
-
let customHint;
|
|
45311
|
-
let customDetail;
|
|
45312
|
-
if (rawError) {
|
|
45313
|
-
let kind = rawError.kind === "other" ? "unknown" : rawError.kind;
|
|
45314
|
-
if (thrown instanceof FetchproxySessionNotReadyError) {
|
|
45315
|
-
kind = "session_not_ready";
|
|
45316
|
-
bridgeHint = thrown.hint;
|
|
45317
|
-
} else if (thrown instanceof FetchproxyBridgeDownError) {
|
|
45318
|
-
bridgeHint = thrown.hint;
|
|
45319
|
-
}
|
|
45320
|
-
if (thrown !== void 0 && classifyThrown) {
|
|
45321
|
-
const custom2 = classifyThrown(thrown);
|
|
45322
|
-
if (custom2) {
|
|
45323
|
-
kind = custom2.kind;
|
|
45324
|
-
customHint = custom2.hint;
|
|
45325
|
-
customDetail = custom2.detail;
|
|
45326
|
-
}
|
|
45327
|
-
}
|
|
45328
|
-
error61 = {
|
|
45329
|
-
kind,
|
|
45330
|
-
message: rawError.message,
|
|
45331
|
-
...bridgeHint !== void 0 ? { bridge_hint: bridgeHint } : {},
|
|
45332
|
-
...customDetail !== void 0 ? { detail: customDetail } : {}
|
|
45333
|
-
};
|
|
45334
|
-
}
|
|
45335
|
-
const direct = pathNow ? pathNow.transport === "direct" : bridge2 === void 0;
|
|
45336
|
-
const arm = ok ? "ok" : error61?.kind === "session_not_ready" ? "session_not_ready" : error61?.kind === "bridge_down" ? "bridge_down" : direct ? "direct" : bridge2 === void 0 || bridge2.role === null ? "no_role" : error61?.kind === "timeout" ? "timeout" : error61?.kind === "protocol" || error61?.kind === "http" ? "protocol" : "unknown";
|
|
45337
|
-
const defaultHint = healthcheckHint({
|
|
45338
|
-
ok,
|
|
45339
|
-
role: bridge2?.role ?? null,
|
|
45340
|
-
// The real configured port from bridgeHealth(), never a literal 37149.
|
|
45341
|
-
port: bridge2?.port ?? null,
|
|
45342
|
-
hostLabel,
|
|
45343
|
-
prefix,
|
|
45344
|
-
probePath,
|
|
45345
|
-
errorKind: error61?.kind,
|
|
45346
|
-
bridgeHint: error61?.kind === "bridge_down" ? bridgeHint : void 0,
|
|
45347
|
-
session: {
|
|
45348
|
-
...bridge2?.session_state !== void 0 ? { state: bridge2.session_state } : {},
|
|
45349
|
-
pairCode: bridge2?.pending_pair_code ?? (thrown instanceof FetchproxySessionNotReadyError ? thrown.pairCode : null)
|
|
45350
|
-
},
|
|
45351
|
-
direct
|
|
45352
|
-
});
|
|
45353
|
-
const result = {
|
|
45354
|
-
ok,
|
|
45355
|
-
...bridge2 ? { bridge: bridge2 } : {},
|
|
45356
|
-
...pathNow ? { transport: pathNow } : {},
|
|
45357
|
-
probe,
|
|
45358
|
-
...error61 ? { error: error61 } : {},
|
|
45359
|
-
// Precedence: classifyThrown's hint > per-arm override > default ladder.
|
|
45360
|
-
hint: customHint ?? hints?.[arm] ?? defaultHint
|
|
45361
|
-
};
|
|
45362
|
-
return {
|
|
45363
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
45364
|
-
};
|
|
45365
|
-
});
|
|
45510
|
+
}, async () => usingBridge() ? runBridgeHealthcheck({ server, prefix, hostLabel, ...bridge2 }) : runCredentialHealthcheck({ server, prefix, hostLabel, ...credential }));
|
|
45366
45511
|
}
|
|
45367
45512
|
|
|
45368
45513
|
// src/transport-fetchproxy.ts
|
|
@@ -45608,6 +45753,15 @@ function registerAccountTools(server, client2, patients2) {
|
|
|
45608
45753
|
|
|
45609
45754
|
// src/tools/auth.ts
|
|
45610
45755
|
function registerAuthTools(server, auth2) {
|
|
45756
|
+
const configured = () => {
|
|
45757
|
+
if (auth2 !== void 0) return auth2;
|
|
45758
|
+
throw new McpToolError(
|
|
45759
|
+
"This server is relaying through your signed-in my.atriumhealth.org browser tab, so there is nothing here to sign in to.",
|
|
45760
|
+
{
|
|
45761
|
+
hint: "Sign in to my.atriumhealth.org in the browser instead. To have this server sign in on its own \u2014 no tab, no extension \u2014 set MAH_USERNAME and MAH_PASSWORD and restart it; verification stays with the account holder either way."
|
|
45762
|
+
}
|
|
45763
|
+
);
|
|
45764
|
+
};
|
|
45611
45765
|
server.registerTool(
|
|
45612
45766
|
"mah_auth_status",
|
|
45613
45767
|
{
|
|
@@ -45616,6 +45770,14 @@ function registerAuthTools(server, auth2) {
|
|
|
45616
45770
|
inputSchema: {}
|
|
45617
45771
|
},
|
|
45618
45772
|
async () => {
|
|
45773
|
+
if (auth2 === void 0) {
|
|
45774
|
+
return minifiedResult({
|
|
45775
|
+
sessionResumable: false,
|
|
45776
|
+
verificationPending: false,
|
|
45777
|
+
trustedDeviceStored: false,
|
|
45778
|
+
nextStep: "This server holds no credentials and signs in to nothing \u2014 requests relay through your signed-in my.atriumhealth.org tab. Run mah_healthcheck to check that path."
|
|
45779
|
+
});
|
|
45780
|
+
}
|
|
45619
45781
|
const hasDevice = auth2.deviceId() !== void 0;
|
|
45620
45782
|
const resumable = await auth2.isSignedIn();
|
|
45621
45783
|
return minifiedResult({
|
|
@@ -45635,7 +45797,7 @@ function registerAuthTools(server, auth2) {
|
|
|
45635
45797
|
},
|
|
45636
45798
|
async () => {
|
|
45637
45799
|
try {
|
|
45638
|
-
await
|
|
45800
|
+
await configured().login();
|
|
45639
45801
|
return minifiedResult({ signedIn: true });
|
|
45640
45802
|
} catch (e) {
|
|
45641
45803
|
if (e instanceof MfaRequiredError) {
|
|
@@ -45663,7 +45825,7 @@ function registerAuthTools(server, auth2) {
|
|
|
45663
45825
|
}
|
|
45664
45826
|
},
|
|
45665
45827
|
async ({ channel, resend }) => {
|
|
45666
|
-
await
|
|
45828
|
+
await configured().sendCode(channel, resend);
|
|
45667
45829
|
return minifiedResult({
|
|
45668
45830
|
sent: true,
|
|
45669
45831
|
channel,
|
|
@@ -45684,7 +45846,7 @@ function registerAuthTools(server, auth2) {
|
|
|
45684
45846
|
}
|
|
45685
45847
|
},
|
|
45686
45848
|
async ({ code, rememberDevice }) => {
|
|
45687
|
-
const r = await
|
|
45849
|
+
const r = await configured().verifyCode(code, rememberDevice);
|
|
45688
45850
|
return minifiedResult({
|
|
45689
45851
|
verified: true,
|
|
45690
45852
|
trustedDeviceStored: r.remembered,
|
|
@@ -49519,6 +49681,30 @@ var kElementsClosedByOpening = {
|
|
|
49519
49681
|
H6: {
|
|
49520
49682
|
h6: true,
|
|
49521
49683
|
H6: true
|
|
49684
|
+
},
|
|
49685
|
+
dt: {
|
|
49686
|
+
dt: true,
|
|
49687
|
+
dd: true,
|
|
49688
|
+
DT: true,
|
|
49689
|
+
DD: true
|
|
49690
|
+
},
|
|
49691
|
+
DT: {
|
|
49692
|
+
dt: true,
|
|
49693
|
+
dd: true,
|
|
49694
|
+
DT: true,
|
|
49695
|
+
DD: true
|
|
49696
|
+
},
|
|
49697
|
+
dd: {
|
|
49698
|
+
dt: true,
|
|
49699
|
+
dd: true,
|
|
49700
|
+
DT: true,
|
|
49701
|
+
DD: true
|
|
49702
|
+
},
|
|
49703
|
+
DD: {
|
|
49704
|
+
dt: true,
|
|
49705
|
+
dd: true,
|
|
49706
|
+
DT: true,
|
|
49707
|
+
DD: true
|
|
49522
49708
|
}
|
|
49523
49709
|
};
|
|
49524
49710
|
var kElementsClosedByClosing = {
|
|
@@ -49589,6 +49775,38 @@ var kElementsClosedByClosing = {
|
|
|
49589
49775
|
table: true,
|
|
49590
49776
|
TR: true,
|
|
49591
49777
|
TABLE: true
|
|
49778
|
+
},
|
|
49779
|
+
dt: {
|
|
49780
|
+
dl: true,
|
|
49781
|
+
body: true,
|
|
49782
|
+
html: true,
|
|
49783
|
+
DL: true,
|
|
49784
|
+
BODY: true,
|
|
49785
|
+
HTML: true
|
|
49786
|
+
},
|
|
49787
|
+
DT: {
|
|
49788
|
+
dl: true,
|
|
49789
|
+
body: true,
|
|
49790
|
+
html: true,
|
|
49791
|
+
DL: true,
|
|
49792
|
+
BODY: true,
|
|
49793
|
+
HTML: true
|
|
49794
|
+
},
|
|
49795
|
+
dd: {
|
|
49796
|
+
dl: true,
|
|
49797
|
+
body: true,
|
|
49798
|
+
html: true,
|
|
49799
|
+
DL: true,
|
|
49800
|
+
BODY: true,
|
|
49801
|
+
HTML: true
|
|
49802
|
+
},
|
|
49803
|
+
DD: {
|
|
49804
|
+
dl: true,
|
|
49805
|
+
body: true,
|
|
49806
|
+
html: true,
|
|
49807
|
+
DL: true,
|
|
49808
|
+
BODY: true,
|
|
49809
|
+
HTML: true
|
|
49592
49810
|
}
|
|
49593
49811
|
};
|
|
49594
49812
|
var kElementsClosedByClosingExcept = { p: {
|
|
@@ -49869,46 +50087,79 @@ function registerBillingTools(server, client2, patients2) {
|
|
|
49869
50087
|
}
|
|
49870
50088
|
|
|
49871
50089
|
// src/tools/healthcheck.ts
|
|
49872
|
-
|
|
49873
|
-
|
|
49874
|
-
|
|
49875
|
-
|
|
49876
|
-
// The app root renders for a signed-in user and redirects to the login
|
|
49877
|
-
// page otherwise, so this probe distinguishes "bridge down" from
|
|
49878
|
-
// "signed out" — the two failures users actually hit.
|
|
49879
|
-
probePath: "Home",
|
|
49880
|
-
hostLabel: "my.atriumhealth.org",
|
|
49881
|
-
transport: transport2,
|
|
49882
|
-
probeFn: (path) => client2.page(path)
|
|
49883
|
-
});
|
|
49884
|
-
}
|
|
49885
|
-
function registerBridgelessHealthcheckTools(server, auth2) {
|
|
49886
|
-
registerCredentialHealthcheckTool({
|
|
50090
|
+
var HOST = "my.atriumhealth.org";
|
|
50091
|
+
function registerMahHealthcheckTool(server, client2, arms) {
|
|
50092
|
+
const { auth: auth2, bridge: bridge2 } = arms;
|
|
50093
|
+
registerAdaptiveHealthcheckTool({
|
|
49887
50094
|
server,
|
|
49888
50095
|
prefix: "mah",
|
|
49889
|
-
hostLabel:
|
|
49890
|
-
|
|
49891
|
-
|
|
49892
|
-
|
|
49893
|
-
|
|
49894
|
-
|
|
49895
|
-
|
|
49896
|
-
|
|
49897
|
-
|
|
49898
|
-
|
|
49899
|
-
const detail = {
|
|
49900
|
-
sessionResumable: resumable,
|
|
49901
|
-
trustedDeviceStored: auth2.deviceId() !== void 0,
|
|
49902
|
-
verificationPending: auth2.mfaPending
|
|
49903
|
-
};
|
|
49904
|
-
return { source: "env", detail };
|
|
50096
|
+
hostLabel: HOST,
|
|
50097
|
+
usingBridge: () => bridge2 !== void 0,
|
|
50098
|
+
bridge: {
|
|
50099
|
+
// The app root renders for a signed-in user and redirects to the login
|
|
50100
|
+
// page otherwise, so this probe distinguishes "bridge down" from
|
|
50101
|
+
// "signed out" — the two failures users actually hit.
|
|
50102
|
+
probePath: "Home",
|
|
50103
|
+
// Only consulted on the bridge arm, which only runs when `bridge` is set.
|
|
50104
|
+
transport: () => bridge2,
|
|
50105
|
+
probeFn: (path) => client2.page(path)
|
|
49905
50106
|
},
|
|
49906
|
-
|
|
49907
|
-
|
|
49908
|
-
|
|
49909
|
-
|
|
49910
|
-
|
|
49911
|
-
|
|
50107
|
+
credential: {
|
|
50108
|
+
// Leading slash + app root: this string is only for display, and the
|
|
50109
|
+
// factory concatenates it onto hostLabel.
|
|
50110
|
+
probePath: "/myatriumhealth/Home",
|
|
50111
|
+
// Report the SOURCE and non-secret facts only — never the password, the
|
|
50112
|
+
// device token, or any cookie. This is the output people paste into a chat
|
|
50113
|
+
// when something is broken.
|
|
50114
|
+
resolveCredential: async () => {
|
|
50115
|
+
if (auth2 === void 0) return { source: null };
|
|
50116
|
+
const resumable = await auth2.isSignedIn();
|
|
50117
|
+
const detail = {
|
|
50118
|
+
sessionResumable: resumable,
|
|
50119
|
+
trustedDeviceStored: auth2.deviceId() !== void 0,
|
|
50120
|
+
verificationPending: auth2.mfaPending
|
|
50121
|
+
};
|
|
50122
|
+
return { source: "env", detail };
|
|
50123
|
+
},
|
|
50124
|
+
// The status/redirect/auth-wall ladder and the three signed-out arms now
|
|
50125
|
+
// live in mcp-utils. What stays here is the part a library cannot write.
|
|
50126
|
+
probeFn: sessionProbe({
|
|
50127
|
+
// Deliberately `auth.request`, NOT the client or transport: those run
|
|
50128
|
+
// ensureSession(), so a healthcheck on a signed-out session would
|
|
50129
|
+
// silently submit credentials. A diagnostic must observe state, never
|
|
50130
|
+
// change it — and on an account whose login controller can switch on a
|
|
50131
|
+
// captcha, one that logs in is actively harmful.
|
|
50132
|
+
request: async () => {
|
|
50133
|
+
const { res, body } = await auth2.request("Home");
|
|
50134
|
+
return { status: res.status, body };
|
|
50135
|
+
},
|
|
50136
|
+
// THE site-specific closure, and the readers' own test rather than a
|
|
50137
|
+
// second copy that could drift: it is title-scoped because every
|
|
50138
|
+
// signed-in page links to two-factor setup, so a body-wide match
|
|
50139
|
+
// reports "signed out" for every request.
|
|
50140
|
+
signedOut: isAuthWall,
|
|
50141
|
+
hostLabel: HOST
|
|
50142
|
+
}),
|
|
50143
|
+
classifyThrown: sessionClassifier({
|
|
50144
|
+
hostLabel: HOST,
|
|
50145
|
+
// Named, never derived. The library used to build these from the tool
|
|
50146
|
+
// prefix, which happened to be right here and wrong everywhere else —
|
|
50147
|
+
// simplepractice signs in with simplepractice_request_sign_in_link,
|
|
50148
|
+
// kiaaccess with kia_start_login.
|
|
50149
|
+
remedies: {
|
|
50150
|
+
signIn: "mah_sign_in",
|
|
50151
|
+
sendCode: "mah_send_verification_code",
|
|
50152
|
+
verifyCode: "mah_verify_code"
|
|
50153
|
+
},
|
|
50154
|
+
verificationPending: () => auth2?.mfaPending === true,
|
|
50155
|
+
credentialsRejected: () => auth2?.credentialsRejected === true,
|
|
50156
|
+
hints: {
|
|
50157
|
+
// The one default worth overriding: naming the two variables beats
|
|
50158
|
+
// "correct them", and this portal's escalation is worth stating.
|
|
50159
|
+
credential_rejected: `${HOST} refused this username and password. Check MAH_USERNAME and MAH_PASSWORD, then call mah_sign_in \u2014 nothing retries for you, because the portal counts failed sign-ins and escalates to a captcha.`
|
|
50160
|
+
}
|
|
50161
|
+
})
|
|
50162
|
+
}
|
|
49912
50163
|
});
|
|
49913
50164
|
}
|
|
49914
50165
|
|
|
@@ -50211,7 +50462,7 @@ function registerVisitTools(server, client2, patients2) {
|
|
|
50211
50462
|
}
|
|
50212
50463
|
|
|
50213
50464
|
// src/version.ts
|
|
50214
|
-
var VERSION = "0.
|
|
50465
|
+
var VERSION = "0.4.0";
|
|
50215
50466
|
|
|
50216
50467
|
// src/index.ts
|
|
50217
50468
|
var dotenvCandidates = [
|
|
@@ -50235,7 +50486,7 @@ if (!bridgeless && (username !== void 0 || password !== void 0)) {
|
|
|
50235
50486
|
);
|
|
50236
50487
|
} else if (!bridgeless) {
|
|
50237
50488
|
console.error(
|
|
50238
|
-
"[myatriumhealth-mcp] No MAH_USERNAME / MAH_PASSWORD found, so
|
|
50489
|
+
"[myatriumhealth-mcp] No MAH_USERNAME / MAH_PASSWORD found, so requests will relay through your signed-in my.atriumhealth.org tab. Set both (env, or a .env beside the server / MAH_DOTENV) for bridge-less sign-in, which needs no tab."
|
|
50239
50490
|
);
|
|
50240
50491
|
}
|
|
50241
50492
|
var port = readPortEnv("MAH_WS_PORT", DEFAULT_PORT);
|
|
@@ -50281,10 +50532,10 @@ await runMcp({
|
|
|
50281
50532
|
(server) => registerAccountTools(server, client, patients),
|
|
50282
50533
|
(server) => registerBillingTools(server, client, patients),
|
|
50283
50534
|
(server) => registerPatientTools(server, client, patients),
|
|
50284
|
-
|
|
50285
|
-
|
|
50286
|
-
|
|
50287
|
-
|
|
50288
|
-
|
|
50535
|
+
// Unconditional, in both modes. What varies is what they DO, not whether
|
|
50536
|
+
// they exist — see registerAuthTools for why a tool surface that follows
|
|
50537
|
+
// the environment is a bug rather than a tidiness.
|
|
50538
|
+
(server) => registerAuthTools(server, auth),
|
|
50539
|
+
(server) => registerMahHealthcheckTool(server, client, { auth, bridge })
|
|
50289
50540
|
]
|
|
50290
50541
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/myatriumhealth-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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": {
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@chrischall/mcp-utils": "^0.
|
|
46
|
-
"@fetchproxy/server": "^2.
|
|
45
|
+
"@chrischall/mcp-utils": "^0.26.0",
|
|
46
|
+
"@fetchproxy/server": "^2.9.0",
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
48
48
|
"dotenv": "^17.4.2",
|
|
49
49
|
"node-html-parser": "^9.0.3",
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^26.0.0",
|
|
54
|
-
"@vitest/coverage-v8": "^
|
|
54
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
55
55
|
"esbuild": "^0.28.1",
|
|
56
56
|
"tsx": "^4.19.0",
|
|
57
57
|
"typescript": "^7.0.2",
|
|
58
|
-
"vitest": "^
|
|
58
|
+
"vitest": "^5.0.0"
|
|
59
59
|
}
|
|
60
60
|
}
|
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.
|
|
9
|
+
"version": "0.4.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/myatriumhealth-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.4.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|