@lastehr/mcp is the smallest installable Last EHR surface: an MCP server
that is read-only by default (search patients, open a chart), with one
opt-in write profile that carries the web app's proposal/approval semantics
onto MCP (below). It is deliberately separate from the web app.
Zero-credential Local Lab (checkout only)
Want to inspect the MCP interaction before creating a Medplum project or
configuring a model-provider API key? The repository includes a separate
synthetic HAPI Local Lab. It is intentionally not part of @lastehr/mcp
and does not broaden that package's support boundary.
From a local checkout with Node 22.18+ and Docker running:
npm install
npm run mcp:demo -- --client claude-codeThe command starts the repository's local HAPI + Postgres stack, waits for it,
recreates the four synthetic fixture charts, then prints a ready-to-paste
Claude Code registration command. For a JSON configuration (including Cursor),
use either the default or --client cursor:
npm run mcp:demo
npm run mcp:demo -- --client cursorThe generated client process invokes the checkout directly, rather than an npm lifecycle command, so its stdout is reserved for MCP JSON-RPC. The lab server does not require or read FHIR/Medplum credentials or a model-provider API key. Your MCP client still needs its usual authenticated model account and may send the returned synthetic chart data to that provider. Docker may also pull the local images on the first run.
Its boundary is deliberately narrow:
- exactly
search_patientsandshow_patient_info, both withreadOnlyHint— the Local Lab deliberately dropsread_chart_sectionandread_documentthat@lastehr/mcpoffers, because its fixture client serves six resource types and the section reader advertises 23, so 17 would refuse; - only the four records carrying this repository's synthetic fixture identifiers are discoverable;
- the generated configuration targets
127.0.0.1:8080/fhir, and the server accepts only loopback HAPI endpoints; - no write tool, write flag, credential configuration, or arbitrary FHIR endpoint exists.
The local HAPI container has no authentication. Use this lab only for synthetic
data on one machine. Compose binds it to 127.0.0.1 by default; do not change
that to a network-facing port. It is an evaluation experience, not generic HAPI
support, an authorization layer, a PHI workflow, or a release of
@lastehr/mcp.
Run npm run mcp:demo -- --prepare when you only want to pre-warm the local
stack. The --serve mode is reserved for the generated MCP configuration and
must not be launched through npm run, because npm may write non-protocol text
to stdout. Keep the local stack running while the client is connected; use
npm run demo:local:down to remove it when finished. Port 8080 must be free.
Install and connect
npx -y @lastehr/mcp initThe command prints a portable MCP configuration. Add a least-privilege token, then place the result in your MCP client's configuration:
{
"mcpServers": {
"lastehr": {
"command": "npx",
"args": ["-y", "@lastehr/mcp"],
"env": {
"MEDPLUM_ACCESS_TOKEN": "<replace-with-a-least-privilege-token>"
}
}
}
}For Claude Code, print the registration command instead:
npx -y @lastehr/mcp init --client claude-codeThe process inherits MEDPLUM_* variables from your shell or MCP client
configuration. Start it directly with npx -y @lastehr/mcp when you want to
test a stdio connection yourself.
Auth
The package uses Medplum credentials:
MEDPLUM_CLIENT_ID=...
MEDPLUM_CLIENT_SECRET=...or:
MEDPLUM_ACCESS_TOKEN=...Set MEDPLUM_BASE_URL for self-hosted Medplum.
Local stack (FHIR_BACKEND=hapi)
The published package also honors the same env pair the web app and seed use, so a fully local synthetic stack gets MCP too:
FHIR_BACKEND=hapi
FHIR_BASE_URL=http://localhost:8080/fhir # or HAPI_BASE_URLNo credentials: the repository's HAPI evaluation stack is no-auth by design,
which is exactly why the same caveats apply as in the web app — local,
single-tenant, synthetic data only; never point it at an exposed server or
treat it as an authorization layer. Any configured MEDPLUM_* values are
unused in this mode (a checkout's .env commonly carries both). The tools
and the write-policy boundary (read-only by default, the same opt-in write
profile) are identical to the Medplum mode.
This is distinct from npm run mcp:demo (the checkout-only Local Lab), which
remains fixture-restricted and needs no configuration at all.
Registry metadata
The package is listed in the Official MCP Registry, the client-facing installation record for the verified npm release.
Maintainers publish that immutable record through the manual Publish MCP Registry metadata GitHub Actions workflow after the corresponding npm version is public.
Tool surface
By default the package exposes four read tools, all marked with MCP's
readOnlyHint:
search_patientsshow_patient_inforead_chart_section— one of 23 patient-scoped sections, with code, measurement-name, status, category and date filtersread_document— the text of one document already listed in the chart
As of 0.3.0 those are the same implementations the Last EHR web agent uses
(packages/mcp/src/chart-read.ts), not a reduced copy. That is deliberate:
each of their honesty properties came from a real false negative found against
a live FHIR server, and a second implementation would have re-earned every one.
The retired 0.1.x line was permanently read-only, and read-only remains the
default forever. As of 0.2.0 there is exactly one opt-in beyond it, the
proposal-shaped write profile below.
What a reply tells you it could not see
An empty result is never proof of absence, and the server says why. These are the fields to check before telling anyone a patient has no record of something:
| Field | Meaning |
|---|---|
truncated | The server's window came back full, so older records may exist beyond it. Measured at the window, not at the surviving row count. |
codeFilterUnmatched | The section does hold records; none carry the code you filtered by. Text-only CodeableConcepts cannot match a coded search. |
includeUnsupported | The backend refused the reference lookup. Not the same as there being no references. |
unreadable (documents) | The document exists and its contents were not read: a scan, or a body stored as a pointer rather than inline. |
A filter a section cannot apply is refused with that section's legal values, and those refusals reach the client verbatim rather than being scrubbed into a generic backend error — the message exists so the caller can correct itself. Backend errors are still scrubbed, since a FHIR server may put resource fragments in one.
Chart free text arrives wrapped in <chart_text> tags: that content is data,
never instructions. The server declares this, and the flag meanings above, in
its MCP instructions, because unlike the web app it does not control the
client's system prompt.
Proposal-shaped writes (0.2.0, opt-in)
LASTEHR_MCP_WRITES=proposal adds the web demo's write actions —
add_note (Communication), record_observation and
record_superseding_observation (Observation), and create_task (Task) — as
elicitation-gated proposals: the tool builds the exact FHIR resource it
would create, presents those fields to the human through MCP elicitation
(client-rendered accept/decline/cancel with a single "Approve and save?"
boolean), and commits only on an explicit approval. A decline, cancel,
unapproved accept, or any approval-transport failure saves nothing and the
tool result says so. Every value the flag accepts other than proposal is
rejected loudly.
The profile is a binding of the repository's framework-neutral Approval-Gated Agent Writes on FHIR protocol (v0.1 draft). The gate is structural, not advisory:
- Capability-gated, fail closed. The write tools are offered only to
clients that declared the
elicitationcapability at initialization; a host that cannot render the approval never sees a write tool. - What you see is what saves. The elicitation message contains the exact proposed fields; the committed resource is built from the same parsed input, with the same caps as the web demo's tools.
- Tagged for audit. Approved writes carry
meta.tag {https://lastehr.com/mcp | approved-proposal}so operators can find every agent-written record with one_tagsearch, plus the standard AIAST security label ("Artificial Intelligence asserted") inmeta.securityper the HL7 AI Transparency on FHIR IG. SetLASTEHR_WRITE_PROVENANCE=trueto also emit aProvenanceresource per approved write naming the agent as author and the reviewer as verifier — see the protocol's Audit section. - Narrowable, never widenable.
LASTEHR_WRITE_TOOLS_DISABLED(comma-separated:add_note,record_observation,record_superseding_observation,create_task) unregisters write tools entirely — unlisted and uncallable, with unknown names refusing startup. Embedders can pass a deny-onlypolicyhook inWriteToolOptions: checked before the reviewer is asked, re-checked at commit, fail-closed, and its denials are attributed to configuration, never to a human (see the protocol's Decision section). - Transport-adaptable. The approval exchange lives behind one function
(
createElicitationApproval); the MCP 2026-07-28 release candidate replaces server-initiated elicitation with Multi Round-Trip Requests, and only that adapter changes when it lands.
The same data caveats as reads apply, doubled: only enable writes against a project whose access policy you have scoped, and never against real data you are not authorized to modify. The elicitation exchange requests a decision, never data.
Data and support boundary
Read-only does not mean low-risk: show_patient_info can return PHI-rich
chart data. Use the smallest Medplum AccessPolicy that meets the task, confirm
that your MCP client and model provider are appropriate for the data, and do
not treat this package as an authorization layer.
@lastehr/mcp supports hosted or self-hosted Medplum authentication, plus
the repository's local no-auth HAPI stack (FHIR_BACKEND=hapi, local synthetic
data only). It does not claim generic FHIR, SMART launch, or browser-approval
parity. See the support matrix for the complete boundary.
From a checkout
The repository includes the same Medplum package for contributors:
npm run mcpThis builds @lastehr/mcp and starts it with your local Medplum environment
variables — including LASTEHR_MCP_WRITES if you have opted in.
Roadmap
- Better read-tool coverage where it can stay bounded and auditable.
- Proposal-shaped writes shipped in
0.2.0behindLASTEHR_MCP_WRITES=proposal(see above), riding MCP's reviewable confirmation protocol (elicitation). AIAST labeling and opt-in Provenance emission aligned with HL7's AI Transparency IG shipped with it (see "Tagged for audit" above).
Want a concrete starting point?
Inspect the fixture-restricted local MCP surface before connecting a real project.