fhirHydrant MCP

by faulkj

Not rated
GitHub

About

Open-source Node.js FHIR MCP server with SMART Backend Services, metadata-aware search/CRUD tools, compact responses, FHIRPath filtering, safe pagination, audit events, and terminology lookup.

Details

Author
faulkj
Categories
Other, Search, Knowledge Base

Setup

Install fhirHydrant MCP in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/faulkj/fhirHydrant

Follow the installation instructions in the repository README, then restart your MCP client.

A modern, fully configurable, open-source Node.js MCP server for R4+ FHIR APIs. It connects MCP-compatible clients to clinical data over SMART on FHIR v2 Backend Services using signed JWT client credentials.

fhirHydrant turns FHIR resources, named operations, terminology lookups, and pagination into MCP tools. The default resources and operations are starting points: resources, operations, search controls, instructions, and messages can be expanded, trimmed, or replaced through config files without source changes.

- SMART Backend Services auth with JWKS hosting, key rotation, token refresh, and dynamic scopes
- Configurable resource tools for search, direct read, vread, history, and optional metadata-gated CRUD
- Config-driven named operations for clinical data, terminology, IPS, patient matching, validation, and custom workflows
- CapabilityStatement-aware tools, search controls, operation gating, and runtime scope checks
- Token economy features: compact responses, FHIRPath filtering, byte limits,_countshaping, and oversized Bundle retry
- Optional terminology tools, PHI-light audit events (no resource content by default), and stdio or Streamable HTTP transport

Note:FHIR data returned through MCP tool calls may contain PHI. Make sure your MCP client's transcript storage and logging behavior match your compliance requirements.

- Quick Start
-
Tools
-
Metadata And Scope Gating
-
Token Economy And Response Shaping
-
Audit Events
-
SMART Backend Auth And Keys
-
Environment Variables
-
FHIR Version Support
-
Customizing Tools And Messages
-
Transports
-
Deployment Examples
-
Development

- Node.js >= 24
- A supported FHIR server
- For SMART auth (default): a SMART Backend Services client registration and an RSA-2048 or EC P-384 private key whose public key is available through JWKS

To run against a public, unauthenticated FHIR test server, setFHIR_AUTH=noneand skip the client and key entirely (seeUnauthenticated Access).

The stdio transport usually needs an externally hosted JWKS URL. The built-in/jwksendpoint is available only when fhirHydrant runs over HTTP with SMART auth.

# install globally npm install -g fhirhydrant # or run without installing npx fhirhydrant
git clone https://github.com/faulkj/fhirhydrant.git cd fhirhydrant npm install npm run build

For desktop MCP clients, stdio is usually the simplest transport:

{ "mcpServers": { "fhirhydrant": { "command": "npx", "args": ["-y", "fhirhydrant"], "env": { "MCP_TRANSPORT": "stdio", "FHIR_BASE_URL": "https://fhir.example.org", "FHIR_CLIENT_ID": "your-client-id", "FHIR_ACTIVE_KEY": "LS0tLS1CRUdJTi...base64-of-your-pem...", "FHIR_JWKS_URL": "https://example.org/.well-known/jwks.json" } } } }

FHIR_ACTIVE_KEYis your PKCS#8 private key (RSA or EC P-384), base64-encoded. Thekidis derived automatically at startup via a truncated JWK Thumbprint and logged to the console.

To point fhirHydrant at a public, unauthenticated FHIR endpoint (handy for testing against open sandboxes), setFHIR_AUTH=none. No client ID or signing key is required, no token is requested, and requests are sent without anAuthorizationheader:

{ "mcpServers": { "fhirhydrant": { "command": "npx", "args": ["-y", "fhirhydrant"], "env": { "MCP_TRANSPORT": "stdio", "FHIR_AUTH": "none", "FHIR_SERVER_URL": "https://hapi.fhir.org/baseR4" } } } }

fhirHydrant registers tools from configuration and runtime capability checks. The exact list depends on theconfig/resources/folder, granted SMART scopes,/metadata, write settings, operation settings, and terminology settings.

Resource tools are generated from theconfig/resources/folder — one JSON file per resource (e.g.patient.json), scanned at startup. The shipped config covers common clinical, administrative, medication, practitioner, organization, and document resources. Add a file to add a resource, or delete one to drop it — no source changes required.

Each resource tool supports configured search params, optional direct reads with_id,fhirpath, and, unless compact-locked,responseMode. Direct read only happens when_idis the only non-empty argument;_idplus other params stays a search so caller intent is not silently discarded.

Resource tools are search/read by default. SetFHIR_WRITE_CAPABILITIESto enable metadata-gated CRUD actions:

FHIR_WRITE_CAPABILITIES=create,update,patch,delete

vreadis available when the resource hassupportsDirectReadand the server advertises thevreadinteraction.historyis available when the server advertiseshistory-instanceorhistory-type. Both require the SMARTrpermission. Optional_sinceand_atparameters filter history results. History responses are Bundles and support compact mode, FHIRPath, and coalescing.

Write bodies are validated before the FHIR call:body.resourceTypemust match the tool resource,body.idmust match_idfor update when present, and patch requires a JSON Patch array. Scopes are derived from enabled capabilities: read/search usessystem/Patient.rs, create/read/search usessystem/Patient.crs, and full write support usessystem/Patient.cruds. SMART v2 has no separate patch letter, so patch maps tou.

capabilitiesreturns the cached CapabilityStatement summary, registered and skipped tools, search params, operations, and metadata notes.

paginatefetches one Bundle page using a server-returnednextURL validated against the FHIR origin and allowed path prefixes. When compact mode is active and the fetched page has more results, paginate automatically coalesces multiple upstream pages into one compact response (same behavior as resource search tools). Passprefetch=falseto disable coalescing and get a single page.

Theoperatetool invokes FHIR named operations fromconfig/operations.json. The shipped operation catalog covers clinical aggregation, validation, document lookup, terminology operations, IPS generation, and patient matching. You can expand, trim, replace, or disable the operation catalog without source changes.

SetFHIR_TERMINOLOGY_BASE_URLto enable:

These tools call the configured terminology server directly. They do not use the clinical FHIR server credentials. Use a terminology endpoint that matches your selected FHIR release, such ashttps://tx.fhir.org/r4.

SetFHIR_BUNDLE_CAPABILITIES=batch(orbatch,transaction) to enablebundle. This tool submits a FHIR batch or transaction Bundle and returns the server's response through the standard response pipeline.

- Read-only batch Bundles (all GET entries) are allowed with justFHIR_BUNDLE_CAPABILITIES=batch.
- Write entries (POST, PUT, PATCH, DELETE) additionally requireFHIR_BUNDLE_WRITES_ENABLED=trueand the corresponding action inFHIR_WRITE_CAPABILITIES.
- Transaction Bundles require explicitFHIR_BUNDLE_CAPABILITIES=transaction.
- Every entry is preflighted against configured resources, SMART scopes, and metadata interactions. If any single entry fails, the entire Bundle is rejected before submission.

V1 exclusions:Conditional requests, system-level_history, absolute URLs, and$operationURLs inside Bundle entries are not supported.

History in Bundles:vread(Resource/id/_history/vid), instance history (Resource/id/_history), and type history (Resource/_history) entries are allowed in Bundles when the server advertises the corresponding interaction and scopes permit it. These count as read entries.

UnlessFHIR_METADATA_MODE=off, fhirHydrant fetches the FHIR server's CapabilityStatement at startup. Instrictmode:

- Resource tools are registered only when the resource type is present in/metadata
- Server-side search controls such as_count,_sort,_summary,_elements,_include, and_revincludeare exposed only when advertised
- Search params are blocked when the server does not advertise them
- Write actions require bothFHIR_WRITE_CAPABILITIESand matching CapabilityStatement interactions
- Named operations require the target resource type to exist, the granted SMART scope to allow the resource, and the operation itself to be advertised in the resource's CapabilityStatement entry

Inwarnmode, unadvertised params are allowed with a warning, but absent resource types are still skipped. SMART scopes are also checked at runtime, so a tool can exist in the schema and still be blocked by the granted token scope.

FHIR responses are often much larger than an MCP client needs. fhirHydrant shapes responses for token economy after retrieval, using server-side controls when the FHIR server advertises them.

Compact output is AI-oriented JSON, not canonical FHIR. It drops or simplifies FHIR noise and common datatypes such asmeta, narrative, extensions,CodeableConcept,Reference,Quantity, and newer datatypes such asCodeableReference. FHIRPath runs locally; the FHIR server never sees the expression. If evaluation fails, the raw response is withheld and an error is returned.

Every FHIR-data tool (resource tools,paginate,operate,bundle,system_history) returns a single structured envelope, advertised via each tool'soutputSchemaand returned asstructuredContent(the text content is the same envelope serialized). It carries the FHIR payload (data) plus metadata: response mode, ahasMore/continuationpagination signal, Bundle and coalescing stats, and human-readablenotes. The full field list is the tool'soutputSchema.

Oversized responses are chunked when possible (datapreserved, retrievable viacontinuation); if unchunkable, the envelope is markedstatus: "truncated"withdataomitted. Truncation is a successful-but-partial result, not an error. The capabilities and terminology tools return their own structured shapes rather than this FHIR envelope.

When compact mode is active for a search (resource tools or paginate), the server fetches multiple upstream FHIR pages sequentially, compacts each page immediately, and returns one consolidated compact Bundle. This reduces MCP round-trips from many "next page" calls down to one.

- maxResultssets a target — the server stops fetching once this threshold is crossed (may slightly exceed since whole pages are appended)
- prefetch=falsedisables coalescing for one call
- _countstill controls the upstream FHIR page size
- Coalescing stops at configurable page, entry, byte, and time limits
- continuation.urlpoints to where the server stopped; callpaginatewithresponseMode=compactto continue (hasMoreindicates more remain)
- FHIRPath-filtered requests stay single-page (no coalescing)
- responseMode=fullalways returns a single upstream page

SetFHIR_AUDIT_SINKto any combination ofconsole,file, andhttp.

Thehttpsink POSTs each audit event to an external collector, SIEM, or FHIR audit repository (not the FHIR server itself). SetFHIR_AUDIT_HTTP_URLto the destination andFHIR_AUDIT_HTTP_FORMATto eitherraw(the internal PHI-light audit JSON, for generic collectors such as Splunk HEC or Datadog) orfhir-auditevent(a minimal FHIR R4AuditEventresource, suitable for ATNA-style and FHIR-native audit repositories). Thefhir-auditeventmapping is intentionally lightweight — it is not a full ATNA/BALP compliance profile. An optionalFHIR_AUDIT_HTTP_AUTHvalue is sent verbatim as theAuthorizationheader. Delivery is fire-and-forget with a 5s timeout; transport failures are logged and never affect tool responses.

Audit events include timestamp, tool, resource type when applicable, operation, status, duration, response size, pagination summary, request ID, and optional proxy-authenticated user. They do not include FHIR resource content by default.

When running behind an authenticating proxy, setFHIR_AUDIT_USER_HEADERto the trusted identity header injected by that proxy:

Common headers: Azure EasyAuthX-MS-CLIENT-PRINCIPAL-NAME, OAuth2 ProxyX-Auth-Request-Email, Cloudflare AccessCf-Access-Authenticated-User-Email.

Only use this when the proxy strips or overwrites inbound copies of that header. Otherwise clients can spoof arbitrary audit users.

fhirHydrant uses SMART Backend Services: client credentials plus a signed JWT assertion. This is backend FHIR access, not browser-based SMART standalone launch; there is no interactive redirect/login flow in the MCP path.

FHIR_ACTIVE_KEYholds the raw PKCS#8 signing key (RSA, signed RS384, or EC P-384, signed ES384). In HTTP mode, the built-in/jwksendpoint exposes public keys for the active key plus any retired keys whenFHIR_JWKS_URLis unset. Thekidfor each key is derived automatically via a truncated RFC 7638 JWK Thumbprint (first 12 base64url chars of SHA-256 over the canonical public JWK members) and logged at startup.
- Generate a new key (RSA-2048 or EC P-384).
- Add the new PEM toFHIR_RETIRED_KEYSand redeploy so JWKS includes both.
- Register the new kid (logged at startup) with your auth server.
- Move the new PEM toFHIR_ACTIVE_KEYand move the old PEM toFHIR_RETIRED_KEYS. Redeploy.
- After auth-server caches expire, remove the old key fromFHIR_RETIRED_KEYS.

If using external JWKS, publish the new public key before switchingFHIR_ACTIVE_KEY.

ExplicitFHIR_SERVER_URLandFHIR_TOKEN_URLvalues always win over derived URLs.

SetFHIR_VERSIONto select the active R4+ FHIR release. It controls the derived FHIR API URL, FHIRPath model context, and compact response model metadata. Some releases may use the nearest compatible FHIRPath model. For terminology, use an endpoint that matches the selected FHIR release. Startup logs hint when explicit FHIR or terminology URLs appear to reference a different version.

Everything underconfig/is customizable without source changes.

Config is resolved as apartial overlay: for each file, a./config/<file>in the current working directory (if present) overrides the packaged default, and anything you omit falls back to the built-in default. So npm installs work out of the box, and to customize you drop a./configfolder next to where you launch the server containingonlythe files you want to change.

- Whole-file(resources/.json,operations.json,search-controls.json,core-tools.json,instructions/): a file you provide replaces the packaged file entirely. A new resource file (e.g../config/resources/myresource.json) adds a tool. The overlay can override and add, butcannot removea packaged resource — to ship a strictly minimal catalog, remove the packagedconfig/resources/files (see the compose example).
- Per-key(messages/.json): a local file overrides only the individual keys it contains; every other key falls back to the packaged default. So you can retune a single description or message without copying the whole file. Unknown keys, empty values, and malformed JSONfail fast at startupto catch typos.

messages/.jsonfiles are read once at process startup. Changing them requires a server restart (and, for tool schemas or instructions, a client reconnect) to take effect. Development hot reload for resources, search controls, and operations is described below.

Each file inconfig/resources/is a single resource definition object. Files are scanned in filename order; the filename is conventionally the lowercase resource name (e.g.patient.json). Each object has these fields:

searchParamsvalues are descriptions, not a full FHIR capability model. Server-specific search behavior can still apply.

In development (NODE_ENVis notproduction), theconfig/resources/folder,search-controls.json, andoperations.jsonare watched. Invalid JSON keeps the last valid snapshot. A materially changed reload is applied transactionally: when the derived SMART scopes change, a replacement token is acquired before the new definitions and tool registrations are committed, so a failed acquisition leaves the running catalog untouched. Adding/removing tools, operation and param-name schema changes are re-registered live — no restart needed. Semantically unchanged saves cause no refresh. Production reads config once at startup, but a runtime/metadatachange (viacapabilities(refresh=true)) or a backend SMART-scope change on token refresh re-evaluates the available tools in every mode.

One boundary is unavoidable: the tool list and schemas hot-refresh, but the serverinstructionsare sent once during MCPinitializeand cannot be replaced on an existing connection. A client must reconnect/reinitialize to receive changed instruction text.

SetMCP_TRANSPORT=stdio. stdout is reserved for the MCP protocol; logs are redirected to stderr. Use an externalFHIR_JWKS_URLfor stdio deployments.

HTTP transport is stateless and exposes MCP at:

POST http://localhost:5000/mcp Accept: application/json, text/event-stream Content-Type: application/json
{ "mcpServers": { "fhirhydrant": { "url": "http://localhost:5000/mcp" } } }

GET /healthreturns a no-PHI readiness snapshot:

{ "status": "ok", "mcp": true, "metadata": true, "tools": 23, "auth": true, "tokenExpiresIn": 287 }

When authorization is enabled,authzreports the active provider andtoolsis omitted because the registered tool count is caller-specific.

Use a reverse proxy for TLS and user authentication when exposing HTTP beyond localhost. SetALLOWED_HOSTSwhen binding to a public interface.

Per-caller authorization (Entra, optional)

By default (MCP_AUTHZ=none) every caller sees the full tool set gated only by/metadataand the backend SMART scopes. SettingMCP_AUTHZ=entraadds an optional per-caller layer: each/mcprequest must carry anAuthorization: Bearer <token>issued by Microsoft Entra, and the caller'sApp Rolesdetermine which tools are built for that request. This is MCP-layer authorization only — it never replaces the FHIR server's own authorization, and it can onlysubtractfrom what the backend SMART token and config already allow.

The API app registration must setrequestedAccessTokenVersionto2in its manifest. The provider validates tenant-specific v2 issuers and expectsMCP_ENTRA_AUDIENCEto be the API application's client ID.

Tools a caller lacks a role for are not registered at all — they are absent fromtools/list, not merely blocked. Helper tools (capabilities,paginate,terminology_lookup,code_search) are never gated.

App Role values (with the defaultFhirHydrantprefix):

Requires HTTP transport;MCP_AUTHZ=entrawithMCP_TRANSPORT=stdiofails at startup. Missing or invalid bearer tokens receive401.

Entra is the only shipped provider, but the authorization layer is provider-neutral. This is asource extension, not a runtime plugin: the npm package ships onlybin/server.js(providers are bundled in), so adding one means forking or cloning the repo and rebuilding.

The shared pipeline is provider-agnostic — a provider only maps anAuthorizationheader to{ subject, roles }. The role vocabulary (.Read/.Write/Operation.<key>/Bundle/SystemHistory.Read/Admin) andMCP_ROLE_PREFIXhandling are applied bydecideAuthzfor every provider.

To add one (e.g.auth0) takes just two edits:
- Createts/mcp/authz/auth0.tsexporting anAuthzProvider— implementvalidate(authorization)to return{ subject, roles }(throw to reject), and optionallyvalidateConfig()to fail fast on missing provider env. Keep all provider-specific env inside this module; do not add fields toConfig.
- Add one entry tots/mcp/authz/registry.ts:auth0: () => import("./auth0.ts").then((m) => m.auth0Provider).

That's it. TheAuthzModetype, theMCP_AUTHZparser, and its error message all derive from the registry keys automatically, soMCP_AUTHZ=auth0just works with full type safety — no other file needs to change.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.