Skip to main content
Glama
mrosata

MCP FHIR Server

by mrosata
README.md
# MCP FHIR Server

A generic MCP server providing read/write access to any FHIR-compliant API with built-in validation.

This server works with **any FHIR server**, not just Zus Health. For Zus-specific features (like getting UPIDs), see the Zus Extensions section below.


## Features

### Core FHIR Features
- **FHIR resource validation** using consolidated FHIR schemas
- **Create and update** resources (POST/PUT)
- **Read** resources by type and ID
- **Search** resources with query parameters
- **Granular permissions** via environment configuration
- **Bearer token authentication**
- **Custom HTTP headers** for multi-tenant or vendor-specific requirements
- **Detailed error messages** for debugging and LLM-based correction

### Zus Health Extensions (Optional)
- **Zus UPID lookup** - Get Universal Patient IDs from Zus FHIR servers
- **Intelligent name matching** - Find best patient match when multiple results exist
- **Builder ID support** - Multi-tenant access via `Zus-Account` header

## Installation

### Prerequisites

- Python 3.13+
- [uv](https://docs.astral.sh/uv/) (recommended) or pip

### Setup

```bash
# Clone the repository
git clone <repository-url>
cd mcp-fhir

# Install dependencies (production only)
uv sync

# For development (includes test tools, linter, etc.)
uv sync --extra dev

# Or with pip
pip install -e .
```

**Note:** The `make` commands will automatically install development dependencies when needed, so you can also just run `make test` directly after cloning.

## Configuration

### Environment File

The server can load environment variables from a file using the `--env-file` command line flag:

```bash
# Load environment variables from a specific file
uv run fastmcp run server.py --env-file /path/to/your/.env

# Or for development
uv run fastmcp dev server.py --env-file /path/to/your/.env
```

If no `--env-file` flag is provided, the server will use system environment variables only.

Create a `.env` file:

```bash
cp .env.example .env
```

### Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `FHIR_BASE_URL` | `http://localhost:8080/fhir` | FHIR server base URL |
| `FHIR_ALLOW_READ` | `true` | Enable GET operations |
| `FHIR_ALLOW_WRITE` | `true` | Enable POST/PUT/PATCH/DELETE operations |
| `FHIR_AUTH_TOKEN` | _(empty)_ | Bearer token for authentication |
| `FHIR_ALLOWED_METHODS` | _(empty)_ | Comma-separated HTTP methods (overrides READ/WRITE) |

### Permission Model

**Option 1: Simple Read/Write** (default)
```bash
FHIR_ALLOW_READ=true   # Enables GET
FHIR_ALLOW_WRITE=true  # Enables POST, PUT, PATCH, DELETE
```

**Option 2: Granular Methods** (takes precedence)
```bash
FHIR_ALLOWED_METHODS=GET,POST  # Only read and create
```

Examples:
- `GET` - Read-only
- `POST,PUT` - Create and update only (no reads)
- `GET,POST` - Read and create (no updates)
- `GET,POST,PUT` - Full access

## Running

### Development
```bash
# Using make (recommended)
make dev

# Or directly
uv run fastmcp dev server.py
```

### Production
```bash
# Using make (recommended)
make run

# Or directly
uv run fastmcp run server.py
```

## Claude Desktop Integration

Edit your Claude Desktop config file:

**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
**Linux**: `~/.config/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "fhir": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/mcp-fhir",
        "run",
        "fastmcp",
        "run",
        "server.py",
        "--env-file",
        "/absolute/path/to/mcp-fhir/.env"
      ]
    }
  }
}
```

**Alternative:** You can also set environment variables directly in the config:

```json
{
  "mcpServers": {
    "fhir": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/mcp-fhir",
        "run",
        "fastmcp",
        "run",
        "server.py"
      ],
      "env": {
        "FHIR_BASE_URL": "https://your-fhir-server.com/fhir",
        "FHIR_ALLOW_READ": "true",
        "FHIR_ALLOW_WRITE": "true",
        "FHIR_AUTH_TOKEN": "your-token-here"
      }
    }
  }
}
```

Restart Claude Desktop after editing.

## Tools

### Core FHIR Tools

These tools work with any FHIR-compliant server:

#### `write_fhir_resource`

Create or update a FHIR resource.

**Parameters:**
- `resource` (object): FHIR resource JSON
- `custom_headers` (object, optional): Custom HTTP headers for the request
  - For Zus servers: `{"Zus-Account": "builder-id"}` for multi-tenant access
  - For other servers: Any vendor-specific headers your FHIR server requires

**Behavior:**
1. Validates resource against FHIR schema
2. Uses POST if no `id` field (create), PUT if `id` exists (update)
3. Returns validation errors for correction if invalid
4. Returns server response on success (if `FHIR_ALLOW_READ=true`)

**Example:**
```json
{
  "resourceType": "Patient",
  "name": [{"family": "Smith", "given": ["John"]}],
  "gender": "male"
}
```

**Note:** If validation schema fails to load, validation is skipped (server-side validation still applies).

---

#### `read_fhir_resource`

Read a resource by type and ID.

**Parameters:**
- `resource_type` (string): e.g., "Patient", "Observation"
- `resource_id` (string): Resource ID
- `custom_headers` (object, optional): Custom HTTP headers for the request

**Returns:** JSON resource or error message

---

#### `search_fhir_resources`

Search resources with query parameters.

**Parameters:**
- `resource_type` (string): Resource type to search
- `search_params` (object, optional): Query parameters
  - Example: `{"name": "Smith", "gender": "female"}`
- `custom_headers` (object, optional): Custom HTTP headers for the request

**Returns:** FHIR Bundle with matching resources

---

#### `get_fhir_config`

View current configuration.

**Returns:** Configuration summary including base URL, permissions, and allowed methods.

---

### Zus Health Extensions

**These tools are specific to Zus Health FHIR servers and will not work with other FHIR implementations.**

#### `get_patient_zus_upid`

Get the Zus UPID (Universal Patient ID) for a Patient resource from Zus FHIR server.

**Parameters:**
- `first_name` (string): Patient's first name
- `last_name` (string): Patient's last name
- `builder_id` (string, optional): Zus builder ID to filter the search

**Behavior:**
1. Searches for Patient resources using `name` parameter (concatenated first and last name)
2. Optionally filters by Zus `builderID` parameter if provided
3. Extracts Zus UPID from Patient's identifiers with system `https://zusapi.com/fhir/identifier/universal-id`
4. When multiple patients are found, uses intelligent name matching to find the best match
5. Returns the Zus UPID value or appropriate error message

**Example usage:**
```
get_patient_zus_upid("John", "Smith")
get_patient_zus_upid("John", "Smith", "builder-123")
```

**Response formats:**
- Single patient found: `Zus UPID: zus-upid-12345`
- Multiple patients with good name match: `Zus UPID: zus-upid-12345 (Best match: John Smith)` + other matches if any
- Multiple patients with no clear match: Lists all found patients with their Zus UPIDs
- No patients found: `Error: No Patient found with name 'John Smith'`
- No Zus UPID: `Error: No Zus UPID found for Patient(s) with name 'John Smith'`

**Name Matching Logic:**
- Exact name matches get highest priority (score 1.0)
- Partial matches (e.g., "John" matching "Johnny") get medium priority (score 0.7 for given name)
- Family name matches are weighted more heavily than given name matches
- Partial matches are permissive: shorter names can match longer ones (e.g., "John" matches "Johnny")
- If the best match has a score ≥ 0.5, it's returned as the primary result
- Other decent matches (score ≥ 0.3) are listed as alternatives

## Technical Details

### HTTP Headers

All requests include:
```
Content-Type: application/fhir+json
Accept: application/fhir+json
Authorization: Bearer {token}  (if FHIR_AUTH_TOKEN set)
```

**Custom Headers:**
You can provide additional custom headers via the `custom_headers` parameter in any tool. This is useful for:
- Multi-tenant systems (e.g., Zus's `Zus-Account` header)
- Vendor-specific authentication or routing headers
- Any other FHIR server-specific requirements

**Example (Zus):**
```json
{"Zus-Account": "builder-123"}
```

### Timeouts

All requests timeout after 30 seconds.

### Error Handling

The server returns detailed errors for:

| Code | Description |
|------|-------------|
| 400 | Invalid request/validation error |
| 401 | Authentication failed |
| 403 | Insufficient permissions |
| 404 | Resource or endpoint not found |
| 422 | Business rule violation |
| Timeout | Connection timeout (30s) |

Errors include full server response when available for debugging.

### Validation

Resources are validated using the `fhir-validator` library before submission:
- Checks FHIR spec compliance
- Validates required fields and data types
- Verifies resource structure

If validation schema loading fails at startup, a warning is logged and validation is bypassed (server-side validation still occurs).

## Development

### Testing

```bash
# Run tests (automatically installs dev dependencies if needed)
make test

# With coverage
make test-cov

# Watch mode
make test-watch
```

Or directly:
```bash
uv run pytest
uv run pytest --cov=. --cov-report=term-missing
```

**Note:** All `make` commands automatically install development dependencies when needed, so new developers can simply run `make test` after cloning the repository.

### Code Quality

```bash
make lint    # Run linter (automatically installs dev dependencies)
make format  # Format code (automatically installs dev dependencies)
make check   # Lint + format check (automatically installs dev dependencies)
```

### Project Structure

```
mcp-fhir/
├── server.py          # Generic MCP FHIR server implementation
├── zus_extensions.py  # Zus Health-specific tools (optional)
├── fhir_validator.py  # FHIR validation logic
├── pyproject.toml     # Dependencies
├── .env.example       # Example configuration
└── tests/             # Test suite
```

### Architecture

The server is designed with modularity in mind:

- **server.py**: Contains generic FHIR operations that work with any FHIR server
- **zus_extensions.py**: Contains Zus Health-specific functionality (UPID lookup, etc.)
- **Generic tools** accept `custom_headers` for flexibility with different FHIR vendors
- **Zus tools** use `builder_id` for Zus-specific multi-tenancy

This separation allows you to:
1. Use the generic tools with any FHIR server
2. Add your own vendor-specific extensions by following the zus_extensions.py pattern
3. Keep the core FHIR functionality clean and standards-compliant

## License

[Add license information]

## Contributing

[Add contribution guidelines]

TDQS

A3.5/5.0

Scored across 5 tools

Disambiguation4/5

Most tools have distinct purposes, but get_fhir_config overlaps slightly with general FHIR operations as it's a configuration tool rather than a core FHIR resource operation. The other tools (read, search, write, get_patient_zus_upid) are clearly differentiated by their specific actions and targets.

Naming Consistency4/5

Tools follow a consistent verb_noun pattern (get_fhir_config, read_fhir_resource, search_fhir_resources, write_fhir_resource), with get_patient_zus_upid being a minor deviation due to its Zus-specific naming. Overall, the naming is predictable and readable.

Tool Count4/5

Five tools is reasonable for a FHIR server, covering core operations like read, search, and write, plus configuration and a Zus-specific utility. It's slightly thin for full FHIR coverage but well-scoped for basic interactions.

Completeness3/5

The toolset covers read, search, and write operations, but lacks update and delete for full CRUD lifecycle management. The inclusion of get_fhir_config and a Zus-specific tool adds utility, but the absence of update/delete operations is a notable gap for FHIR resource management.

Maintenance

ActivityInactive
ResponsivenessNo issues