{
  "markdown": "# fhirHydrant: _FHIR MCP Server_\n\nA modern, fully configurable, open-source Node.js Model Context Protocol (MCP) \nserver for R4+ FHIR APIs. It connects MCP-compatible LLM AI clients to \nclinical data over SMART on FHIR v2 Backend Services using signed JWT client \ncredentials.\n\nfhirHydrant turns FHIR resources, named operations, terminology lookups, and\npagination into MCP tools. The default resources and operations are starting\npoints: resources, operations, search controls, instructions, and messages can\nbe expanded, trimmed, or replaced through config files without source changes.\n\n- SMART Backend Services auth with JWKS hosting, key rotation, token refresh,\n  and dynamic scopes\n- Configurable resource tools for search, direct read, vread, history, and\n  optional metadata-gated CRUD\n- Config-driven named operations for clinical data, terminology, IPS, patient\n  matching, validation, and custom workflows\n- CapabilityStatement-aware tools, search controls, operation gating, and\n  runtime scope checks\n- Token economy features: compact responses, FHIRPath filtering, byte limits,\n  `_count` shaping, and oversized Bundle retry\n- Optional terminology tools, PHI-light audit events (no resource content by\n  default), and stdio or Streamable HTTP transport\n\n> **Note:** FHIR data returned through MCP tool calls may contain PHI.\n> Make sure your MCP client's transcript storage and logging behavior match\n> your compliance requirements.\n\n## Contents\n\n- [Quick Start](#quick-start)\n- [Tools](#tools)\n- [Metadata And Scope Gating](#metadata-and-scope-gating)\n- [Token Economy And Response Shaping](#token-economy-and-response-shaping)\n- [Audit Events](#audit-events)\n- [SMART Backend Auth And Keys](#smart-backend-auth-and-keys)\n- [Environment Variables](#environment-variables)\n- [FHIR Version Support](#fhir-version-support)\n- [Customizing Tools And Messages](#customizing-tools-and-messages)\n- [Transports](#transports)\n- [Deployment Examples](#deployment-examples)\n- [Development](#development)\n\n## Quick Start\n\n### Requirements\n\n- Node.js >= 24\n- A supported FHIR server\n- For SMART auth (default): a SMART Backend Services client registration and an\n  RSA-2048 or EC P-384 private key whose public key is available through JWKS\n\nTo run against a public, unauthenticated FHIR test server, set `FHIR_AUTH=none`\nand skip the client and key entirely (see [Unauthenticated Access](#unauthenticated-access)).\n\nThe stdio transport usually needs an externally hosted JWKS URL. The built-in\n`/jwks` endpoint is available only when fhirHydrant runs over HTTP with SMART auth.\n\n### Install\n\n```sh\n# install globally\nnpm install -g fhirhydrant\n\n# or run without installing\nnpx fhirhydrant\n```\n\nRun from source:\n\n```sh\ngit clone https://github.com/faulkj/fhirhydrant.git\ncd fhirhydrant\nnpm install\nnpm run build\n```\n\n### MCP Client Config\n\nFor desktop MCP clients, stdio is usually the simplest transport:\n\n```json\n{\n   \"mcpServers\": {\n      \"fhirhydrant\": {\n         \"command\": \"npx\",\n         \"args\": [\"-y\", \"fhirhydrant\"],\n         \"env\": {\n            \"MCP_TRANSPORT\": \"stdio\",\n            \"FHIR_BASE_URL\": \"https://fhir.example.org\",\n            \"FHIR_CLIENT_ID\": \"your-client-id\",\n            \"FHIR_ACTIVE_KEY\": \"LS0tLS1CRUdJTi...base64-of-your-pem...\",\n            \"FHIR_JWKS_URL\": \"https://example.org/.well-known/jwks.json\"\n         }\n      }\n   }\n}\n```\n\n`FHIR_ACTIVE_KEY` is your PKCS#8 private key (RSA or EC P-384), base64-encoded.\nThe `kid` is derived automatically at startup via a truncated JWK Thumbprint and\nlogged to the console.\n\n#### Unauthenticated Access\n\nTo point fhirHydrant at a public, unauthenticated FHIR endpoint (handy for\ntesting against open sandboxes), set `FHIR_AUTH=none`. No client ID or signing\nkey is required, no token is requested, and requests are sent without an\n`Authorization` header:\n\n```json\n{\n   \"mcpServers\": {\n      \"fhirhydrant\": {\n         \"command\": \"npx\",\n         \"args\": [\"-y\", \"fhirhydrant\"],\n         \"env\": {\n            \"MCP_TRANSPORT\": \"stdio\",\n            \"FHIR_AUTH\": \"none\",\n            \"FHIR_SERVER_URL\": \"https://hapi.fhir.org/baseR4\"\n         }\n      }\n   }\n}\n```\n\n## Tools\n\nfhirHydrant registers tools from configuration and runtime capability checks.\nThe exact list depends on the `config/resources/` folder, granted SMART scopes,\n`/metadata`, write settings, operation settings, and terminology settings.\n\n| Tool or family | Available when | Purpose |\n| --- | --- | --- |\n| Resource tools | Resource is configured and allowed by metadata/scopes | Search, direct-read, vread, history, and optionally CRUD FHIR resources |\n| `system_history` | Server advertises system `history` interaction and scopes allow it | Retrieve system-level change history across all resource types |\n| `capabilities` | Always registered | Inspect CapabilityStatement summary, registered tools, skipped tools, search params, operations, and metadata notes |\n| `paginate` | Always registered | Fetch the next page of a FHIR Bundle using a server-returned `next` URL |\n| `operate` | At least one named operation passes gating | Invoke configured FHIR named operations for clinical data, terminology, IPS, matching, validation, or custom workflows |\n| `bundle` | `FHIR_BUNDLE_CAPABILITIES` is set | Submit a FHIR batch or transaction Bundle; writes require additional opt-in |\n| `terminology_lookup` | `FHIR_TERMINOLOGY_BASE_URL` is set | Look up one LOINC or SNOMED CT code |\n| `code_search` | `FHIR_TERMINOLOGY_BASE_URL` is set | Search LOINC or SNOMED CT codes by text |\n\n### Resource Tools\n\nResource tools are generated from the [config/resources/](config/resources/)\nfolder — one JSON file per resource (e.g. `patient.json`), scanned at startup.\nThe shipped config covers common clinical, administrative, medication,\npractitioner, organization, and document resources. Add a file to add a\nresource, or delete one to drop it — no source changes required.\n\nEach resource tool supports configured search params, optional direct reads\nwith `_id`, `fhirpath`, and, unless compact-locked, `responseMode`. Direct read\nonly happens when `_id` is the only non-empty argument; `_id` plus other params\nstays a search so caller intent is not silently discarded.\n\nResource tools are search/read by default. Set `FHIR_WRITE_CAPABILITIES` to\nenable metadata-gated CRUD actions:\n\n```sh\nFHIR_WRITE_CAPABILITIES=create,update,patch,delete\n```\n\n| Action | Required params | FHIR call |\n| --- | --- | --- |\n| `vread` | `_id`, `_vid` | `GET /ResourceType/{id}/_history/{vid}` |\n| `history` | `_id` (instance) or none (type) | `GET /ResourceType/{id}/_history` or `GET /ResourceType/_history` |\n| `create` | `body` | `POST /ResourceType` |\n| `update` | `_id`, `body` | `PUT /ResourceType/{id}` |\n| `patch` | `_id`, `body` | `PATCH /ResourceType/{id}` with JSON Patch |\n| `delete` | `_id` | `DELETE /ResourceType/{id}` |\n\n`vread` is available when the resource has `supportsDirectRead` and the server\nadvertises the `vread` interaction. `history` is available when the server\nadvertises `history-instance` or `history-type`. Both require the SMART `r`\npermission. Optional `_since` and `_at` parameters filter history results.\nHistory responses are Bundles and support compact mode, FHIRPath, and\ncoalescing.\n\nWrite bodies are validated before the FHIR call: `body.resourceType` must match\nthe tool resource, `body.id` must match `_id` for update when present, and patch\nrequires a JSON Patch array. Scopes are derived from enabled capabilities:\nread/search uses `system/Patient.rs`, create/read/search uses\n`system/Patient.crs`, and full write support uses `system/Patient.cruds`.\nSMART v2 has no separate patch letter, so patch maps to `u`.\n\n### Core Tools\n\n`capabilities` returns the cached CapabilityStatement summary, registered and\nskipped tools, search params, operations, and metadata notes.\n\n`paginate` fetches one Bundle page using a server-returned `next` URL validated\nagainst the FHIR origin and allowed path prefixes. When compact mode is active\nand the fetched page has more results, paginate automatically coalesces\nmultiple upstream pages into one compact response (same behavior as resource\nsearch tools). Pass `prefetch=false` to disable coalescing and get a single\npage.\n\n### Named Operations\n\nThe `operate` tool invokes FHIR named operations from `config/operations.json`.\nThe shipped operation catalog covers clinical aggregation, validation, document\nlookup, terminology operations, IPS generation, and patient matching. You can\nexpand, trim, replace, or disable the operation catalog without source changes.\n\n### Terminology Tools\n\nSet `FHIR_TERMINOLOGY_BASE_URL` to enable:\n\n| Tool | Description |\n| --- | --- |\n| `terminology_lookup` | Looks up one LOINC or SNOMED CT code |\n| `code_search` | Searches codes by text filter with paging support |\n\nThese tools call the configured terminology server directly. They do not use\nthe clinical FHIR server credentials. Use a terminology endpoint that matches\nyour selected FHIR release, such as `https://tx.fhir.org/r4`.\n\n### Bundle Execution\n\nSet `FHIR_BUNDLE_CAPABILITIES=batch` (or `batch,transaction`) to enable\n`bundle`. This tool submits a FHIR batch or transaction Bundle and\nreturns the server's response through the standard response pipeline.\n\n**Safety model:**\n- Read-only batch Bundles (all GET entries) are allowed with just\n  `FHIR_BUNDLE_CAPABILITIES=batch`.\n- Write entries (POST, PUT, PATCH, DELETE) additionally require\n  `FHIR_BUNDLE_WRITES_ENABLED=true` and the corresponding action in\n  `FHIR_WRITE_CAPABILITIES`.\n- Transaction Bundles require explicit `FHIR_BUNDLE_CAPABILITIES=transaction`.\n- Every entry is preflighted against configured resources, SMART scopes, and\n  metadata interactions. If any single entry fails, the entire Bundle is\n  rejected before submission.\n\n**V1 exclusions:** Conditional requests, system-level `_history`, absolute URLs,\nand `$operation` URLs inside Bundle entries are not supported.\n\n**History in Bundles:** `vread` (`Resource/id/_history/vid`), instance history\n(`Resource/id/_history`), and type history (`Resource/_history`) entries are\nallowed in Bundles when the server advertises the corresponding interaction and\nscopes permit it. These count as read entries.\n\n## Metadata And Scope Gating\n\nUnless `FHIR_METADATA_MODE=off`, fhirHydrant fetches the FHIR server's\nCapabilityStatement at startup. In `strict` mode:\n\n- Resource tools are registered only when the resource type is present in\n  `/metadata`\n- Server-side search controls such as `_count`, `_sort`, `_summary`,\n  `_elements`, `_include`, and `_revinclude` are exposed only when advertised\n- Search params are blocked when the server does not advertise them\n- Write actions require both `FHIR_WRITE_CAPABILITIES` and matching\n  CapabilityStatement interactions\n- Named operations require the target resource type to exist, the granted\n  SMART scope to allow the resource, and the operation itself to be advertised\n  in the resource's CapabilityStatement entry\n\nIn `warn` mode, unadvertised params are allowed with a warning, but absent\nresource types are still skipped. SMART scopes are also checked at runtime, so a\ntool can exist in the schema and still be blocked by the granted token scope.\n\n## Token Economy And Response Shaping\n\nFHIR responses are often much larger than an MCP client needs. fhirHydrant\nshapes responses for token economy after retrieval, using server-side controls\nwhen the FHIR server advertises them.\n\n| Feature | Behavior |\n| --- | --- |\n| `_count` default/cap | No `_count` injected by default (server decides page size). Set `FHIR_DEFAULT_COUNT` to inject one; `FHIR_MAX_COUNT` caps explicit caller values (0 = no cap) |\n| Page coalescing | When compact mode is active, the server fetches multiple upstream pages sequentially, compacts each immediately, and returns one consolidated Bundle. Controlled by `maxResults`, `prefetch`, and `FHIR_PREFETCH_*` env vars |\n| Byte limit | `FHIR_MAX_RESPONSE_BYTES` limits every model-facing JSON response; oversized Bundles are chunked transparently |\n| Auto-retry | Oversized search Bundles attempt local chunking first, then retry with smaller `_count` as a fallback |\n| FHIRPath | `fhirpath` filters the returned FHIR JSON locally and returns matching nodes as an array |\n| Compact mode | `responseMode=compact` strips common FHIR envelope noise and simplifies datatypes |\n| Full mode | `responseMode=full` returns raw FHIR JSON |\n| Locked compact | `FHIR_RESPONSE_MODE=compact-locked` hides `responseMode` from the tool schema |\n| Native artifacts | Non-JSON responses (documents, images, DICOM, RTF, HTML, XML, CSV, NDJSON, ZIP, octet-stream) and JSON FHIR Binary are normalized into a metadata envelope plus one MCP embedded text/blob resource. Capped by `FHIR_MAX_ARTIFACT_MB` (not the JSON limit), never chunked, and never passed through FHIRPath/compaction/coalescing. JSON-only shaping arguments are ignored with a note |\n\nCompact output is AI-oriented JSON, not canonical FHIR. It drops or simplifies\nFHIR noise and common datatypes such as `meta`, narrative, extensions,\n`CodeableConcept`, `Reference`, `Quantity`, and newer datatypes such as\n`CodeableReference`.\nFHIRPath runs locally; the FHIR server never sees the expression. If evaluation\nfails, the raw response is withheld and an error is returned.\n\n### Structured Response Envelope\n\nEvery FHIR-data tool (resource tools, `paginate`, `operate`, `bundle`,\n`system_history`) returns a single structured envelope, advertised via each\ntool's `outputSchema` and returned as `structuredContent` (the text content is\nthe same envelope serialized). It carries the FHIR payload (`data`) plus\nmetadata: response mode, a `hasMore`/`continuation` pagination signal, Bundle and\ncoalescing stats, and human-readable `notes`. The full field list is the tool's\n`outputSchema`.\n\nOversized responses are chunked when possible (`data` preserved, retrievable via\n`continuation`); if unchunkable, the envelope is marked `status: \"truncated\"`\nwith `data` omitted. Truncation is a successful-but-partial result, not an error.\nThe capabilities and terminology tools return their own structured shapes rather\nthan this FHIR envelope.\n\n### Page Coalescing\n\nWhen compact mode is active for a search (resource tools or paginate), the\nserver fetches multiple upstream FHIR pages sequentially, compacts each page\nimmediately, and returns one consolidated compact Bundle. This reduces MCP\nround-trips from many \"next page\" calls down to one.\n\n- `maxResults` sets a target — the server stops fetching once this threshold\n  is crossed (may slightly exceed since whole pages are appended)\n- `prefetch=false` disables coalescing for one call\n- `_count` still controls the upstream FHIR page size\n- Coalescing stops at configurable page, entry, byte, and time limits\n- `continuation.url` points to where the server stopped; call `paginate` with\n  `responseMode=compact` to continue (`hasMore` indicates more remain)\n- FHIRPath-filtered requests stay single-page (no coalescing)\n- `responseMode=full` always returns a single upstream page\n\n## Audit Events\n\nSet `FHIR_AUDIT_SINK` to any combination of `console`, `file`, and `http`.\n\nThe `http` sink POSTs each audit event to an external collector, SIEM, or FHIR\naudit repository (not the FHIR server itself). Set `FHIR_AUDIT_HTTP_URL` to the\ndestination and `FHIR_AUDIT_HTTP_FORMAT` to either `raw` (the internal\nPHI-light audit JSON, for generic collectors such as Splunk HEC or Datadog) or\n`fhir-auditevent` (a minimal FHIR R4 `AuditEvent` resource, suitable for\nATNA-style and FHIR-native audit repositories). The `fhir-auditevent` mapping is\nintentionally lightweight — it is not a full ATNA/BALP compliance profile. An\noptional `FHIR_AUDIT_HTTP_AUTH` value is sent verbatim as the `Authorization`\nheader. Delivery is fire-and-forget with a 5s timeout; transport failures are\nlogged and never affect tool responses.\n\nAudit events include timestamp, tool, resource type when applicable, operation,\nstatus, duration, response size, pagination summary, request ID, and optional\nproxy-authenticated user. They do not include FHIR resource content by default.\n\nWhen running behind an authenticating proxy, set `FHIR_AUDIT_USER_HEADER` to\nthe trusted identity header injected by that proxy:\n\nCommon headers: Azure EasyAuth `X-MS-CLIENT-PRINCIPAL-NAME`, OAuth2 Proxy\n`X-Auth-Request-Email`, Cloudflare Access\n`Cf-Access-Authenticated-User-Email`.\n\nOnly use this when the proxy strips or overwrites inbound copies of that\nheader. Otherwise clients can spoof arbitrary audit users.\n\n## SMART Backend Auth And Keys\n\nfhirHydrant uses SMART Backend Services: client credentials plus a signed JWT\nassertion. This is backend FHIR access, not browser-based SMART standalone\nlaunch; there is no interactive redirect/login flow in the MCP path.\n\n`FHIR_ACTIVE_KEY` holds the raw PKCS#8 signing key (RSA, signed RS384, or EC\nP-384, signed ES384). In HTTP mode, the built-in `/jwks` endpoint exposes public\nkeys for the active key plus any retired keys when `FHIR_JWKS_URL` is unset. The\n`kid` for each key is derived automatically via a truncated RFC 7638 JWK\nThumbprint (first 12 base64url chars of SHA-256 over the canonical public JWK\nmembers) and logged at startup.\n\nKey rotation workflow:\n1. Generate a new key (RSA-2048 or EC P-384).\n2. Add the new PEM to `FHIR_RETIRED_KEYS` and redeploy so JWKS includes both.\n3. Register the new kid (logged at startup) with your auth server.\n4. Move the new PEM to `FHIR_ACTIVE_KEY` and move the old PEM to\n   `FHIR_RETIRED_KEYS`. Redeploy.\n5. After auth-server caches expire, remove the old key from `FHIR_RETIRED_KEYS`.\n\nIf using external JWKS, publish the new public key before switching\n`FHIR_ACTIVE_KEY`.\n\n## Environment Variables\n\nSee [.env.example](.env.example) for a complete sample.\n\n### Required\n\n| Variable | Description |\n| --- | --- |\n| `FHIR_BASE_URL` | Base URL used to derive the FHIR server URL and token URL. Optional when `FHIR_SERVER_URL` is set (and, for smart auth, `FHIR_TOKEN_URL`) |\n| `FHIR_CLIENT_ID` | SMART Backend Services client ID (not needed when `FHIR_AUTH=none`) |\n| `FHIR_ACTIVE_KEY` | Base64-encoded PKCS#8 PEM signing key, RSA or EC P-384 (not needed when `FHIR_AUTH=none`) |\n\n### Optional\n\n| Variable | Default | Description |\n| --- | --- | --- |\n| `FHIR_AUTH` | `smart` | `smart` (SMART Backend Services) or `none` (unauthenticated, for public test endpoints) |\n| `FHIR_RETIRED_KEYS` | unset | Comma-separated base64-encoded PEMs for JWKS rotation |\n| `FHIR_VERSION` | `R4` | Active R4+ FHIR release; controls derived URL, FHIRPath model, and compact model metadata |\n| `FHIR_SERVER_URL` | `<base>/api/FHIR/<FHIR_VERSION>` | Explicit FHIR API URL override |\n| `FHIR_TOKEN_URL` | `<base>/oauth2/token` | Explicit token endpoint override |\n| `FHIR_JWKS_URL` | unset | External JWKS URL. Omit in HTTP mode to enable built-in `/jwks` |\n| `MCP_TRANSPORT` | `http` | `http` or `stdio` |\n| `PORT` | `5000` | HTTP listener port |\n| `BIND_HOST` | `0.0.0.0` (or `127.0.0.1` with `--dev` flag) | HTTP bind address |\n| `ALLOWED_HOSTS` | unset | Comma-separated hostnames for DNS rebinding protection |\n| `FHIR_METADATA_MODE` | `strict` | `strict`, `warn`, or `off` for `/metadata` validation |\n| `FHIR_DEFAULT_COUNT` | `0` | Default `_count` injected into searches when allowed; 0 = server decides |\n| `FHIR_MAX_COUNT` | `0` | Cap on explicit caller `_count` values; 0 = no cap |\n| `FHIR_MAX_RESPONSE_BYTES` | `262144` | Byte limit for model-facing JSON responses; oversized Bundles are chunked |\n| `FHIR_MAX_ARTIFACT_MB` | `16` | Separate byte ceiling (MiB) for native/binary artifact bodies; independent of the JSON limit (base64 transport ≈ +33%) |\n| `FHIR_REQUEST_TIMEOUT_MS` | `30000` | Per-attempt timeout for outgoing FHIR requests |\n| `MCP_JSON_LIMIT` | `4mb` | Max accepted MCP request body size (Express json limit string); raise if large write/bundle payloads are rejected |\n| `MCP_AUTHZ` | `none` | Authorization provider: `none` or `entra`. Gates tools per caller (HTTP + `Authorization: Bearer` only) |\n| `MCP_ROLE_PREFIX` | `FhirHydrant` | Prefix on granted role values (e.g. `FhirHydrant.Patient.Read`) |\n| `MCP_ENTRA_TENANT_ID` | unset | Entra tenant GUID (not a domain alias); required when `MCP_AUTHZ=entra` |\n| `MCP_ENTRA_AUDIENCE` | unset | API application (client) ID expected in the v2 access token `aud`; required when `MCP_AUTHZ=entra` |\n| `FHIR_RESPONSE_MODE` | unset | `compact`, `full`, or `compact-locked`; unset means search defaults compact and direct reads default full |\n| `FHIR_WRITE_CAPABILITIES` | unset | Comma-separated write actions: `create`, `update`, `patch`, `delete` |\n| `FHIR_VALIDATE_WRITES` | `local` | `off`, `local` (client-side structural checks), or `server` (local + server `$validate` preflight for create/update) |\n| `FHIR_WRITE_DRY_RUN` | `false` | Set to `true` to validate and log writes without executing them against the FHIR server |\n| `FHIR_BUNDLE_CAPABILITIES` | unset | Comma-separated Bundle types: `batch`, `transaction`; enables `bundle` tool |\n| `FHIR_BUNDLE_WRITES_ENABLED` | `false` | Set to `true` to allow write entries inside Bundles (also requires `FHIR_WRITE_CAPABILITIES`) |\n| `FHIR_OPERATIONS` | unset | Comma-separated operation keys; `none` disables all catalog operations. Default catalog: `everything`, `lastn`, `validate`, `docref`, `expand`, `lookup`, `translate`, `summary`, `match` |\n| `FHIR_TERMINOLOGY_BASE_URL` | unset | Enables terminology tools, e.g. `https://tx.fhir.org/r4` |\n| `FHIR_PAGINATION_PATHS` | unset | Extra allowed path prefixes for pagination links, e.g. `FHIRProxy` |\n| `FHIR_PREFETCH_MAX_PAGES` | `5` | Max upstream pages fetched per coalesced compact search |\n| `FHIR_PREFETCH_MAX_ENTRIES` | `5000` | Max upstream entries accumulated before stopping |\n| `FHIR_PREFETCH_MAX_BYTES` | `2097152` | Max raw bytes fetched before stopping |\n| `FHIR_PREFETCH_TIMEOUT_MS` | `25000` | Wall-clock budget for the coalescing loop |\n| `FHIR_AUDIT_SINK` | unset | Any combination of `console`, `file`, `http` |\n| `FHIR_AUDIT_FILE` | `./audit.jsonl` | JSONL file used when the `file` audit sink is enabled |\n| `FHIR_AUDIT_HTTP_URL` | unset | Destination URL for the `http` audit sink; required when `http` is enabled |\n| `FHIR_AUDIT_HTTP_FORMAT` | `raw` | `raw` (internal AuditEvent JSON) or `fhir-auditevent` (FHIR R4 AuditEvent) |\n| `FHIR_AUDIT_HTTP_AUTH` | unset | Authorization header value sent verbatim by the `http` sink |\n| `FHIR_AUDIT_USER_HEADER` | unset | Proxy-authenticated user header copied into audit events |\n| `LOG_LEVEL` | `info` | Log verbosity: `error`, `warn`, `info`, or `debug` |\n\nExplicit `FHIR_SERVER_URL` and `FHIR_TOKEN_URL` values always win over derived\nURLs.\n\n## FHIR Version Support\n\nSet `FHIR_VERSION` to select the active R4+ FHIR release. It controls the\nderived FHIR API URL, FHIRPath model context, and compact response model\nmetadata. Some releases may use the nearest compatible FHIRPath model. For\nterminology, use an endpoint that matches the selected FHIR release. Startup\nlogs hint when explicit FHIR or terminology URLs appear to reference a\ndifferent version.\n\n## Customizing Tools And Messages\n\nEverything under `config/` is customizable without source changes.\n\nConfig is resolved as a **partial overlay**: for each file, a `./config/<file>`\nin the current working directory (if present) overrides the packaged default,\nand anything you omit falls back to the built-in default. So npm installs work\nout of the box, and to customize you drop a `./config` folder next to where you\nlaunch the server containing **only** the files you want to change.\n\nThere are two overlay granularities:\n\n- **Whole-file** (`resources/*.json`, `operations.json`, `search-controls.json`,\n  `core-tools.json`, `instructions/*`): a file you provide replaces the packaged\n  file entirely. A new resource file (e.g. `./config/resources/myresource.json`)\n  adds a tool. The overlay can override and add, but **cannot remove** a packaged\n  resource — to ship a strictly minimal catalog, remove the packaged\n  `config/resources/` files (see the compose example).\n- **Per-key** (`messages/*.json`): a\n  local file overrides only the individual keys it contains; every other key\n  falls back to the packaged default. So you can retune a single description or\n  message without copying the whole file. Unknown keys, empty values, and\n  malformed JSON **fail fast at startup** to catch typos.\n\n`messages/*.json` files are read once at process startup. Changing them requires\na server restart (and, for tool schemas or instructions, a client reconnect) to\ntake effect. Development hot reload for resources, search controls, and\noperations is described below.\n\n| File | Purpose |\n| --- | --- |\n| `resources/*.json` | FHIR resource tools (one file per resource): search params, direct-read behavior, and `requireOneOf` rules |\n| `operations.json` | Named operation catalog for `operate` (per-operation descriptions and notes) |\n| `search-controls.json` | Descriptions for `_count`, `_sort`, `_summary`, `_elements`, `_include`, `_revinclude`, `_lastUpdated`, `fhirpath`, `responseMode`, `maxResults`, and `prefetch` |\n| `messages/output-schema.json` | Descriptions for every tool `outputSchema` field (per-key overlay) |\n| `messages/input-schema.json` | Descriptions for generated resource input params (`_id`, `_vid`, `_since`, `_at`, `action`, `body`) and the `operate` tool's title and params (per-key overlay) |\n| `instructions/manifest.json` | Ordered list of instruction fragments to compose, each with an optional `when` gate (`terminology`, `writes`, `operations`, `bundle`). Custom builds reorder, add, or remove sections by editing this file. |\n| `instructions/*.md` | Instruction fragments referenced by the manifest. Gated sections are included only when their feature is enabled; the `{{OPERATIONS_LIST}}` token is replaced with the live operation catalog. |\n| `messages/*.json` | User-facing messages, errors, and response notes (per-key overlay, split by domain: core, write, operations, terminology, bundle, artifact) |\n| `core-tools.json` | Built-in tool descriptions and param hints |\n\n### Resource Definition Schema\n\nEach file in `config/resources/` is a single resource definition object. Files\nare scanned in filename order; the filename is conventionally the lowercase\nresource name (e.g. `patient.json`). Each object has these fields:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `resource` | `string` | FHIR resource type |\n| `toolName` | `string` | MCP tool name; must be unique |\n| `description` | `string` | Tool description |\n| `supportsDirectRead` | `boolean` | Enables `GET /ResourceType/{id}` via `_id` |\n| `searchParams` | `Record<string,string>` | FHIR search params and descriptions |\n| `requireOneOf` | `(string \\| string[])[]` | Search requires at least one option. A string is a single required param; a nested array is a param set where every param is required. `[\"patient\"]` accepts `patient`; `[[\"given\",\"family\"],[\"identifier\"]]` accepts `given`+`family` together, or `identifier` |\n\n`searchParams` values are descriptions, not a full FHIR capability model.\nServer-specific search behavior can still apply.\n\n### Hot Reload\n\nIn development (`NODE_ENV` is not `production`), the `config/resources/` folder,\n`search-controls.json`, and `operations.json` are watched. Invalid JSON keeps\nthe last valid snapshot. A materially changed reload is applied transactionally:\nwhen the derived SMART scopes change, a replacement token is acquired before the\nnew definitions and tool registrations are committed, so a failed acquisition\nleaves the running catalog untouched. Adding/removing tools, operation and\nparam-name schema changes are re-registered live — no restart needed. Semantically\nunchanged saves cause no refresh. Production reads config once at startup, but a\nruntime `/metadata` change (via `capabilities(refresh=true)`) or a backend\nSMART-scope change on token refresh re-evaluates the available tools in every mode.\n\nOne boundary is unavoidable: the tool list and schemas hot-refresh, but the server\n`instructions` are sent once during MCP `initialize` and cannot be replaced on an\nexisting connection. A client must reconnect/reinitialize to receive changed\ninstruction text.\n\n## Transports\n\n### Stdio\n\nSet `MCP_TRANSPORT=stdio`. stdout is reserved for the MCP protocol; logs are\nredirected to stderr. Use an external `FHIR_JWKS_URL` for stdio deployments.\n\n### Streamable HTTP\n\nHTTP transport is stateless and exposes MCP at:\n\n```http\nPOST http://localhost:5000/mcp\nAccept: application/json, text/event-stream\nContent-Type: application/json\n```\n\nMCP client config:\n\n```json\n{\n   \"mcpServers\": {\n      \"fhirhydrant\": {\n         \"url\": \"http://localhost:5000/mcp\"\n      }\n   }\n}\n```\n\n`GET /health` returns a no-PHI readiness snapshot:\n\n```json\n{\n   \"status\": \"ok\",\n   \"mcp\": true,\n   \"metadata\": true,\n   \"tools\": 23,\n   \"auth\": true,\n   \"tokenExpiresIn\": 287\n}\n```\n\nWhen authorization is enabled, `authz` reports the active provider and `tools`\nis omitted because the registered tool count is caller-specific.\n\nUse a reverse proxy for TLS and user authentication when exposing HTTP beyond\nlocalhost. Set `ALLOWED_HOSTS` when binding to a public interface.\n\n## Per-caller authorization (Entra, optional)\n\nBy default (`MCP_AUTHZ=none`) every caller sees the full tool set gated only by\n`/metadata` and the backend SMART scopes. Setting `MCP_AUTHZ=entra` adds an\noptional per-caller layer: each `/mcp` request must carry an\n`Authorization: Bearer <token>` issued by Microsoft Entra, and the caller's\n**App Roles** determine which tools are built for that request. This is\nMCP-layer authorization only — it never replaces the FHIR server's own\nauthorization, and it can only *subtract* from what the backend SMART token and\nconfig already allow.\n\nThe API app registration must set `requestedAccessTokenVersion` to `2` in its\nmanifest. The provider validates tenant-specific v2 issuers and expects\n`MCP_ENTRA_AUDIENCE` to be the API application's client ID.\n\nTools a caller lacks a role for are not registered at all — they are absent from\n`tools/list`, not merely blocked. Helper tools (`capabilities`, `paginate`,\n`terminology_lookup`, `code_search`) are never gated.\n\nApp Role values (with the default `FhirHydrant` prefix):\n\n| Role | Grants |\n| --- | --- |\n| `FhirHydrant.<Resource>.Read` | search, read, vread, history for that resource |\n| `FhirHydrant.<Resource>.Write` | read actions plus create, update, patch, delete (subject to `FHIR_WRITE_CAPABILITIES`) |\n| `FhirHydrant.Operation.<key>` | the named operation via the `operate` tool (e.g. `FhirHydrant.Operation.everything`) |\n| `FhirHydrant.Bundle` | the `bundle` tool |\n| `FhirHydrant.SystemHistory.Read` | the system-wide `system_history` tool |\n| `FhirHydrant.Admin` | all of the above, still bounded by backend SMART scopes, `/metadata`, and write/bundle/operation config |\n\nRequires HTTP transport; `MCP_AUTHZ=entra` with `MCP_TRANSPORT=stdio` fails at\nstartup. Missing or invalid bearer tokens receive `401`.\n\n### Adding an authorization provider\n\nEntra is the only shipped provider, but the authorization layer is\nprovider-neutral. This is a **source extension**, not a runtime plugin: the npm\npackage ships only `bin/server.js` (providers are bundled in), so adding one\nmeans forking or cloning the repo and rebuilding.\n\nThe shared pipeline is provider-agnostic — a provider only maps an\n`Authorization` header to `{ subject, roles }`. The role vocabulary\n(`.Read`/`.Write`/`Operation.<key>`/`Bundle`/`SystemHistory.Read`/`Admin`) and\n`MCP_ROLE_PREFIX` handling are applied by `decideAuthz` for every provider.\n\nTo add one (e.g. `auth0`) takes just two edits:\n\n1. Create `ts/mcp/authz/auth0.ts` exporting an `AuthzProvider` — implement\n   `validate(authorization)` to return `{ subject, roles }` (throw to reject),\n   and optionally `validateConfig()` to fail fast on missing provider env. Keep\n   all provider-specific env inside this module; do not add fields to `Config`.\n2. Add one entry to `ts/mcp/authz/registry.ts`:\n   `auth0: () => import(\"./auth0.ts\").then((m) => m.auth0Provider)`.\n\nThat's it. The `AuthzMode` type, the `MCP_AUTHZ` parser, and its error message\nall derive from the registry keys automatically, so `MCP_AUTHZ=auth0` just works\nwith full type safety — no other file needs to change.\n\n## Deployment Examples\n\nThe [`examples/`](examples/) directory has standalone deployment examples for\nDocker Compose, reverse proxy (Caddy), Azure Container Apps, Azure App Service,\nand Kubernetes. Each includes a Dockerfile that installs from npm and a\n`config/` overlay demonstrating how to override different config files.\n\n## Development\n\n```sh\n# dev server\nnpm run dev\n\n# type-check\nnpm run check\n\n# build and run\nnpm run build\nnpm start\n```\n\nBuild output goes to `bin/server.js`.\n",
  "bytes": 32841,
  "sha": "84b1e0247b17135e1f377a4d45373bec7f125fe6da571fdef0057338575f1d43",
  "repo_slug": "faulkj/fhirhydrant",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_faulkj_fhirhydrant_81db40e5/readme"
}