mcp-fhir
io.github.pcmedsinge/mcp-fhir
Documentation
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)
