Skip to main content
Glama
sdesani

NPI MCP Server

by sdesani
README.md
---
title: NPI MCP Server
emoji: 🩺
colorFrom: blue
colorTo: green
sdk: docker
pinned: false
app_port: 8000
---

# NPI MCP Server

An MCP server that wraps the public
[NPPES NPI Registry API](https://npiregistry.cms.hhs.gov/api) (version 2.1, no
authentication) and exposes it over streamable HTTP at `/mcp`.

It does more than proxy the registry. Three tools pass NPPES queries through;
three add logic the registry does not provide — offline checksum validation, a
composite referral-eligibility assessment, and a plain-English specialty search
that maps everyday clinical words onto NUCC taxonomy codes.

## Tools

Direct NPPES access:

| Tool | Use it when |
| --- | --- |
| `search_provider(first_name, last_name, state, limit)` | You know the provider's name. `state` is a 2-letter code. |
| `lookup_by_npi(npi_number)` | You already hold an exact 10-digit NPI. |
| `get_specialties(npi_number)` | You only need the provider's NUCC taxonomies. |

Derived intelligence, not available from NPPES:

| Tool | Use it when |
| --- | --- |
| `validate_npi_format(npi_number)` | Cheap offline pre-flight. Runs the CMS Luhn checksum over `80840` + the first 9 digits. Synchronous, no network call — use it before `lookup_by_npi` to avoid a wasted round trip on a mistyped number. |
| `check_provider_status(npi_number)` | "Can I refer a patient to this provider?" Five ordered checks: checksum → registry lookup → active enumeration → declared primary taxonomy → complete practice address. Returns an actionable concern per failed check. |
| `find_providers_by_specialty(specialty_keyword, state, last_name_hint)` | The user describes a *kind* of provider — "a cardiologist", "peds", "skin". Maps the phrase to NUCC codes, searches, then filters out the registry's text-match noise. |

Supported specialty keywords: cardiology, dermatology, family medicine,
neurology, oncology, orthopedic, pediatrics, psychiatry — plus common synonyms
such as `heart`, `peds`, `mental health`, `skin`, `cancer` and `primary care`.

The three derived tools never raise. A mistyped NPI, an unknown NPI, an NPPES
outage or an unrecognized keyword each return a structured result explaining
what happened, so the calling model can recover instead of seeing an exception.
An upstream outage is reported as *inconclusive* rather than "no such provider".

## Endpoints

| Path | Purpose |
| --- | --- |
| `POST /mcp` | MCP streamable HTTP endpoint. |
| `GET /health` | `{"status", "version", "uptime_seconds"}`. |
| `GET /` | Server name, version and MCP endpoint path, so a platform root probe gets 200. |

## Configuration

| Variable | Meaning |
| --- | --- |
| `MCP_ALLOWED_HOSTS` | Comma-separated public hostnames to trust for DNS-rebinding protection. `localhost` and `127.0.0.1` (with and without ports) are always allowed. Each host `H` also trusts the origin `https://H`. |
| `HOST` / `PORT` | Bind address. The container defaults to `0.0.0.0:8000`. |
| `LOG_LEVEL` | Default `INFO`. |

The public hostname is runtime configuration, never compiled in, so one image
serves both deployment targets. The resolved `allowed_hosts` are logged at
startup.

**Set `MCP_ALLOWED_HOSTS` for any non-local deployment.** Without it, requests
arriving with a public `Host` header are rejected with HTTP 421.

## Deploy

### Hugging Face Spaces (Docker SDK)

The frontmatter above configures the Space; `app_port: 8000` matches the
Dockerfile's `EXPOSE`. Add a Space **variable** `MCP_ALLOWED_HOSTS` set to the
Space hostname, e.g. `your-name-npi-mcp.hf.space`. The MCP endpoint is then
`https://your-name-npi-mcp.hf.space/mcp`.

### Cloudflare tunnel

```bash
docker build -t npi-mcp .
docker run -d -p 8000:8000 -e MCP_ALLOWED_HOSTS="<your-tunnel-host>.trycloudflare.com" npi-mcp
cloudflared tunnel --url http://localhost:8000
```

A quick tunnel's hostname changes every run, so pass the new one via
`MCP_ALLOWED_HOSTS` and restart the container. Multiple hosts are accepted, so a
stable named tunnel and a Space hostname can be trusted at the same time.

## Local development

```bash
pip install -e .
npi-mcp                    # http://0.0.0.0:8000
mcp dev src/npi_mcp/server.py
```

`mcp dev` loads `server.py` by file path, so the module has no parent package —
every intra-package import is absolute (`from npi_mcp.models import ...`).

The SDK is pinned to `mcp[cli]>=1.0,<2`: the 2.x SDK renames `FastMCP` to
`MCPServer` and changes the API.