{
  "markdown": "# myatriumhealth-mcp\n\nMCP server for **MyAtriumHealth** — Atrium Health's [Epic MyChart](https://www.mychart.com/)\npatient portal at `my.atriumhealth.org`. Reads test results, medications, allergies,\nimmunizations, health issues, goals and visits.\n\n> This project was developed and is maintained by AI. Use at your own discretion.\n> It reads personal health information, and only ever from the signed-in user's own account.\n\n## Two ways to authenticate\n\n| Mode | When | Credentials held |\n|---|---|---|\n| **Browser bridge** (default) | no credentials configured | **none** |\n| **Bridge-less** | `MAH_USERNAME` + `MAH_PASSWORD` set | your portal password, on disk |\n\nBridge-less logs in server-side — no browser, no extension — which is what makes\nhosting possible. Verification is **human-in-the-loop**: the portal challenges,\n`mah_sign_in` reports the channels your account allows, the portal sends a code to\n**you**, and `mah_verify_code` submits only what you provide. Nothing here bypasses the\nsecond factor.\n\n**When the session expires** you do *not* reconnect the MCP or restart anything. The\nnext tool call returns an actionable error naming the channels and your masked\ndestinations; you pick one, the portal texts or emails **you**, and `mah_verify_code`\nresumes the session in place. A pending challenge is remembered, so further tool calls\nreport it rather than re-submitting your password each time.\n\n**Where credentials are read from.** A real `MAH_USERNAME` / `MAH_PASSWORD` in the\nenvironment always wins. Failing that the server reads the first `.env` it finds, in\nthis order: `MAH_DOTENV`, then `~/.myatriumhealth-mcp/.env`, then `./.env`.\n\nThe middle one exists because MCP clients launch the server from whatever directory\nthey happen to be in, so a `.env` sitting in a checkout is invisible to it — and the\nfailure is quiet: with no credentials the server falls back to the browser bridge,\nbinds a port and waits for a signed-in tab. If you meant to run bridge-less and see the\nbridge start, that is what happened; the startup line now says so.\n\n**How the session persists.** After one verification the cookie jar is stored (0600,\nbound to the account) and reused, so restarts resume the existing session rather than\nsigning in again — no browser and no further codes until the session lapses.\n\n**How long that lasts, measured rather than assumed.** Every one of the ten cookies\nMyChart sets is a *session* cookie — none carries an `expires` or `max-age` — so the\nlifetime is the server's alone and cannot be read from the jar. A jar left **idle for\n219 minutes** no longer authenticated: the next call fell through to a fresh sign-in\nand was challenged for a code immediately.\n\nWhat that does and does not establish: it bounds an **idle** session, and says nothing\nabout an active one. The measurement cannot tell an idle timeout from an absolute one,\nand portals of this kind usually expire on inactivity — so a session in steady use may\noutlive 3.6h comfortably, while one left alone will not. Plan re-verification around\n**gaps in use**, not around wall-clock age.\n\n**Why the jar is written back mid-session.** Every response's `Set-Cookie` is absorbed,\nand the jar is re-persisted whenever a value actually changed (`transport-server.ts`\nafter each request; a no-op write when nothing rotated). That is not bookkeeping: if\nthe portal refreshes its ticket as a session is used, the refreshed one only survives a\nprocess restart by reaching disk. On a scale-to-zero host — where the child exits\nbetween tool calls — that write-back is the whole reason an extended session is still\nthere on the next call, rather than the copy frozen at sign-in.\n\nTwo things none of this changes: a lapse still costs one code rather than a reconnect,\nand detection alone sends nothing — only `mah_sign_in` asks the portal to send\nanything.\n\n> **What does NOT work, measured rather than assumed:** the `RememberDeviceId` this\n> portal returns is *not* a device-tracking id it will accept back. Sending it neither\n> skips verification nor is harmless — it breaks the challenge, leaving the\n> SecondaryValidation page without its `templateContext` so the antiforgery token\n> cannot be read and `SendCode` returns 500. The account reports\n> `RememberMeSettings.EnrollDeviceTracking: False`, which fits. The token is therefore\n> stored but deliberately never sent.\n\nThe bridge mode's real virtue is that it holds **no credentials at all**. Prefer it\nunless you specifically need bridge-less.\n\n## How it works\n\nEvery MyChart cookie is `HttpOnly` and login is MFA-gated, so the session cannot be\ncopied out of the browser and replayed from Node. Requests are therefore relayed\nthrough the user's own signed-in tab via the\n[fetchproxy](https://github.com/chrischall/fetchproxy) bridge and the Transporter\nextension, reusing their authenticated session. **The server never reads or stores the\nsession cookie.**\n\nThe portal's web app talks to a JSON API in two generations — modern\n`POST api/<area>/<Action>` and legacy form-encoded `POST <Area>/<Controller>/<Action>`.\nBoth are documented, with live-captured shapes, in\n[`docs/MYATRIUMHEALTH-API.md`](docs/MYATRIUMHEALTH-API.md).\n\n## Install\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"myatriumhealth\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@chrischall/myatriumhealth-mcp\"]\n    }\n  }\n}\n```\n\nThen sign in to `my.atriumhealth.org` in Chrome and call `mah_healthcheck`. The first\ncall prints a pair code — approve it once in the Transporter popup.\n\n| Env | Default | Purpose |\n|---|---|---|\n| `MAH_USERNAME` | — | MyAtriumHealth username. Set **with** `MAH_PASSWORD` to enable bridge-less mode. |\n| `MAH_PASSWORD` | — | Portal password. Both are required; setting only one falls back to the bridge (with a warning). |\n| `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. |\n| `MAH_WS_PORT` | `37149` | fetchproxy concentrator port (bridge mode only). The whole fleet shares this one port; override only when hosting. |\n\n## Tools\n\nAll read-only except `mah_set_active_patient`, which changes only which patient this\nconnector reads — it writes nothing to any chart.\n\nEvery reading tool returns `{ patient, data }`, so the chart a response belongs to is\nstated rather than inferred.\n\n| Tool | What it returns |\n|---|---|\n| `mah_list_allergies` | Allergies with reactions and severity |\n| `mah_list_health_issues` | The problem list |\n| `mah_list_immunizations` | Immunizations and dates, by organization |\n| `mah_list_medications` | Medications with dosing instructions (sig) and prescriber |\n| `mah_list_test_results` | Labs and imaging: name, abnormal flag, date, provider comments |\n| `mah_list_upcoming_visits` | Upcoming and in-progress appointments |\n| `mah_list_past_visits` | Past visits, grouped by organization |\n| `mah_list_goals` | Patient goals |\n| `mah_get_health_summary` | Health-summary header and action plans |\n| `mah_list_message_folders` | Message Center folders with unread counts |\n| `mah_list_messages` | Message Center conversations for a folder |\n| `mah_list_insurance` | Insurance coverages on file |\n| `mah_list_care_team` | Care team providers, internal and external |\n| `mah_list_billing_accounts` | Billing accounts and balances (parsed from HTML) |\n| `mah_get_menu` | Which portal features this account exposes |\n| `mah_healthcheck` | Connection health — bridge status, or credential/session status in bridge-less mode |\n| `mah_auth_status` | Whether a session can be resumed and whether a device token is stored *(bridge-less only)* |\n| `mah_sign_in` | Sign in server-side; reports verification channels if a code is needed *(bridge-less only)* |\n| `mah_send_verification_code` | Ask the portal to send a code to the account holder *(bridge-less only)* |\n| `mah_verify_code` | Submit the code the user received *(bridge-less only)* |\n| `mah_list_patients` | The patients this login can open — the account holder and any proxy subjects |\n| `mah_get_patient_context` | Which patient the readers are serving, confirmed with the portal |\n| `mah_set_active_patient` | Point every reader at one of those patients; survives restarts |\n\nEvery reading tool takes `view`: `compact` (the default) or `full`. The raw envelopes\nare large — test results ~33 KB, medications ~30 KB — so `compact` is what you want for\nbrowsing, and `full` returns MyAtriumHealth's payload untouched.\n\n`compact` always strips image and avatar URLs, which is subtractive and cannot drop a\nfield nobody knew about. Ten readers additionally reduce each record to its clinically\nmeaningful fields, because their real payloads were captured and a projection derived\nfrom them: `mah_list_allergies`, `mah_list_health_issues`, `mah_list_immunizations`,\n`mah_list_medications`, `mah_list_care_team`, `mah_list_goals`, `mah_list_test_results`,\n`mah_list_past_visits`, `mah_list_insurance` and `mah_list_messages`.\n\nEvery other reader gets the URL strip only. That field list is applied where one was\nactually established and nowhere else — the list above is generated from\n`PROJECTED_ENDPOINTS` and checked against the `project()` call sites by a test, so it\ncannot quietly drift out of step with the code. If the portal's shape drifts, the\nprojection warns to stderr and returns the raw response rather than an empty list.\n\n## Sessions expire, and they do it quietly\n\nMyChart answers an **expired session with HTTP 200 whose body is the login page**, never\na 401, and the JSON endpoints then return `{}`. Tools raise a \"Not signed in\" error with\nthe remedy rather than reporting empty results. Sessions are short-lived; expect to sign\nin again between uses.\n\n## Without the MCP\n\n[`skills/myatriumhealth-fpx`](skills/myatriumhealth-fpx) does the same thing from a\nshell with the `fpx` CLI — no server to run. Its `references/endpoints.md` carries\nlive-verified `jq` recipes for every endpoint here.\n\n## Messages\n\n`mah_list_messages` is the one endpoint that cannot be called with an empty body. It\nneeds a five-key request whose `PageNonce` is the CSP nonce of an `/app/*` page, and\nwhose `externalLoadParams` lists the **non-local** organizations only — passing the\nlocal organization returns HTTP 500. The client assembles this from\n`conversations/GetOrganizations` and its explicit `isLocal` flag.\n\n`api/item-feed/FetchItemFeed` still needs parameters that have not been captured.\n\n## Development\n\n```sh\nnpm install\nnpm test          # typecheck + vitest\nnpm run build\n```\n\n## License\n\nMIT\n",
  "bytes": 10537,
  "sha": "6069950b78b654a188e3dd40dd26f02d09294f22edf95b0778ea1b37adc6c96c",
  "repo_slug": "chrischall/myatriumhealth-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_chrischall_myatriumhealth_mcp_a3f34b4d/readme"
}