FHIR R4 MCP server — read, search, validate resources + SMART-on-FHIR auth (Epic/Cerner sandbox)
Project description
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.0 │
│ ┌───────────────────────────┐│
│ │ 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_fhir-1.1.0.tar.gz.
File metadata
- Download URL: mcp_fhir-1.1.0.tar.gz
- Upload date:
- Size: 21.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8346523ea3fe7177e2e598d0b69702f20f90f0a692bb3762fe9fbbf2d6c4945c
|
|
| MD5 |
f4b89b772e42065f9d865c99efe68703
|
|
| BLAKE2b-256 |
389ee6c04ecb2e14db6f01bd6b46cc876822a46c8aaabd2c08ad14ddf88f6744
|
Provenance
The following attestation bundles were made for mcp_fhir-1.1.0.tar.gz:
Publisher:
release.yml on pcmedsinge/fhir-mcp-suite
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_fhir-1.1.0.tar.gz -
Subject digest:
8346523ea3fe7177e2e598d0b69702f20f90f0a692bb3762fe9fbbf2d6c4945c - Sigstore transparency entry: 1880572538
- Sigstore integration time:
-
Permalink:
pcmedsinge/fhir-mcp-suite@9856212358dcb1a682bff9e0037b9ec2286235fe -
Branch / Tag:
refs/tags/mcp-fhir-v1.1.0 - Owner: https://github.com/pcmedsinge
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9856212358dcb1a682bff9e0037b9ec2286235fe -
Trigger Event:
push
-
Statement type:
File details
Details for the file mcp_fhir-1.1.0-py3-none-any.whl.
File metadata
- Download URL: mcp_fhir-1.1.0-py3-none-any.whl
- Upload date:
- Size: 22.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1624a516e984738fee9f3dd69be355562cf8e92e337295f16ae4357aa414325e
|
|
| MD5 |
090be7001d431526f517928a0ad7936f
|
|
| BLAKE2b-256 |
ad6a71973c245d30cf0594c8bc79527d3632687192a2b4fc4da603a4f19a8581
|
Provenance
The following attestation bundles were made for mcp_fhir-1.1.0-py3-none-any.whl:
Publisher:
release.yml on pcmedsinge/fhir-mcp-suite
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mcp_fhir-1.1.0-py3-none-any.whl -
Subject digest:
1624a516e984738fee9f3dd69be355562cf8e92e337295f16ae4357aa414325e - Sigstore transparency entry: 1880572771
- Sigstore integration time:
-
Permalink:
pcmedsinge/fhir-mcp-suite@9856212358dcb1a682bff9e0037b9ec2286235fe -
Branch / Tag:
refs/tags/mcp-fhir-v1.1.0 - Owner: https://github.com/pcmedsinge
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9856212358dcb1a682bff9e0037b9ec2286235fe -
Trigger Event:
push
-
Statement type: