Metadata-Version: 2.4
Name: mcp-fhir
Version: 1.1.1
Summary: FHIR R4 MCP server — read, search, validate resources + SMART-on-FHIR auth (Epic/Cerner sandbox)
Project-URL: Homepage, https://github.com/pcmedsinge/fhir-mcp-suite
Project-URL: Repository, https://github.com/pcmedsinge/fhir-mcp-suite
Project-URL: Bug Tracker, https://github.com/pcmedsinge/fhir-mcp-suite/issues
Project-URL: Documentation, https://pcmedsinge.github.io/fhir-mcp-suite/
Project-URL: Changelog, https://github.com/pcmedsinge/fhir-mcp-suite/releases
Author-email: Parag Medsinge <pcmedsinge@gmail.com>
License: Apache-2.0
Keywords: ai,clinical-ai,fhir,healthcare,hl7,ips,llm,mcp,us-core
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Healthcare Industry
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: fhir-mcp-shared>=0.1.0
Requires-Dist: httpx>=0.27
Requires-Dist: mcp[cli]>=1.6
Requires-Dist: pydantic-settings>=2.6
Requires-Dist: pydantic>=2.9
Requires-Dist: structlog>=24.4
Description-Content-Type: text/markdown

# mcp-fhir

<!-- mcp-name: io.github.pcmedsinge/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](https://github.com/pcmedsinge/fhir-mcp-suite) monorepo.

[![PyPI](https://img.shields.io/pypi/v/mcp-fhir)](https://pypi.org/project/mcp-fhir/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mcp-fhir)](https://pypi.org/project/mcp-fhir/)
[![CI](https://github.com/pcmedsinge/fhir-mcp-suite/actions/workflows/ci.yml/badge.svg)](https://github.com/pcmedsinge/fhir-mcp-suite/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](../../LICENSE)

## 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

```bash
# 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`)

```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](https://pcmedsinge.github.io/fhir-mcp-suite/mcp-fhir/installation/)
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`](../../.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

1. Register at <https://fhir.epic.com/> → **My Apps** → create a backend-services app.
2. Note your **Client ID** and **Client Secret**.
3. Create `.env` in the monorepo root (already in `.gitignore`):

```dotenv
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

1. Register at <https://code.cerner.com/> → create a backend-services app.
2. Note your **Client ID** and the token endpoint URL.
3. Update `.env`:

```dotenv
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

```json
{
  "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.

```python
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`](../../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

```bash
# 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

```bash
# 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

[Apache-2.0](../../LICENSE)
