@chrischall/myatriumhealth-mcp 1.3.4 → 1.3.5

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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for MyAtriumHealth (Atrium Health's Epic MyChart patient portal) — test results, medications, allergies, immunizations, health issues, visits and messages. Requests relay through the user's own signed-in browser tab via the ContextMint Bridge extension, reusing their existing session.",
10
- "version": "1.3.4"
10
+ "version": "1.3.5"
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 ContextMint Bridge extension.",
18
- "version": "1.3.4",
18
+ "version": "1.3.5",
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": "1.3.4",
4
+ "version": "1.3.5",
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
@@ -125,7 +125,8 @@ first call prints a pair code — approve it once in the ContextMint Bridge popu
125
125
  | `MAH_DEVICE_FILE` | `~/.myatriumhealth-mcp/device.json` | Session state (0600). Holds the **live cookie jar** as well as the device token — treat as a credential. |
126
126
  | `MAH_WS_PORT` | `37149` | fetchproxy concentrator port (bridge mode only). The whole fleet shares this one port; override only when hosting. |
127
127
  | `MAH_READ_ONLY` | `false` | `true` refuses `mah_reply_message` sends (previews still work). The tool stays listed either way. |
128
- | `MCP_CONFIRM_MODE` | `ask-user` | What a reply does on a client that cannot show a confirmation prompt (claude.ai, Claude Desktop). `ask-user`: two steps — the first call sends nothing and returns a preview plus a token, and the model must get your approval in chat before calling again with it. `auto`: the same two steps, but the model may use the token after reviewing the preview itself. `refuse`: replies are refused on such clients. A client that can show prompts (Claude Code) always gets the real prompt. An unrecognised value is treated as `refuse`. |
128
+ | `MCP_CONFIRM_MODE` | `ask-user` | What a reply does on a client that cannot show a confirmation prompt (claude.ai, Claude Desktop). `ask-user`: two steps — the first call sends nothing and returns a preview plus a token, and the model must get your approval in chat before calling again with it. `auto`: the same two steps, but the model may use the token after reviewing the preview itself. `refuse`: replies are refused on such clients. A client that can show prompts (Claude Code) always gets the real prompt, unless `MCP_CONFIRM_ELICITATION=off`. An unrecognised value is treated as `refuse`. |
129
+ | `MCP_CONFIRM_ELICITATION` | `on` | `off` never shows a confirmation prompt, so every client gets the `MCP_CONFIRM_MODE` path. Set it for a client that claims to support prompts but never shows one (the reply hangs — opencode 2.0.x). Any other value stays `on`, with a warning on stderr. |
129
130
  | `MCP_CONFIRM_TTL_SECONDS` | `600` | How long a token stays valid. |
130
131
  | `MCP_CONFIRM_SECRET` | random per process | Signing key; set it only if tokens must survive a server restart. |
131
132
 
@@ -213,7 +214,8 @@ local organization returns HTTP 500. The client assembles this from
213
214
  `body`, and optionally `attachments`. It replies **as whichever patient is active**.
214
215
 
215
216
  - **It asks before it sends.** A client that can show a confirmation prompt (Claude
216
- Code) gets one with the thread, recipients, body and attachments. Elsewhere the first
217
+ Code) gets one with the thread, recipients, body and attachments, unless
218
+ `MCP_CONFIRM_ELICITATION=off`. Elsewhere the first
217
219
  call sends nothing and returns that preview plus a `confirmToken`; only a repeat call
218
220
  with the same arguments and that token sends. `MCP_CONFIRM_MODE` (above) decides
219
221
  whether the model must get your approval in chat before using it (`ask-user`, the
package/dist/bundle.js CHANGED
@@ -20693,8 +20693,8 @@ var require_schemas2 = __commonJS({
20693
20693
  return core._stringFormat(exports.ZodCustomStringFormat, "currency_code", regexes.currencyCode, _params);
20694
20694
  }
20695
20695
  function hash2(alg, params) {
20696
- const enc6 = params?.enc ?? "hex";
20697
- const format = `${alg}_${enc6}`;
20696
+ const enc7 = params?.enc ?? "hex";
20697
+ const format = `${alg}_${enc7}`;
20698
20698
  const regex = core.regexes[format];
20699
20699
  if (!regex)
20700
20700
  throw new Error(`Unrecognized hash format: ${format}`);
@@ -25993,11 +25993,23 @@ var require_websocket = __commonJS({
25993
25993
  this._isServer = false;
25994
25994
  this._redirects = 0;
25995
25995
  if (protocols === void 0) {
25996
- protocols = [];
25996
+ if (!options || options.protocols === void 0) {
25997
+ protocols = [];
25998
+ } else if (Array.isArray(options.protocols)) {
25999
+ protocols = options.protocols;
26000
+ } else {
26001
+ protocols = [options.protocols];
26002
+ }
25997
26003
  } else if (!Array.isArray(protocols)) {
25998
26004
  if (typeof protocols === "object" && protocols !== null) {
25999
26005
  options = protocols;
26000
- protocols = [];
26006
+ if (options.protocols === void 0) {
26007
+ protocols = [];
26008
+ } else if (Array.isArray(options.protocols)) {
26009
+ protocols = options.protocols;
26010
+ } else {
26011
+ protocols = [options.protocols];
26012
+ }
26001
26013
  } else {
26002
26014
  protocols = [protocols];
26003
26015
  }
@@ -26194,7 +26206,6 @@ var require_websocket = __commonJS({
26194
26206
  }
26195
26207
  return;
26196
26208
  }
26197
- this._readyState = _WebSocket.CLOSING;
26198
26209
  this._sender.close(code2, data, !this._isServer, (err) => {
26199
26210
  if (err) return;
26200
26211
  this._closeFrameSent = true;
@@ -26202,6 +26213,7 @@ var require_websocket = __commonJS({
26202
26213
  this._socket.end();
26203
26214
  }
26204
26215
  });
26216
+ this._readyState = _WebSocket.CLOSING;
26205
26217
  setCloseTimer(this);
26206
26218
  }
26207
26219
  /**
@@ -26426,6 +26438,7 @@ var require_websocket = __commonJS({
26426
26438
  socketPath: void 0,
26427
26439
  hostname: void 0,
26428
26440
  protocol: void 0,
26441
+ protocols: void 0,
26429
26442
  timeout: void 0,
26430
26443
  method: "GET",
26431
26444
  host: void 0,
@@ -60033,8 +60046,8 @@ function currencyCode2(_params) {
60033
60046
  return _stringFormat(ZodCustomStringFormat, "currency_code", currencyCode, _params);
60034
60047
  }
60035
60048
  function hash(alg, params) {
60036
- const enc6 = params?.enc ?? "hex";
60037
- const format = `${alg}_${enc6}`;
60049
+ const enc7 = params?.enc ?? "hex";
60050
+ const format = `${alg}_${enc7}`;
60038
60051
  const regex = regexes_exports[format];
60039
60052
  if (!regex)
60040
60053
  throw new Error(`Unrecognized hash format: ${format}`);
@@ -62109,6 +62122,7 @@ function escapeKey(key) {
62109
62122
  var DEFAULT_REQUEST_KEY = "confirmation";
62110
62123
  var DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
62111
62124
  var UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
62125
+ var DISABLED_NOTE = "Nothing was done because confirmation prompts are turned off for this server, and this action is never taken without one";
62112
62126
  var STATE_PREFIX = "mcpu.confirm.v1.";
62113
62127
  function bindingKey(binding) {
62114
62128
  if (binding.ttlSeconds !== void 0 && !(Number.isFinite(binding.ttlSeconds) && binding.ttlSeconds > 0)) {
@@ -62181,6 +62195,15 @@ ${JSON.stringify(preview, null, 2)}`,
62181
62195
  };
62182
62196
  const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
62183
62197
  const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
62198
+ if (options.elicitation === false) {
62199
+ return textResult({
62200
+ confirmed: false,
62201
+ dispatched: false,
62202
+ action: options.action,
62203
+ reason: "confirmation-unsupported",
62204
+ note: options.unsupportedNote ? `${DISABLED_NOTE}. ${options.unsupportedNote}` : `${DISABLED_NOTE}.`
62205
+ });
62206
+ }
62184
62207
  if (response.kind === "missing") {
62185
62208
  if (callerAcceptsFormElicitation(ctx) === false) {
62186
62209
  return textResult({
@@ -62375,7 +62398,7 @@ async function tokenConfirmation(action, fb) {
62375
62398
  }
62376
62399
  async function requireConfirmationWithFallback(ctx, options) {
62377
62400
  const { tokenFallback, ...confirmation } = options;
62378
- if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
62401
+ if (tokenFallback && (options.elicitation === false || callerAcceptsFormElicitation(ctx) === false)) {
62379
62402
  return tokenConfirmation(options.action, tokenFallback);
62380
62403
  }
62381
62404
  return requireConfirmation(ctx, confirmation);
@@ -62534,6 +62557,20 @@ function readConfirmMode(env = process.env) {
62534
62557
  }
62535
62558
  return "refuse";
62536
62559
  }
62560
+ function readConfirmElicitation(env = process.env) {
62561
+ const raw = readEnvVar("MCP_CONFIRM_ELICITATION", { env })?.trim().toLowerCase();
62562
+ if (!raw || raw === "on")
62563
+ return "on";
62564
+ if (raw === "off")
62565
+ return "off";
62566
+ const key = `elicitation:${raw}`;
62567
+ if (!warned.has(key)) {
62568
+ warned.add(key);
62569
+ process.stderr.write(`MCP_CONFIRM_ELICITATION="${raw}" is not one of on, off; treating it as on.
62570
+ `);
62571
+ }
62572
+ return "on";
62573
+ }
62537
62574
  function readConfirmTtl(env) {
62538
62575
  const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
62539
62576
  if (!raw)
@@ -62578,6 +62615,7 @@ function spentTokenStoreFromEnv(env = process.env) {
62578
62615
  }
62579
62616
  var CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
62580
62617
  var REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
62618
+ var ELICITATION_OFF_HINT = "MCP_CONFIRM_ELICITATION=off on the server turns confirmation prompts off.";
62581
62619
  var BAD_TTL_HINT = "MCP_CONFIRM_TTL_SECONDS on the server is not a positive whole number of seconds; fix it to allow two-step confirmation.";
62582
62620
  function boundArgs(args) {
62583
62621
  if (args === null || typeof args !== "object" || Array.isArray(args) || !("confirmToken" in args))
@@ -62591,10 +62629,13 @@ function confirmationFromEnv(options) {
62591
62629
  const spent = callerSpent ?? spentTokenStoreFromEnv(env);
62592
62630
  const mode = readConfirmMode(env);
62593
62631
  const ttlSeconds = readConfirmTtl(env);
62632
+ const elicitationOff = readConfirmElicitation(env) === "off";
62594
62633
  const bound = args === void 0 ? void 0 : boundArgs(args);
62595
- const confirmation = bound === void 0 || rest.binding ? rest : { ...rest, binding: { key: confirmKeyFromEnv(env), args: bound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 } };
62634
+ const bindingAdded = bound === void 0 || rest.binding ? rest : { ...rest, binding: { key: confirmKeyFromEnv(env), args: bound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 } };
62635
+ const confirmation = elicitationOff ? { ...bindingAdded, elicitation: false } : bindingAdded;
62596
62636
  if (mode === "refuse" || ttlSeconds === void 0) {
62597
- const hint = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
62637
+ const reason = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
62638
+ const hint = elicitationOff ? `${ELICITATION_OFF_HINT} ${reason}` : reason;
62598
62639
  return {
62599
62640
  ...confirmation,
62600
62641
  unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${hint}` : hint
@@ -64529,6 +64570,60 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
64529
64570
  "graphql",
64530
64571
  "write_cookies"
64531
64572
  ]);
64573
+ var ACCOUNT_KEY_FRAME = "account-key";
64574
+ var ACCOUNT_ATTEST_FRAME = "account-attest";
64575
+ var BRIDGE_ROLE_FRAME = "bridge-role";
64576
+ var BRIDGE_SERVE_FRAME = "bridge-serve";
64577
+ var ROOM_PING_FRAME = "room-ping";
64578
+ var ROOM_PONG_FRAME = "room-pong";
64579
+ var BRIDGE_ROLE_LABEL_MAX = 64;
64580
+ var ROOM_FRAME_ACCEPTS = Object.freeze({
64581
+ [BRIDGE_ROLE_FRAME]: BRIDGE_ROLE_FRAME,
64582
+ [BRIDGE_SERVE_FRAME]: BRIDGE_SERVE_FRAME,
64583
+ [ROOM_PING_FRAME]: ROOM_PING_FRAME,
64584
+ [ROOM_PONG_FRAME]: ROOM_PING_FRAME
64585
+ });
64586
+
64587
+ // node_modules/@fetchproxy/protocol/dist/mcp-id.js
64588
+ var ID_RE = /^([^:]+):([^:]+):([0-9a-f]{16})$/;
64589
+ function generateMcpId(serverName, version2) {
64590
+ if (serverName.includes(":")) {
64591
+ throw new Error(`serverName cannot contain ':': ${serverName}`);
64592
+ }
64593
+ if (version2.includes(":")) {
64594
+ throw new Error(`version cannot contain ':': ${version2}`);
64595
+ }
64596
+ const rand = randomHex(8);
64597
+ return `${serverName}:${version2}:${rand}`;
64598
+ }
64599
+ function isValidMcpId(id) {
64600
+ return typeof id === "string" && ID_RE.test(id);
64601
+ }
64602
+ function randomHex(bytes) {
64603
+ const arr = new Uint8Array(bytes);
64604
+ globalThis.crypto.getRandomValues(arr);
64605
+ return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
64606
+ }
64607
+
64608
+ // node_modules/@fetchproxy/protocol/dist/account-attest.js
64609
+ var ACCOUNT_ATTEST_CONSENTS = Object.freeze([
64610
+ "silent",
64611
+ "confirm",
64612
+ "confirm-each"
64613
+ ]);
64614
+ var NO_SCOPE_DIGEST = "0".repeat(64);
64615
+ var ACCOUNT_ID_RE = /^[A-Za-z0-9_.\-]{1,128}$/;
64616
+ var HEX64_RE = /^[0-9a-f]{64}$/;
64617
+ var ACCOUNT_KID_RE = /^[0-9a-f]{16}$/;
64618
+ var enc2 = new TextEncoder();
64619
+
64620
+ // node_modules/@fetchproxy/protocol/dist/awaiting-approval.js
64621
+ var AWAITING_APPROVAL_REASON_PREFIX = "awaiting-approval:";
64622
+ function parseAwaitingApprovalReason(reason) {
64623
+ if (!reason.startsWith(AWAITING_APPROVAL_REASON_PREFIX))
64624
+ return null;
64625
+ return reason.slice(AWAITING_APPROVAL_REASON_PREFIX.length).trim();
64626
+ }
64532
64627
 
64533
64628
  // node_modules/@fetchproxy/protocol/dist/capability-availability.js
64534
64629
  var CAPABILITY_UNAVAILABLE_CODE = "capability_unavailable";
@@ -64554,27 +64649,6 @@ function parseUnsupportedCapabilityReason(reason) {
64554
64649
  return rest.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
64555
64650
  }
64556
64651
 
64557
- // node_modules/@fetchproxy/protocol/dist/mcp-id.js
64558
- var ID_RE = /^([^:]+):([^:]+):([0-9a-f]{16})$/;
64559
- function generateMcpId(serverName, version2) {
64560
- if (serverName.includes(":")) {
64561
- throw new Error(`serverName cannot contain ':': ${serverName}`);
64562
- }
64563
- if (version2.includes(":")) {
64564
- throw new Error(`version cannot contain ':': ${version2}`);
64565
- }
64566
- const rand = randomHex(8);
64567
- return `${serverName}:${version2}:${rand}`;
64568
- }
64569
- function isValidMcpId(id) {
64570
- return typeof id === "string" && ID_RE.test(id);
64571
- }
64572
- function randomHex(bytes) {
64573
- const arr = new Uint8Array(bytes);
64574
- globalThis.crypto.getRandomValues(arr);
64575
- return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
64576
- }
64577
-
64578
64652
  // node_modules/@fetchproxy/protocol/dist/json-pointer.js
64579
64653
  function matchesDeclaredKey(requested, declared) {
64580
64654
  for (const decl of declared) {
@@ -65170,6 +65244,16 @@ function validateFrame(raw) {
65170
65244
  return { type: "extension-disconnected" };
65171
65245
  if (t === "peer-gone")
65172
65246
  return validatePeerGone(raw);
65247
+ if (t === ACCOUNT_KEY_FRAME)
65248
+ return validateAccountKey(raw);
65249
+ if (t === ACCOUNT_ATTEST_FRAME)
65250
+ return validateAccountAttest(raw);
65251
+ if (t === BRIDGE_ROLE_FRAME)
65252
+ return validateBridgeRole(raw);
65253
+ if (t === BRIDGE_SERVE_FRAME || t === ROOM_PING_FRAME || t === ROOM_PONG_FRAME) {
65254
+ assertExactFields(raw, TYPE_ONLY_FIELDS, t);
65255
+ return { type: t };
65256
+ }
65173
65257
  throw new ProtocolError3(`unknown frame type: ${String(t)}`);
65174
65258
  }
65175
65259
  function validateHello(raw) {
@@ -65373,6 +65457,165 @@ function validateHelloRejected(raw) {
65373
65457
  }
65374
65458
  return { type: "hello-rejected", mcpId: raw.mcpId, reason: raw.reason };
65375
65459
  }
65460
+ var DISPLAY_FORBIDDEN_RE = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/;
65461
+ function assertExactFields(raw, allowed, label) {
65462
+ for (const k of Object.keys(raw)) {
65463
+ if (!allowed.includes(k)) {
65464
+ throw new ProtocolError3(`${label}: unexpected field ${JSON.stringify(k)}`);
65465
+ }
65466
+ }
65467
+ }
65468
+ function assertMatch(x, re, label, what) {
65469
+ assertString(x, label);
65470
+ if (!re.test(x))
65471
+ throw new ProtocolError3(`${label}: must be ${what}`);
65472
+ }
65473
+ function assertPositiveSafeInt(x, label) {
65474
+ if (typeof x !== "number" || !Number.isSafeInteger(x) || x <= 0) {
65475
+ throw new ProtocolError3(`${label}: expected positive safe integer`);
65476
+ }
65477
+ }
65478
+ function assertDisplayString(x, label, min, max) {
65479
+ assertString(x, label);
65480
+ if (x.length < min || x.length > max) {
65481
+ throw new ProtocolError3(`${label}: must be ${min}-${max} characters`);
65482
+ }
65483
+ if (DISPLAY_FORBIDDEN_RE.test(x)) {
65484
+ throw new ProtocolError3(`${label}: control or bidi-override character`);
65485
+ }
65486
+ }
65487
+ var ACCOUNT_KEY_FIELDS = [
65488
+ "type",
65489
+ "accountId",
65490
+ "slug",
65491
+ "displayName",
65492
+ "confirmedBy",
65493
+ "tokenId",
65494
+ "kid",
65495
+ "publicKey",
65496
+ "generation",
65497
+ "bridgedRegistrations"
65498
+ ];
65499
+ function validateAccountKey(raw) {
65500
+ const L = "account-key";
65501
+ assertMatch(raw.accountId, ACCOUNT_ID_RE, `${L}.accountId`, "an account id");
65502
+ assertMatch(raw.slug, ACCOUNT_ID_RE, `${L}.slug`, "a slug");
65503
+ assertDisplayString(raw.displayName, `${L}.displayName`, 0, 128);
65504
+ assertDisplayString(raw.confirmedBy, `${L}.confirmedBy`, 1, 256);
65505
+ assertMatch(raw.tokenId, ACCOUNT_ID_RE, `${L}.tokenId`, "a token id");
65506
+ assertMatch(raw.kid, ACCOUNT_KID_RE, `${L}.kid`, "16 lowercase hex");
65507
+ assertBase64Bytes(raw.publicKey, `${L}.publicKey`, 32);
65508
+ assertPositiveSafeInt(raw.generation, `${L}.generation`);
65509
+ const n = raw.bridgedRegistrations;
65510
+ if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 0) {
65511
+ throw new ProtocolError3(`${L}.bridgedRegistrations: expected non-negative safe integer`);
65512
+ }
65513
+ assertExactFields(raw, ACCOUNT_KEY_FIELDS, L);
65514
+ return {
65515
+ type: ACCOUNT_KEY_FRAME,
65516
+ accountId: raw.accountId,
65517
+ slug: raw.slug,
65518
+ displayName: raw.displayName,
65519
+ confirmedBy: raw.confirmedBy,
65520
+ tokenId: raw.tokenId,
65521
+ kid: raw.kid,
65522
+ publicKey: raw.publicKey,
65523
+ generation: raw.generation,
65524
+ bridgedRegistrations: n
65525
+ };
65526
+ }
65527
+ var ACCOUNT_ATTEST_FIELDS = [
65528
+ "type",
65529
+ "mcpId",
65530
+ "accountId",
65531
+ "generation",
65532
+ "tokenId",
65533
+ "kid",
65534
+ "registrationId",
65535
+ "slug",
65536
+ "identityHash",
65537
+ "identityEd25519Pub",
65538
+ "scopeDigest",
65539
+ "consent",
65540
+ "notAfter",
65541
+ "sig"
65542
+ ];
65543
+ function validateAccountAttest(raw) {
65544
+ const L = "account-attest";
65545
+ if (!isValidMcpId(raw.mcpId))
65546
+ throw new ProtocolError3(`${L}.mcpId: invalid format`);
65547
+ assertMatch(raw.accountId, ACCOUNT_ID_RE, `${L}.accountId`, "an account id");
65548
+ assertPositiveSafeInt(raw.generation, `${L}.generation`);
65549
+ assertMatch(raw.tokenId, ACCOUNT_ID_RE, `${L}.tokenId`, "a token id");
65550
+ assertMatch(raw.kid, ACCOUNT_KID_RE, `${L}.kid`, "16 lowercase hex");
65551
+ assertMatch(raw.registrationId, ACCOUNT_ID_RE, `${L}.registrationId`, "a registration id");
65552
+ assertMatch(raw.slug, ACCOUNT_ID_RE, `${L}.slug`, "a slug");
65553
+ assertMatch(raw.identityHash, HEX64_RE, `${L}.identityHash`, "64 lowercase hex");
65554
+ assertBase64Bytes(raw.identityEd25519Pub, `${L}.identityEd25519Pub`, 32);
65555
+ assertMatch(raw.scopeDigest, HEX64_RE, `${L}.scopeDigest`, "64 lowercase hex");
65556
+ assertString(raw.consent, `${L}.consent`);
65557
+ const consent = raw.consent;
65558
+ const known = ACCOUNT_ATTEST_CONSENTS.find((c) => c === consent);
65559
+ if (known === void 0) {
65560
+ throw new ProtocolError3(`${L}.consent: must be one of ${ACCOUNT_ATTEST_CONSENTS.join(", ")}`);
65561
+ }
65562
+ assertPositiveSafeInt(raw.notAfter, `${L}.notAfter`);
65563
+ assertBase64Bytes(raw.sig, `${L}.sig`, 64);
65564
+ assertExactFields(raw, ACCOUNT_ATTEST_FIELDS, L);
65565
+ return {
65566
+ type: ACCOUNT_ATTEST_FRAME,
65567
+ mcpId: raw.mcpId,
65568
+ accountId: raw.accountId,
65569
+ generation: raw.generation,
65570
+ tokenId: raw.tokenId,
65571
+ kid: raw.kid,
65572
+ registrationId: raw.registrationId,
65573
+ slug: raw.slug,
65574
+ identityHash: raw.identityHash,
65575
+ identityEd25519Pub: raw.identityEd25519Pub,
65576
+ scopeDigest: raw.scopeDigest,
65577
+ consent: known,
65578
+ notAfter: raw.notAfter,
65579
+ sig: raw.sig
65580
+ };
65581
+ }
65582
+ var TYPE_ONLY_FIELDS = ["type"];
65583
+ var BRIDGE_ROLE_FIELDS = ["type", "role", "canServe", "serving"];
65584
+ var BRIDGE_ROLE_SERVING_FIELDS = ["label", "since"];
65585
+ function validateBridgeRole(raw) {
65586
+ const L = BRIDGE_ROLE_FRAME;
65587
+ const role = raw.role;
65588
+ if (role !== "serving" && role !== "standby") {
65589
+ throw new ProtocolError3(`${L}.role: must be one of serving, standby`);
65590
+ }
65591
+ const canServe = raw.canServe;
65592
+ assertBoolean(canServe, `${L}.canServe`);
65593
+ assertExactFields(raw, BRIDGE_ROLE_FIELDS, L);
65594
+ const serving = raw.serving;
65595
+ if (role === "serving") {
65596
+ if (serving !== void 0) {
65597
+ throw new ProtocolError3(`${L}.serving: must be absent when role is serving`);
65598
+ }
65599
+ return { type: BRIDGE_ROLE_FRAME, role, canServe };
65600
+ }
65601
+ if (serving === void 0) {
65602
+ throw new ProtocolError3(`${L}.serving: required when role is standby`);
65603
+ }
65604
+ assertObject(serving, `${L}.serving`);
65605
+ const label = serving.label;
65606
+ assertDisplayString(label, `${L}.serving.label`, 1, BRIDGE_ROLE_LABEL_MAX);
65607
+ const since = serving.since;
65608
+ if (typeof since !== "number" || !Number.isSafeInteger(since) || since < 0) {
65609
+ throw new ProtocolError3(`${L}.serving.since: expected non-negative safe integer (Unix ms)`);
65610
+ }
65611
+ assertExactFields(serving, BRIDGE_ROLE_SERVING_FIELDS, `${L}.serving`);
65612
+ return {
65613
+ type: BRIDGE_ROLE_FRAME,
65614
+ role,
65615
+ canServe,
65616
+ serving: { label, since }
65617
+ };
65618
+ }
65376
65619
  function validateInnerFrame(raw) {
65377
65620
  assertObject(raw, "inner");
65378
65621
  const t = raw.type;
@@ -65840,10 +66083,10 @@ function validateInnerResponse(raw) {
65840
66083
  }
65841
66084
 
65842
66085
  // node_modules/@fetchproxy/protocol/dist/pair-code.js
65843
- var enc2 = new TextEncoder();
66086
+ var enc3 = new TextEncoder();
65844
66087
  var PAIR_LABEL = "fetchproxy/4/pair";
65845
66088
  async function pairTranscript(mcpIdentityPub, extIdentityPub, mcpNonce, extNonce, mcpSessionPub) {
65846
- const label = enc2.encode(PAIR_LABEL);
66089
+ const label = enc3.encode(PAIR_LABEL);
65847
66090
  const parts = [mcpIdentityPub, extIdentityPub, mcpNonce, extNonce, mcpSessionPub];
65848
66091
  const out = new Uint8Array(label.length + 1 + parts.reduce((n, p) => n + p.length, 0));
65849
66092
  out.set(label, 0);
@@ -65864,7 +66107,7 @@ function digits(hash2) {
65864
66107
  }
65865
66108
 
65866
66109
  // node_modules/@fetchproxy/protocol/dist/seal.js
65867
- var enc3 = new TextEncoder();
66110
+ var enc4 = new TextEncoder();
65868
66111
  var dec = new TextDecoder();
65869
66112
  function randomIv() {
65870
66113
  const iv = new Uint8Array(12);
@@ -65877,14 +66120,14 @@ function base64Length(byteLength) {
65877
66120
  return Math.ceil(byteLength / 3) * 4;
65878
66121
  }
65879
66122
  function encodeInnerFrame(inner) {
65880
- return enc3.encode(JSON.stringify(inner));
66123
+ return enc4.encode(JSON.stringify(inner));
65881
66124
  }
65882
66125
  function plaintextOf(inner) {
65883
66126
  return inner instanceof Uint8Array ? inner : encodeInnerFrame(inner);
65884
66127
  }
65885
66128
  function sealedFrameWireBytes(mcpId, seq, inner) {
65886
66129
  const plaintext = plaintextOf(inner).length;
65887
- const envelope = enc3.encode(JSON.stringify({ type: "frame", mcpId, seq, iv: "", ciphertext: "" })).length;
66130
+ const envelope = enc4.encode(JSON.stringify({ type: "frame", mcpId, seq, iv: "", ciphertext: "" })).length;
65888
66131
  return envelope + IV_B64_BYTES + base64Length(plaintext + AES_GCM_TAG_BYTES);
65889
66132
  }
65890
66133
  var MAX_FRAME_BYTES = 42 * 1024 * 1024;
@@ -66224,14 +66467,23 @@ var FetchproxyHelloRejectedError = class extends Error {
66224
66467
  platform;
66225
66468
  /**
66226
66469
  * #418: the remedy, when the reason is one this package knows how to
66227
- * explain — today only `unsupported-capability`, which blames the browser
66228
- * rather than the MCP. `null` otherwise.
66470
+ * explain — `unsupported-capability`, which blames the browser rather than
66471
+ * the MCP, and (D12) `awaiting-approval`, which says to approve in the
66472
+ * browser. `null` otherwise.
66229
66473
  */
66230
66474
  hint;
66475
+ /**
66476
+ * D12: `true` only for an `awaiting-approval:` refusal — the extension has
66477
+ * queued an approval card and nobody is at the browser yet, so the same call
66478
+ * succeeds once the person approves. Every other refusal is the extension's
66479
+ * final answer, and retrying unchanged is refused the same way.
66480
+ */
66481
+ retryable;
66231
66482
  constructor(info) {
66232
66483
  const unavailable = parseUnsupportedCapabilityReason(info.reason);
66484
+ const awaiting = parseAwaitingApprovalReason(info.reason);
66233
66485
  const platform = info.platform ?? null;
66234
- const hint = unavailable !== null ? `${platform ? `this browser (${platform})` : "this browser"} cannot serve any of the capabilities this MCP needs (${unavailable.join(", ")}) \u2014 ContextMint Bridge checked for the browser APIs and they are missing. Nothing is wrong with the MCP or your pairing, and updating will not change it: use this MCP from a browser that provides them (for example Chrome).` : null;
66486
+ const hint = awaiting !== null ? `The extension is waiting for you to approve this MCP` + (awaiting !== "" ? ` (${awaiting})` : "") + ` \u2014 open ContextMint Bridge in your browser, approve it there, then retry. Nothing is wrong with the MCP or your sign-in.` : unavailable !== null ? `${platform ? `this browser (${platform})` : "this browser"} cannot serve any of the capabilities this MCP needs (${unavailable.join(", ")}) \u2014 ContextMint Bridge checked for the browser APIs and they are missing. Nothing is wrong with the MCP or your pairing, and updating will not change it: use this MCP from a browser that provides them (for example Chrome).` : null;
66235
66487
  super(`fetchproxy: the extension refused the connection for "${info.mcpId}": ${info.reason}. ` + (hint !== null ? hint : `This is the extension's own reason \u2014 it is not a timeout, and retrying unchanged will be refused the same way.`));
66236
66488
  this.name = "FetchproxyHelloRejectedError";
66237
66489
  this.mcpId = info.mcpId;
@@ -66239,6 +66491,7 @@ var FetchproxyHelloRejectedError = class extends Error {
66239
66491
  this.unavailableCapabilities = unavailable ?? [];
66240
66492
  this.platform = platform;
66241
66493
  this.hint = hint;
66494
+ this.retryable = awaiting !== null;
66242
66495
  Object.setPrototypeOf(this, new.target.prototype);
66243
66496
  }
66244
66497
  };
@@ -66791,7 +67044,7 @@ function originVerdict(origin, allowLocal) {
66791
67044
  }
66792
67045
  var HANDSHAKE_TIMEOUT_MS = 15e3;
66793
67046
  var MAX_PAYLOAD_BYTES = MAX_FRAME_BYTES;
66794
- var enc4 = new TextEncoder();
67047
+ var enc5 = new TextEncoder();
66795
67048
  var OWN_DECRYPT_FAILURES_BEFORE_REHANDSHAKE = 3;
66796
67049
  async function startHost(opts) {
66797
67050
  const allowLocalOrigins = envAllowsLocalOrigins();
@@ -66862,10 +67115,20 @@ async function startHost(opts) {
66862
67115
  let resolveOwnSession;
66863
67116
  let rejectOwnSession;
66864
67117
  let ownSessionReady;
67118
+ let ownRetryableRefusal = false;
67119
+ let ownSessionSettled = false;
66865
67120
  function resetSessionPromise() {
67121
+ ownRetryableRefusal = false;
67122
+ ownSessionSettled = false;
66866
67123
  ownSessionReady = new Promise((resolve2, reject) => {
66867
- resolveOwnSession = resolve2;
66868
- rejectOwnSession = reject;
67124
+ resolveOwnSession = (st) => {
67125
+ ownSessionSettled = true;
67126
+ resolve2(st);
67127
+ };
67128
+ rejectOwnSession = (e) => {
67129
+ ownSessionSettled = true;
67130
+ reject(e);
67131
+ };
66869
67132
  });
66870
67133
  ownSessionReady.catch(() => {
66871
67134
  });
@@ -67132,13 +67395,15 @@ async function startHost(opts) {
67132
67395
  const extPub = fromB64(frame.extensionSessionPub);
67133
67396
  const shared = await ecdhX25519(held.priv, extPub);
67134
67397
  const salt = await transcriptHash(held.nonce, extNonce, held.pub, extPub);
67135
- const key = await hkdfSha256(shared, salt, enc4.encode(HKDF_SESSION_INFO), 32);
67398
+ const key = await hkdfSha256(shared, salt, enc5.encode(HKDF_SESSION_INFO), 32);
67136
67399
  if (extensionWs !== ws)
67137
67400
  return;
67138
67401
  ownSession = new SessionState(key);
67139
67402
  ownSessionInfo = sessionInfoFromHello(verifiedHello);
67140
67403
  ownDecryptFailures = 0;
67141
67404
  ownPendingPairCode = null;
67405
+ if (ownRetryableRefusal)
67406
+ resetSessionPromise();
67142
67407
  resolveOwnSession(ownSession);
67143
67408
  } else {
67144
67409
  const slot = peers.get(frame.mcpId);
@@ -67207,11 +67472,17 @@ async function startHost(opts) {
67207
67472
  }
67208
67473
  if (frame.type === "hello-rejected" && identified === "extension") {
67209
67474
  if (frame.mcpId === opts.ownMcpId) {
67210
- rejectOwnSession(new FetchproxyHelloRejectedError({
67475
+ const refusal = new FetchproxyHelloRejectedError({
67211
67476
  mcpId: frame.mcpId,
67212
67477
  reason: frame.reason,
67213
67478
  platform: extensionHello?.platform ?? null
67214
- }));
67479
+ });
67480
+ if (ownRetryableRefusal)
67481
+ resetSessionPromise();
67482
+ const settlesPending = !ownSessionSettled;
67483
+ rejectOwnSession(refusal);
67484
+ if (refusal.retryable && settlesPending)
67485
+ ownRetryableRefusal = true;
67215
67486
  } else {
67216
67487
  const slot = peers.get(frame.mcpId);
67217
67488
  if (slot?.helloFrame.accepts?.includes("hello-rejected")) {
@@ -67327,7 +67598,7 @@ async function startHost(opts) {
67327
67598
 
67328
67599
  // node_modules/@fetchproxy/server/dist/peer.js
67329
67600
  var PEER_DIAL_TIMEOUT_MS = 5e3;
67330
- var enc5 = new TextEncoder();
67601
+ var enc6 = new TextEncoder();
67331
67602
  function notSentError() {
67332
67603
  return new Error("fetchproxy: request not sent \u2014 the connection to the bridge host closed before it went out, so nothing reached the browser. It is safe to retry.");
67333
67604
  }
@@ -67427,7 +67698,9 @@ async function startPeer(opts) {
67427
67698
  let rejectFirstReady;
67428
67699
  let sessionPromise;
67429
67700
  let sessionPromiseSettled = false;
67701
+ let retryableRefusal = false;
67430
67702
  function resetSessionPromise() {
67703
+ retryableRefusal = false;
67431
67704
  sessionPromiseSettled = false;
67432
67705
  sessionPromise = new Promise((resolve2, reject) => {
67433
67706
  resolveFirstReady = (st) => {
@@ -67524,6 +67797,8 @@ async function startPeer(opts) {
67524
67797
  raw = JSON.parse(data.toString());
67525
67798
  const frame = validateFrame(raw);
67526
67799
  if (frame.type === "hello" && frame.role === "extension") {
67800
+ if (retryableRefusal)
67801
+ resetSessionPromise();
67527
67802
  extensionHello = frame;
67528
67803
  const mintedKeypair = await generateSessionKeypair();
67529
67804
  const mintedHello = await buildServerHello({
@@ -67553,6 +67828,8 @@ async function startPeer(opts) {
67553
67828
  session = null;
67554
67829
  if (sessionPromiseSettled)
67555
67830
  resetSessionPromise();
67831
+ } else if (retryableRefusal) {
67832
+ resetSessionPromise();
67556
67833
  }
67557
67834
  extensionDisconnectListeners.forEach((cb) => cb());
67558
67835
  return;
@@ -67574,7 +67851,7 @@ async function startPeer(opts) {
67574
67851
  const extNonce = fromB64(authenticated.sessionNonce);
67575
67852
  const shared = await ecdhX25519(held.priv, extPub);
67576
67853
  const salt = await transcriptHash(held.nonce, extNonce, held.pub, extPub);
67577
- const sessionKey = await hkdfSha256(shared, salt, enc5.encode(HKDF_SESSION_INFO), 32);
67854
+ const sessionKey = await hkdfSha256(shared, salt, enc6.encode(HKDF_SESSION_INFO), 32);
67578
67855
  if (sessionEphemeral !== held) {
67579
67856
  console.warn(`[fetchproxy] ${opts.serverName}: dropping a session derived against an ephemeral that was superseded while it was being derived`);
67580
67857
  return;
@@ -67587,16 +67864,24 @@ async function startPeer(opts) {
67587
67864
  if (isRenegotiation) {
67588
67865
  renegotiateListeners.forEach((cb) => cb());
67589
67866
  } else {
67867
+ if (retryableRefusal)
67868
+ resetSessionPromise();
67590
67869
  resolveFirstReady(session);
67591
67870
  }
67592
67871
  return;
67593
67872
  }
67594
67873
  if (frame.type === "hello-rejected" && frame.mcpId === opts.mcpId) {
67595
- rejectFirstReady(new FetchproxyHelloRejectedError({
67874
+ const refusal = new FetchproxyHelloRejectedError({
67596
67875
  mcpId: frame.mcpId,
67597
67876
  reason: frame.reason,
67598
67877
  platform: extensionHello?.platform ?? null
67599
- }));
67878
+ });
67879
+ if (retryableRefusal)
67880
+ resetSessionPromise();
67881
+ const settlesPending = !sessionPromiseSettled;
67882
+ rejectFirstReady(refusal);
67883
+ if (refusal.retryable && settlesPending)
67884
+ retryableRefusal = true;
67600
67885
  }
67601
67886
  if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
67602
67887
  const hello2 = extensionHello;
@@ -67760,8 +68045,9 @@ function classifyFetchError(error62, code2) {
67760
68045
 
67761
68046
  // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
67762
68047
  function classifyBridgeError(err) {
67763
- if (err instanceof FetchproxyHelloRejectedError)
67764
- return "hello_rejected";
68048
+ if (err instanceof FetchproxyHelloRejectedError) {
68049
+ return err.retryable ? "awaiting_approval" : "hello_rejected";
68050
+ }
67765
68051
  if (err instanceof FetchproxySessionNotReadyError)
67766
68052
  return "session_not_ready";
67767
68053
  if (err instanceof FetchproxyTimeoutError)
@@ -75968,7 +76254,7 @@ function registerVisitTools(server, client2, patients2) {
75968
76254
  }
75969
76255
 
75970
76256
  // src/version.ts
75971
- var VERSION = "1.3.4";
76257
+ var VERSION = "1.3.5";
75972
76258
 
75973
76259
  // src/index.ts
75974
76260
  var dotenvCandidates = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/myatriumhealth-mcp",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
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": "^2.14.0",
46
- "@fetchproxy/server": "^3.4.1",
45
+ "@chrischall/mcp-utils": "^2.15.0",
46
+ "@fetchproxy/server": "^3.6.0",
47
47
  "@modelcontextprotocol/server": "^2.3.0",
48
48
  "dotenv": "^18.0.1",
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": "1.3.4",
9
+ "version": "1.3.5",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/myatriumhealth-mcp",
14
- "version": "1.3.4",
14
+ "version": "1.3.5",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },