Skip to main content
Glama
Voysi-IA

healow-mcp-server

by Voysi-IA
README.md
# healow-mcp-server

An [MCP](https://modelcontextprotocol.io) server that wraps the
**eClinicalWorks / healow FHIR R4 API** so an MCP client (e.g. Claude Desktop)
can read patient clinical data from one or more practices.

Auth is **SMART on FHIR — Standalone Launch, confidential symmetric client**
with PKCE. Each practice has its own FHIR base URL and its own OAuth endpoints;
endpoints are discovered at runtime from
`{base}/.well-known/smart-configuration` (never hardcoded) and cached per
practice.

## Security model

- **Secrets live in the OS keychain** (macOS Keychain / Windows Credential Locker
  / Linux Secret Service) via [`keyring`](https://github.com/jaraco/keyring).
  The SQLite store at `$HEALOW_TOKEN_STORE` holds only **non-secret metadata**
  (practice name, base URL, patient id, token expiry, timestamps). Long-lived
  refresh tokens — which are standing keys to PHI — never touch the DB file.
- **Logs go only to stderr.** stdout is the MCP stdio transport. Tokens, codes,
  and secrets are never logged.
- The SQLite file is created `0600`, its directory `0700`. TLS verification is
  always on; HTTP uses a 30s timeout with exponential backoff on 429/5xx and a
  single retry on 401 after a token refresh.

> **Keychain note:** the bootstrap helper and the MCP server should both be
> launched via `uv` so macOS Keychain sees the same interpreter identity and
> does not prompt. (Verified: writing a token in one `uv run` process and
> reading it in another produces no prompt.) The definitive confirmation is the
> first keychain access under the real Claude Desktop launch
> (`uv run --directory … healow-mcp` as a subprocess of the GUI app); if macOS
> ever shows an access prompt there, approve "Always Allow" once.

## Requirements

- Python 3.11+
- [`uv`](https://docs.astral.sh/uv/)

```bash
uv sync
```

## Environment variables

| Variable                | Required | Description                                                             |
| ----------------------- | -------- | ----------------------------------------------------------------------- |
| `HEALOW_CLIENT_ID`      | yes      | OAuth client id issued by healow/eCW.                                   |
| `HEALOW_CLIENT_SECRET`  | yes      | OAuth client secret (confidential client).                              |
| `HEALOW_REDIRECT_URI`   | yes      | Registered redirect URI. A loopback URL (e.g. `http://localhost:9999/callback`) enables auto-capture during bootstrap. |
| `HEALOW_TOKEN_STORE`    | no       | SQLite metadata DB path. Default `~/.healow-mcp/tokens.db`.             |

## One-time OAuth bootstrap

Connecting a practice is a one-time, human-in-the-loop step. There are two
equivalent paths.

### A) CLI helper (recommended)

```bash
export HEALOW_CLIENT_ID=...      # or set these in your shell profile
export HEALOW_CLIENT_SECRET=...
export HEALOW_REDIRECT_URI=http://localhost:9999/callback

uv run python scripts/oauth_helper.py \
  --name "Acme Family Medicine" \
  --fhir-base-url https://fhir4.eclinicalworks.com/fhir/r4/<orgId>
```

The helper prints (and opens) the authorize URL. After you log in and consent:

- if `HEALOW_REDIRECT_URI` is a **loopback** URL, the helper runs a temporary
  local server, captures the `code` automatically, and finishes the exchange;
- otherwise it prompts you to **paste** the `code` (or the full redirect URL).

On success it prints the connected practice and patient id, with tokens stored
in your keychain.

### B) Through MCP tools

1. `connect_practice(name, fhir_base_url)` → returns `{authorize_url, state}`.
2. Open `authorize_url` in a browser, log in, and consent.
3. Copy the `code` from the redirect and call
   `complete_oauth(code, state)` → returns `{practice, patient_id}`.

After connecting, tokens auto-refresh: proactively when the access token has
< 60s of life, and reactively on a 401.

## MCP tools

**Auth / admin:** `connect_practice`, `complete_oauth`, `list_practices`,
`whoami`, `get_capability_statement`.

**Generic FHIR:** `fhir_get(practice, resource, id)`,
`fhir_search(practice, resource, params)` — return raw resources / Bundles.

**Typed wrappers:** `get_patient`, `search_encounter`, `get_coverage`,
`get_practitioner`, `search_observation(category=…)`,
`search_condition(category=…)`, `search_medication_request`,
`get_document_reference`, `get_binary`, `list_allergies`, `list_immunizations`,
`list_care_plans`, `list_problems`.

`search_observation`/`search_condition` take a friendly USCDI `category`
(e.g. `laboratory`, `vital-signs`, `problem-list-item`) which maps to the FHIR
`category` search parameter.

**Pagination:** typed `search_*`/`list_*` return the **first page** Bundle.
Follow `Bundle.link` (rel `next`) via `fhir_search` to page further; no
auto-pagination.

## Claude Desktop config

Add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "healow": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/healow-mcp-server", "healow-mcp"],
      "env": {
        "HEALOW_CLIENT_ID": "your-client-id",
        "HEALOW_CLIENT_SECRET": "your-client-secret",
        "HEALOW_REDIRECT_URI": "http://localhost:9999/callback"
      }
    }
  }
}
```

Run the one-time bootstrap (path A or B) before or after adding this entry;
tokens persist in your keychain across restarts.

## Development

```bash
uv run pytest        # tests use httpx MockTransport + an in-memory keychain
```

Tests cover SMART discovery + caching, PKCE, proactive token refresh, the
401-refresh-retry path, 429 backoff, and an example resource search.

## Note on category-qualified scopes

The exact string format for the category-qualified Condition/Observation scopes
differs between SMART v1 and v2 granular scopes. It is centralized in
`healow_mcp/scopes.py` (`CATEGORY_SCOPE_STYLE` / `_category_scope`) so it is a
single edit once confirmed against healow's published scope list for your
registered app.

TDQS

B3.4/5.0

Scored across 20 tools

Disambiguation4/5

Most tools are clearly distinct by resource type (e.g., search_encounter vs. search_condition). The generic fhir_get/fhir_search overlap with specific wrappers, but descriptions clarify their broad vs. focused nature. A few potential confusions exist (e.g., list_allergies vs. search_condition), but overall boundaries are clear.

Naming Consistency3/5

Tool names are all snake_case but mix verb styles: get_ (get_patient), search_ (search_encounter), list_ (list_allergies), fhir_ (fhir_get), plus standalone names (whoami, connect_practice). This inconsistent verb choice across similar operations (e.g., search_observation vs. list_immunizations) makes the pattern less predictable.

Tool Count4/5

20 tools is reasonable for a FHIR server covering authentication, generic FHIR operations, and a broad set of clinical resource types. While slightly on the higher side, each tool serves a distinct resource or workflow, and the count matches the domain complexity.

Completeness4/5

The toolset covers core FHIR resources (Patient, Encounter, Observation, Condition, MedicationRequest, etc.) with both list and search capabilities. Missing common resources like DiagnosticReport or Procedure can be accessed via generic fhir_search, so agents can work around gaps. The read-only scope is consistent with the OAuth launch context.

Maintenance

ActivityInactive
ResponsivenessUnresponsive