{
  "markdown": "# Last EHR\n\n[![CI](https://github.com/cbetz/last-ehr/actions/workflows/ci.yml/badge.svg)](https://github.com/cbetz/last-ehr/actions/workflows/ci.yml)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)\n[![npm: @lastehr/mcp](https://img.shields.io/npm/v/%40lastehr%2Fmcp?label=%40lastehr%2Fmcp)](https://www.npmjs.com/package/@lastehr/mcp)\n[![npm: @lastehr/agent-write-conformance](https://img.shields.io/npm/v/%40lastehr%2Fagent-write-conformance?label=%40lastehr%2Fagent-write-conformance)](https://www.npmjs.com/package/@lastehr/agent-write-conformance)\n[![Official MCP Registry](https://img.shields.io/badge/Official%20MCP%20Registry-active-2563eb)](https://registry.modelcontextprotocol.io/?q=io.github.cbetz%2Flast-ehr)\n\n**Read the chart broadly. Write only what a human approved.** Last EHR is the\nagent layer for a headless FHIR EHR. It does three things, and the write\nprotocol is only one of them.\n\n**It reads the chart broadly.** 25 of [US Core 9.0.0](https://hl7.org/fhir/us/core/)'s\n27 readable resource types, across 23 patient-scoped sections, following\nreferences so authors, encounters, and provenance stop being dead pointers.\nCounted in the open, with the ceiling published rather than implied, in\n[FHIR coverage](./docs/fhir-coverage.md).\n\n**It is built so the agent cannot fake an absence.** \"She has never had a flu\nshot\" is the answer a chart agent must never invent, so a read reports when\nits window was capped, when a reference lookup was refused, and when a code\nfilter matched nothing in a section that does hold records. Filters that a\nsection cannot apply are refused with the legal values instead of silently\ndropped. These are safety-boundary tests, not best-effort behavior.\n\n**Every write is a proposal.** Show the exact resource, require an explicit\nhuman decision, commit exactly what was reviewed, and record what created it.\nThat half is a small protocol,\n**[Approval-Gated Agent Writes on FHIR](./docs/agent-write-protocol.md)**\n(v0.1 draft): Proposal → Decision → Commit → Audit. Two implementations run\nhere, a web approval card and MCP elicitation-gated write tools, plus a\n[conformance suite](./docs/conformance.md) that checks implementations with\nits own FHIR reads. Independent implementations and criticism are invited.\n\nFive FHIR backends sit behind one interface. This repository is the reference\nimplementation: bring your own FHIR backend and model key for a real agent\n(Medplum for the authenticated path, or any verified adapter for synthetic\nevaluation), or start with the zero-key local synthetic walkthrough.\n\n> **Last EHR is a _layer_, not an EHR.** It runs *on top of* a headless FHIR backend (Medplum for authenticated use; HAPI FHIR, Firely Server, Aidbox, or Oystehr for synthetic evaluation) and talks to it over the FHIR API. It is not the system of record, stores no PHI of its own, and never bundles or forks the backend.\n\n**Status: early / alpha.** APIs, structure, and scope will change. Use synthetic data only. · License: [Apache-2.0](./LICENSE)\n\n**[Try the live demo](https://www.lastehr.com/demo)**: no sign-up, synthetic data, and every write goes through the approval gate.\n\n![Last EHR demo: the agent finds a patient, then records an observation to the chart after the user approves the write](.github/demo.gif)\n\n## Start here\n\n| Goal | Start here |\n| --- | --- |\n| See the approval loop now | [Try the live synthetic-data demo](https://www.lastehr.com/demo): no sign-up. |\n| Read the protocol | [Approval-Gated Agent Writes on FHIR, v0.1 draft](./docs/agent-write-protocol.md) |\n| See exactly what the agent can reach in FHIR | [FHIR coverage](./docs/fhir-coverage.md): counted against US Core, ceiling included |\n| Test an MCP (stdio) implementation of the protocol | `npx @lastehr/agent-write-conformance` — see the [conformance guide](./docs/conformance.md) |\n| Give an MCP client bounded chart tools — read-only by default, opt-in approved writes (Medplum, or the local HAPI stack) | `npx -y @lastehr/mcp init --client claude-code` |\n| Try fixture MCP locally without FHIR credentials or a provider API key | `npm run mcp:demo -- --client claude-code` |\n| Prove the synthetic web-agent workflow locally | `npm run eval` |\n| Inspect the complete flow locally with no account or model key | `npm run demo:local` |\n\n### 30-second synthetic-data walkthrough\n\n1. Open the [live demo](https://www.lastehr.com/demo).\n2. Ask: `Find patients named Smith`.\n3. Ask: `Record a heart rate of 72 bpm for Maria Garcia`.\n4. Inspect the proposed write, then approve it. The data is synthetic and the\n   approval card shows exactly what will be saved.\n\nFor MCP, `@lastehr/mcp` is deliberately a separate surface that is\n**read-only by default** (`search_patients`, `show_patient_info`,\n`read_chart_section`, and `read_document` — the same bounded chart reads the\nweb agent gets), with one opt-in: elicitation-gated write proposals a human\napproves per action.\nConfigure a least-privilege Medplum token before connecting it to a real\nproject; full setup is in the\n[MCP guide](./docs/mcp.md) and the [Official MCP Registry listing](https://registry.modelcontextprotocol.io/?q=io.github.cbetz%2Flast-ehr).\n\nWant to inspect the read surface before configuring Medplum? From a\ncheckout, `npm run mcp:demo` starts the included local HAPI stack, resets the\nfour synthetic fixture charts, and prints a Claude Code/Cursor configuration.\nThat checkout-only **MCP Local Lab** needs no FHIR/Medplum credentials or\nmodel-provider API key of its own and is restricted to fixture patients. Your\nMCP client still uses its normal account and may send those synthetic results\nto its model provider. It is not the published package, generic HAPI support,\nor a path for PHI.\n\nFor a deterministic safety-mechanics check, `npm run eval` creates a separate\nsynthetic target, proves the proposal/approval and denial paths, verifies chart\nassociation, cleans up, and writes a scrubbed report. It is not a clinical,\nauthorization, or compliance certification; see the [evaluation guide](./docs/evals.md).\n\n## What it does\n\n- A chat agent (Vercel AI SDK) with FHIR tools. It **reads** the chart (search patients, view a patient chart, read one filtered chart section) and **writes** to it (add a note, record an observation, create a follow-up task), streamed and rendered as structured cards.\n- **Writes are confirmation-gated**: the agent proposes a write, you approve it, and only then is it saved. Nothing touches the chart without your click. Approved writes carry the standard AIAST security label, with optional Provenance recording author-agent and human-verifier roles (role labels, not user identity).\n- **The gate is a specified protocol, not just app behavior**: [Approval-Gated Agent Writes on FHIR](./docs/agent-write-protocol.md) (v0.1 draft) names the rules — only an explicit approval commits, every ambiguous outcome fails closed, policy can only narrow the gate — and the [conformance suite](./docs/conformance.md) tests an implementing MCP stdio server's gate mechanics with independent FHIR reads. CI runs it against this repository's own server on every pull request and merge.\n- Authentication, multi-tenancy, and access control are delegated to your **Medplum** project (`Project` = tenant, `ProjectMembership` = user, `AccessPolicy` = RBAC). Last EHR doesn't reimplement any of that, and writes are bounded by your AccessPolicy.\n\n## Support status\n\nLast EHR is **Medplum-supported** for authenticated deployments and includes a\n**local, no-auth HAPI FHIR mode** for synthetic-data evaluation. **Firely\nServer** (`FHIR_BACKEND=firely`), **Aidbox** (`FHIR_BACKEND=aidbox`), and\n**Oystehr** (`FHIR_BACKEND=oystehr`) are verified synthetic-evaluation\nadapters: each passed both contract harnesses and the\n[FHIR Agent Safety Eval](./docs/evals.md) against a disposable synthetic\ntarget. Other FHIR R4 backends need an adapter before they are\nsupported. See the full [support matrix](./docs/support.md) for the web,\nSMART, MCP, auth, and evaluation boundaries before choosing a path.\n\nSelf-hosted demos can offer a **backend picker** (swap the EHR under the\nlive agent, per session) and an **\"under the hood\" panel** streaming the\nagent's FHIR operations — both off by default; see the support matrix for\nthe eligibility rules and `npm run check:backends` for preflight.\n\n## What it isn't\n\n- Not a charting EHR, not a system of record, and not a Medplum replacement. It's a thin agent layer: a small, growing set of tools over your FHIR backend.\n- Not a guarantee. The approval gate is a human-in-the-loop boundary, not a safety proof: it stops unilateral writes, but it relies on you reading what you approve. Models can propose wrong or fabricated clinical facts, and approval fatigue is real. Last EHR provides the gate; you provide the review.\n\n## How it works\n\nNext.js 15 (App Router) + React 19. The agent lives in `app/api/chat/route.ts` (`streamText` + FHIR tools); the FHIR calls go through a small backend interface ([`lib/fhir/backend.ts`](./lib/fhir/backend.ts)) with five built-in adapters: Medplum (hosted or self-hosted, token-authenticated), plus local HAPI FHIR, Firely Server, Aidbox, and Oystehr for synthetic evaluation ([docs/support.md](./docs/support.md)). The interface is four methods plus contract notes, so an adapter for another headless EHR is a small, well-scoped contribution. See [docs/adapters.md](./docs/adapters.md) and the [roadmap](./ROADMAP.md).\n\n```mermaid\nflowchart LR\n    B[\"Browser chat\"] --> A[\"/api/chat<br/>streamText + FHIR tools\"]\n    A -- \"reads<br/>search_patients, show_patient_info, read_chart_section\" --> M[(\"Your FHIR backend<br/>(Medplum, HAPI, Firely, Aidbox, Oystehr)\")]\n    A -- \"writes<br/>add_note, record_observation,<br/>record_superseding_observation, create_task\" --> C{\"Approval card\"}\n    C -- \"Approve & save\" --> M\n    C -- \"Cancel\" --> N[\"Nothing saved\"]\n    M -. \"Every call runs as the signed-in user,<br/>bounded by your AccessPolicy.<br/>The layer stores no PHI.\" .-> A\n```\n\nOn the public demo, writes are also tagged with your session, so you only ever see the seed data plus your own edits.\n\n## Quickstart\n\n**Fastest**: try the hosted demo at [lastehr.com/demo](https://www.lastehr.com/demo). No account, no keys, synthetic data.\n\n**One-click deploy** with your own keys:\n\n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcbetz%2Flast-ehr&env=OPENAI_API_KEY,MEDPLUM_CLIENT_ID,MEDPLUM_CLIENT_SECRET,NEXT_PUBLIC_QUICKSTART&envDescription=A%20model%20key%20plus%20Medplum%20ClientApplication%20credentials%20for%20the%20no-sign-in%20quickstart&envLink=https%3A%2F%2Fgithub.com%2Fcbetz%2Flast-ehr%2Fblob%2Fmain%2F.env.example)\n\nYou'll still need a **Medplum** project seeded with the synthetic patients (`npm run seed`, below) for the demo to have data.\n\n**Run it locally (authenticated Medplum path).** Prerequisites: Node 22.18+ (or 24.2+), a **Medplum** project (Medplum-hosted [free tier](https://app.medplum.com/) or your own), and one tool-capable model API key (OpenAI, Anthropic, or Amazon Bedrock). For a real agent without Medplum, the same `.env.local` works with `FHIR_BACKEND=hapi` against the local Docker stack, or with any verified synthetic-evaluation adapter ([support matrix](./docs/support.md)).\n\n```bash\ngit clone https://github.com/cbetz/last-ehr.git\ncd last-ehr\nnpm install\ncp .env.example .env.local      # then edit .env.local (see below)\nnpm run seed                     # load synthetic patients into your Medplum\nnpm run dev                      # http://localhost:3000/demo\n```\n\nAt minimum set, in `.env.local`:\n\n- a model key: `OPENAI_API_KEY` (default provider), **or** `ANTHROPIC_API_KEY` with `AI_PROVIDER=anthropic`, **or** AWS credentials plus `AI_PROVIDER=bedrock` and `MODEL_ID`;\n- `NEXT_PUBLIC_MEDPLUM_BASE_URL` / `MEDPLUM_BASE_URL` if you're pointing at your own Medplum (leave blank to use Medplum's hosted API);\n- `MEDPLUM_CLIENT_ID` + `MEDPLUM_CLIENT_SECRET` (a Medplum [ClientApplication](https://www.medplum.com/docs/auth/methods/client-credentials)): used by `npm run seed`, and by the **no-sign-in quickstart** when you also set `NEXT_PUBLIC_QUICKSTART=true`. Or set `NEXT_PUBLIC_MEDPLUM_GOOGLE_CLIENT_ID` to sign in via Medplum's Google OAuth instead.\n\n`npm run seed` loads a small **synthetic** patient set (`scripts/fixtures/patients.ts`: four patients with conditions, medications, allergies, immunizations, and vitals/labs, two named \"Smith\"). It wipes and recreates those patients each run, so it is safe to re-run. Then open `/demo` and ask: *\"find patients named Smith.\"* Use synthetic data only.\n\n**Local FHIR stack, no Medplum account or model key:** the repo ships a Docker\nCompose stack with HAPI FHIR and Postgres, plus an explicit scripted\nwalkthrough that exercises the approval loop without calling an external model\nprovider.\n\n```bash\nnpm install\nnpm run demo:local                            # HAPI, seed data, and http://localhost:3000/demo\n```\n\n`demo:local` forces the local HAPI + scripted configuration without creating or\noverwriting `.env.local`. This fixed, no-network walkthrough always finds the\nseeded Maria Garcia record and proposes one `Heart rate: 72 bpm` Observation.\nIt does not interpret your prompt, browse charts, or call a model provider; the\nFHIR wrapper permits only that synthetic record and observation. Press Ctrl-C\nto stop Next.js; use `npm run demo:local:down` to remove the local stack.\nHonest scope: the local HAPI server runs with **no auth**, so this mode is for\nlocal, single-tenant use only; per-browser session isolation is client-side\nfiltering, not a security boundary. The published `@lastehr/mcp` package targets\nMedplum (credentialed) or a local HAPI stack; the separate checkout-only\n`npm run mcp:demo` Local Lab exposes four synthetic, read-only fixture\ncharts without credentials.\nNeither local route is a real-data or production path. To run a real agent\nagainst HAPI, configure `.env.local` with a supported model key instead. The\nhosted public demo stays on Medplum.\n\nTo run the app container too, use `npm run docker:local` after filling\n`.env.local`; it combines the HAPI/Postgres compose stack with the app image.\nRelease tags and manual publish runs push a prebuilt scripted-demo image to\n`ghcr.io/cbetz/last-ehr`; if the pull fails, no public image has been\npublished yet. See\n[Pull and run from GHCR](./docs/deployment.md#pull-and-run-from-ghcr).\n\nFor the longer version, see [docs/quickstart.md](./docs/quickstart.md).\n\n## Docs\n\n- [Approval-Gated Agent Writes on FHIR](./docs/agent-write-protocol.md): the v0.1 draft protocol — Proposal, Decision, Commit, Audit, and the policy rules.\n- [Protocol conformance suite](./docs/conformance.md): test an implementing MCP stdio server with a scripted reviewer and independent FHIR verification.\n- [Quickstart](./docs/quickstart.md): hosted demo, Medplum local run, and local HAPI evaluation.\n- [Support matrix](./docs/support.md): exactly which backends and interfaces work today.\n- [Architecture](./docs/architecture.md): the chat route, tools, backend adapters, and data boundary.\n- [Backend adapters](./docs/adapters.md): the adapter contract, harnesses, checklist, and contribution path.\n- [Adapter starter](./examples/fhir-adapter-starter): an executable bearer-token FHIR REST baseline with a contract suite.\n- [Approval-gated writes](./docs/approval-gates.md): what the gate protects and what it does not.\n- [MCP server](./docs/mcp.md): published chart tools (Medplum, or the local HAPI stack) — read-only by default with an opt-in human-approved write profile — plus the checkout-only synthetic Local Lab.\n- [FHIR Agent Safety Eval](./docs/evals.md): a disposable synthetic workflow report for proposal, approval, denial, association, and cleanup mechanics.\n- [Deployment](./docs/deployment.md): env vars, rate limiting, Docker, and public-demo hardening.\n- [Threat model](./docs/threat-model.md): trust boundaries and known limitations.\n- [Roadmap](./ROADMAP.md): what is current, next, and deliberately out of scope.\n\n## Launch from the Medplum app (SMART on FHIR)\n\nLast EHR can launch directly from app.medplum.com. Register it once in your\nMedplum project by creating a **ClientApplication** with:\n\n- `launchUri` = `https://<your-deploy>/launch`\n- `redirectUri` = `https://<your-deploy>/launch/callback`\n\nthen set `SMART_CLIENT_ID` to that ClientApplication's id in your deployment.\nLast EHR appears on the **Apps tab of every Patient and Encounter page**;\nlaunching it opens the chat already scoped to that patient, reusing your\nMedplum sign-in (SMART App Launch with PKCE, public client, no secret). The\ntoken is bounded by the granted SMART scopes and your AccessPolicy, and writes\nstill stop at the approval card.\n\n## Use chart tools over MCP\n\n[`@lastehr/mcp`](./packages/mcp) is a small, standalone MCP server for\nMedplum (or the local HAPI stack). It gives Claude Code, Cursor, and other\nMCP clients four bounded chart read tools — `search_patients`,\n`show_patient_info`, `read_chart_section`, and `read_document`, the same read\nsurface the web agent has; writes exist only as an explicit opt-in where a\nhuman approves each proposed write through the client's approval prompt\n([details](./docs/mcp.md)).\n\n```bash\nnpx -y @lastehr/mcp init --client claude-code\n```\n\nUse `npx -y @lastehr/mcp --version` to check the installed MCP package version\nwithout starting the server.\n\nAuthenticate with a least-privilege `MEDPLUM_ACCESS_TOKEN`, or\n`MEDPLUM_CLIENT_ID` plus `MEDPLUM_CLIENT_SECRET`; set `MEDPLUM_BASE_URL` for a\nself-hosted Medplum instance. Read access can still return PHI, so review the\nMCP client's model-provider and data-handling boundary before connecting it to\na real project.\n\nFrom a checkout, `npm run mcp` builds and starts that same package. Full setup,\nclient configuration, and the support boundary are in the [MCP guide](./docs/mcp.md).\nThe [Official MCP Registry listing](https://registry.modelcontextprotocol.io/?q=io.github.cbetz%2Flast-ehr) is the canonical discovery record for the exact, verified release.\n\nTo evaluate the MCP surface before configuring Medplum, use the separate local\nlab from a repository checkout:\n\n```bash\nnpm install\nnpm run mcp:demo -- --client claude-code\n```\n\nIt starts local HAPI, reloads the repository's synthetic fixture records, and\nprints a client registration command. The generated MCP process exposes two of\nthe package's four read-only tool names — `search_patients` and\n`show_patient_info` only, since its fixture client serves too few resource\ntypes for the section reader — and can only resolve those fixture patients.\nIt is not included in `@lastehr/mcp`, does not make HAPI a supported package\nbackend, and must never be pointed at real data. The Local Lab server needs no\nFHIR credential or provider API key, but the MCP client itself still needs its\nnormal model account and may transmit the synthetic tool output to that model.\n\n## Configuration\n\nEvery variable is documented in [`.env.example`](./.env.example). The real-agent\nmodel path is provider-agnostic with one hard rule: **every external provider\noffered here can carry a BAA**, because deployments of this project head toward\nreal clinical data even though the demo is synthetic-only. The `scripted`\noption is a separate, explicit, zero-key local HAPI walkthrough, not a model\nprovider or a PHI-ready mode.\n\n| AI_PROVIDER | Key | Example MODEL_ID | BAA path |\n|---|---|---|---|\n| `scripted` | None | Fixed local synthetic sequence | No external provider call. Requires explicit local HAPI flags; cannot act as a general agent. |\n| `openai` (default) | `OPENAI_API_KEY` | `gpt-4.1-mini` | OpenAI signs BAAs with zero-retention options for API traffic on qualifying plans. |\n| `anthropic` | `ANTHROPIC_API_KEY` | `claude-sonnet-4-6` | Anthropic signs BAAs with zero-retention options for API traffic on qualifying plans. |\n| `bedrock` | AWS credential env + `AWS_REGION` | `us.anthropic.claude-haiku-4-5-20251001-v1:0` | Amazon Bedrock is available under an AWS BAA for eligible services; set an explicit model id or inference profile. |\n\nThe BAA requirement applies to the external-provider rows: signing one is the\noperator's step, not a default, and a bare API key is not PHI-ready.\nAggregators that cannot sign a BAA (OpenRouter, per their current public terms)\nare deliberately not offered. Analytics (PostHog) and the marketing-site\nwaitlist (Neon) are optional and lastehr.com-specific.\n\n## Security & data\n\nLast EHR stores no patient data of its own; everything lives in your FHIR backend. But be clear about what the approval gate is: **it is a write-safety control, not a privacy control.** In external-model modes, anything the agent reads from the chart is sent to your model provider as context, under your API key, with no approval step. The scripted local demo makes no model request and is restricted to its one seeded synthetic record; it is not a path for real data.\n\n**Use synthetic data unless you have real agreements in place.** Pointing this at real PHI requires, at minimum:\n\n- a BAA with your **model provider** that covers your API traffic (consumer plans don't qualify);\n- a HIPAA-eligible **FHIR backend** with its own BAA;\n- your own compliance review. This project is alpha and is **not** a HIPAA-covered service; PHI handling is the operator's responsibility.\n\nSee [SECURITY.md](./SECURITY.md) for the full posture and how to report vulnerabilities.\n\n## Open-core\n\nSelf-hosting is free and Apache-2.0. A managed hosted tier (managed Medplum + a signed BAA, multi-tenancy, billing) may follow, built only after the open-source core has traction.\n\n## Not affiliated\n\nA personal open-source project. Not affiliated with, endorsed by, or sponsored by Medplum, Vercel, or any employer.\n\n## License\n\n[Apache-2.0](./LICENSE). See [NOTICE](./NOTICE) for third-party attributions, [CONTRIBUTING.md](./CONTRIBUTING.md) to contribute, and [GOVERNANCE.md](./GOVERNANCE.md) for how the project is run.\n",
  "bytes": 22058,
  "sha": "f1f6bd14a17bfe38473fd292ebc12eba089e37e8cf0352abf720f7dc8707ae9b",
  "repo_slug": "cbetz/last-ehr",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_cbetz_last_ehr_dd081cad/readme"
}