mcp-fhir
FHIR R4 MCP server — read, search, paginate, validate resources against US Core/IPS profiles, and authenticate against Epic/Cerner EHR sandboxes via SMART-on-FHIR OAuth 2.0.
Part of the fhir-mcp-suite monorepo.
What it does
mcp-fhir exposes five MCP tools that let any MCP-compatible AI (Claude, GPT-4o, etc.)
interact with any FHIR R4 server — and validate resources against US Core and IPS profiles —
in a single server process. No other public FHIR MCP server combines search + validation today.
| Tool | Description |
|---|---|
fhir_capabilities |
CapabilityStatement summary — what the server supports |
fhir_read |
Read a single resource by type + logical ID |
fhir_search |
Search a resource type with FHIR query params; returns Bundle with _next_url when paginated |
fhir_search_next |
Follow a _next_url pagination link (SSRF-guarded) |
validate_against_profile |
Validate a resource via the HAPI validator; supports US Core & IPS aliases |
Architecture
Claude Desktop / AI client
│ MCP (stdio or SSE)
▼
┌───────────────────────────────┐
│ mcp-fhir v1.1 │
│ ┌───────────────────────────┐│
│ │ tools/ ││
│ │ ├─ fhir_capabilities.py ││
│ │ ├─ fhir_read.py ││
│ │ ├─ fhir_search.py ││ ──► FHIR R4 server
│ │ └─ validate_profile.py ││ ──► HAPI validator sidecar
│ └───────────────────────────┘│
│ fhir-mcp-shared │
│ ├─ LangFuse traces (session) │
│ ├─ Pydantic structured output │
│ └─ structlog JSON logging │
└───────────────────────────────┘
Quick start
# requires Python 3.12+ and uv
uvx mcp-fhir # stdio transport (Claude Desktop, default)
# SSE transport (HTTP, for API access)
MCP_TRANSPORT=sse uvx mcp-fhir
Claude Desktop config (claude_desktop_config.json)
{
"mcpServers": {
"mcp-fhir": {
"command": "uvx",
"args": ["mcp-fhir"],
"env": {
"FHIR_BASE_URL": "https://hapi.fhir.org/baseR4",
"HAPI_VALIDATOR_URL": "http://localhost:8082"
}
}
}
}
See full installation guide for HAPI validator setup, troubleshooting, and self-hosted FHIR server configuration.
Eval results
Golden query suite — run against the public HAPI demo server (hapi.fhir.org/baseR4):
| Category | Cases | Pass rate |
|---|---|---|
fhir_capabilities |
2 | 100 % |
fhir_read |
4 | 100 % |
fhir_search |
6 | 100 % |
fhir_search_next |
2 | 100 % |
validate_against_profile |
6 | 100 % |
| Total | 20 | 100 % |
CI gate:
uv run python evals/mcp-fhir/run_eval.py --ci --threshold 0.9
Validator cases require the HAPI sidecar (docker compose up hapi-validator).
Configuration
All settings via environment variables (see .env.example):
| Variable | Default | Description |
|---|---|---|
FHIR_BASE_URL |
https://hapi.fhir.org/baseR4 |
FHIR server base URL |
HAPI_VALIDATOR_URL |
http://localhost:8082 |
HAPI validator sidecar (runs on port 3500 internally; map 8082:3500) |
FHIR_TIMEOUT_S |
30 |
HTTP timeout in seconds |
FHIR_MAX_RESULTS |
20 |
Default _count for searches |
MCP_TRANSPORT |
stdio |
stdio or sse |
MCP_PORT |
8000 |
Port for SSE transport |
LOG_FORMAT |
json |
json (prod) or console (dev) |
LANGFUSE_PUBLIC_KEY |
(unset) | LangFuse observability (optional) |
LANGFUSE_SECRET_KEY |
(unset) | LangFuse observability (optional) |
SMART-on-FHIR authentication
mcp-fhir v1.1.0 adds SMART-on-FHIR backend services (client_credentials grant, RFC 6749)
so the server can authenticate against real Epic / Cerner sandboxes — no browser redirect needed.
When SMART_ENABLED=true, every FHIR request gets an Authorization: Bearer <token> header.
Tokens are cached in-process and refreshed automatically 30 s before expiry.
SMART environment variables
| Variable | Default | Description |
|---|---|---|
SMART_ENABLED |
false |
Set true to activate SMART auth |
SMART_CLIENT_ID |
"" |
App client ID from EHR registration portal |
SMART_CLIENT_SECRET |
"" |
Client secret (stored as SecretStr, never logged) |
SMART_TOKEN_URL |
"" |
Token endpoint URL; auto-discovered if blank |
SMART_SCOPES |
system/*.read |
Space-separated OAuth 2.0 scopes |
SMART_GRANT_TYPE |
client_credentials |
OAuth grant type (only client_credentials supported) |
SMART_TOKEN_TIMEOUT_S |
15.0 |
HTTP timeout for token requests |
When SMART_TOKEN_URL is blank the server performs SMART auto-discovery:
GET {FHIR_BASE_URL}/.well-known/smart-configuration → token_endpoint.
Falls back to {FHIR_BASE_URL}/oauth2/token if discovery returns a non-200.
Epic sandbox quick-start
- Register at https://fhir.epic.com/ → My Apps → create a backend-services app.
- Note your Client ID and Client Secret.
- Create
.envin the monorepo root (already in.gitignore):
FHIR_BASE_URL=https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4
SMART_ENABLED=true
SMART_CLIENT_ID=your-epic-client-id
SMART_CLIENT_SECRET=your-epic-client-secret
# SMART_TOKEN_URL is optional — auto-discovered from FHIR_BASE_URL
Cerner sandbox quick-start
- Register at https://code.cerner.com/ → create a backend-services app.
- Note your Client ID and the token endpoint URL.
- Update
.env:
FHIR_BASE_URL=https://fhir-ehr-code.cerner.com/r4/your-tenant-id
SMART_ENABLED=true
SMART_CLIENT_ID=your-cerner-client-id
SMART_CLIENT_SECRET=your-cerner-client-secret
SMART_TOKEN_URL=https://authorization.cerner.com/tenants/your-tenant-id/protocols/oauth2/profiles/smart-v1/token
Claude Desktop config with SMART auth
{
"mcpServers": {
"mcp-fhir": {
"command": "uvx",
"args": ["mcp-fhir"],
"env": {
"FHIR_BASE_URL": "https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4",
"SMART_ENABLED": "true",
"SMART_CLIENT_ID": "your-epic-client-id",
"SMART_CLIENT_SECRET": "your-epic-client-secret"
}
}
}
}
Programmatic client (LangGraph / any Python agent)
Use StdioServerParameters to point any Python MCP client at a FHIR server.
The same pattern works in LangGraph tool nodes, LangChain agents, or plain asyncio scripts.
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import asyncio, os
server = StdioServerParameters(
command="uv",
args=["run", "--package", "mcp-fhir", "mcp-fhir"],
env={
**os.environ,
# ── Public HAPI (no auth) ──────────────────────────────────
"FHIR_BASE_URL": "https://hapi.fhir.org/baseR4",
"SMART_ENABLED": "false",
# ── Epic sandbox (uncomment + fill in) ────────────────────
# "FHIR_BASE_URL": "https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4",
# "SMART_ENABLED": "true",
# "SMART_CLIENT_ID": os.environ["EPIC_CLIENT_ID"],
# "SMART_CLIENT_SECRET": os.environ["EPIC_CLIENT_SECRET"],
# ── Cerner sandbox (uncomment + fill in) ──────────────────
# "FHIR_BASE_URL": "https://fhir-myrecord.cerner.com/r4/<tenant-id>",
# "SMART_ENABLED": "true",
# "SMART_CLIENT_ID": os.environ["CERNER_CLIENT_ID"],
# "SMART_CLIENT_SECRET": os.environ["CERNER_CLIENT_SECRET"],
# "SMART_TOKEN_URL": "https://authorization.cerner.com/tenants/<tenant-id>/...",
},
)
async def main():
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool(
"fhir_search",
arguments={"resource_type": "Patient", "params": {"_count": "3"}},
)
print(result.content[0].text)
asyncio.run(main())
See demo/fhir_server_configs.py for ready-to-run
configs for all supported servers (public HAPI, SMART Health IT, Epic, Cerner, local Docker).
Running SMART integration tests
# Requires real sandbox credentials in .env
uv run pytest packages/mcp-fhir/tests -m smart_integration -v
Profile validation
The validate_against_profile tool supports shorthand aliases:
| Alias | Profile URL |
|---|---|
us-core-patient |
http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient |
us-core-observation |
http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-clinical-result |
us-core-condition |
http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition-problems-health-concerns |
us-core-medication-request |
http://hl7.org/fhir/us/core/StructureDefinition/us-core-medicationrequest |
us-core-encounter |
http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter |
ips-patient |
http://hl7.org/fhir/uv/ips/StructureDefinition/Patient-uv-ips |
Profile validation requires the HAPI validator sidecar (included in
docker-compose.yml at repo root).
Development
# From monorepo root
uv sync
uv run pytest packages/mcp-fhir/tests -v
# Integration tests (requires HAPI public server access)
uv run pytest packages/mcp-fhir/tests -m integration
Architecture
Client (Claude / GPT-4o)
│ MCP protocol (stdio or SSE)
▼
mcp-fhir server (Python, anyio)
├── fhir_read ──────────► FHIR R4 server (configurable)
├── fhir_search ─────────► FHIR R4 server
└── validate_against_profile
│
▼
HAPI validator sidecar (markiantorno/validator-wrapper)
│
▼
LangFuse (optional observability)
License
Metadata
Release files for mcp-fhir 1.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mcp_fhir-1.1.1.tar.gz | 21.3 kB | With provenance | Details for mcp_fhir-1.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mcp_fhir-1.1.1-py3-none-any.whl | Python 3 | none | any | Details for mcp_fhir-1.1.1-py3-none-any.whl |
Total release size: 43.4 kB
Release files / mcp_fhir-1.1.1.tar.gz
| Download URL | mcp_fhir-1.1.1.tar.gz |
|---|---|
| Size | 21.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b29fe58083ad553d6c35b03714bfe7190fe4278de3d1fc8da0749436ddc31ea9
|
|
BLAKE2b-256 checksum How to use checksums |
291aa7feb2aa7121ef2cb55bdef39faee008964c70aa6f4d02e8c60023770c37
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 20, 2026.
Transparency logRelease files / mcp_fhir-1.1.1-py3-none-any.whl
| Download URL | mcp_fhir-1.1.1-py3-none-any.whl |
|---|---|
| Size | 22.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4e66b264186a95e37d0394fd66ade1635221fb13ebc704055779874f0488f5ac
|
|
BLAKE2b-256 checksum How to use checksums |
a6a9ef2abcc329b3c814bfdbc4a36d05388ffcfe46481e8694927a30cafc4d87
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 20, 2026.
Transparency logSecurity for mcp-fhir 1.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for mcp_fhir-1.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 20, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.