Skip to main content
Glama
README.md
# MedBridge

An MCP server that gives an LLM live access to clinical trials, FDA drug recalls, adverse-event reports, drug labels, and drug-name normalization.

MCP (Model Context Protocol) is an open protocol that lets an AI application discover and call external tools over a standard interface. A client — Claude Desktop, an MCP-compatible IDE, or a custom agent — connects to a server, asks what tools it offers, and invokes them with typed arguments; MedBridge is one such server, wrapping three public healthcare APIs behind six validated tools.

![Demo: asking Claude Desktop about recruiting diabetes trials near Dallas and metformin recalls, tools firing live](docs/demo.gif)

*Claude Desktop answering from live data: `search_trials` returns recruiting Dallas trials by NCT number, then `search_drug_recalls` pulls FDA enforcement records for metformin.*

**This is informational public data only. Nothing MedBridge returns is medical advice, and it is not a clinical decision tool.**

## Tools

| Tool | Purpose | Key parameters |
|---|---|---|
| `search_trials` | Find clinical trials for a condition | `condition`, `status`, `location`, `max_results` |
| `get_trial` | Full record for one trial, including eligibility criteria | `nct_id` |
| `search_drug_recalls` | FDA recall/enforcement reports for a drug | `drug_name`, `max_results` |
| `get_adverse_events` | Most frequently reported side effects for a drug | `drug_name`, `top_n` |
| `get_drug_label` | FDA-approved label: indications, warnings, dosage forms | `drug_name` |
| `normalize_drug_name` | Resolve a (possibly misspelled) drug name to its RxNorm concept | `name` |

Every response carries `source` and `retrieved_at`. Long text fields are truncated at stated limits with a `<field>_truncated: true` flag when cut. Failures come back as a structured `{error: true, error_type, message}` rather than a stack trace or a silently empty result — see [Design decisions](#design-decisions).

## Install

Requires Python 3.11+.

```bash
git clone https://github.com/MYASHWANTHREDDY/medbridge-mcp.git
cd medbridge-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```

`OPENFDA_API_KEY` is optional — it raises openFDA's rate limit but every tool works without it. To set it:

```bash
cp .env.example .env
# then edit .env and set OPENFDA_API_KEY=your-key-here
```

Confirm the install:

```bash
pytest -q
```

## Connect to Claude Desktop

Claude Desktop launches MCP servers as a local subprocess, configured in its `claude_desktop_config.json`:

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

Add a `medbridge` entry pointing at the venv's Python interpreter, running the server as a module. [`examples/claude_desktop_config.json`](examples/claude_desktop_config.json) has the template:

```json
{
  "mcpServers": {
    "medbridge": {
      "command": "/ABSOLUTE/PATH/TO/medbridge-mcp/.venv/bin/python",
      "args": ["-m", "medbridge.server"]
    }
  }
}
```

Replace the path with the absolute path to your clone's `.venv/bin/python`, then restart Claude Desktop fully.

**Running under WSL:** Claude Desktop only ships for macOS and Windows, so on WSL the Windows-side Desktop app has to reach into the Linux filesystem to launch the server. Point `command` at `wsl.exe` instead and pass the real command as arguments — see [`examples/claude_desktop_config.wsl.json`](examples/claude_desktop_config.wsl.json):

```json
{
  "mcpServers": {
    "medbridge": {
      "command": "wsl.exe",
      "args": ["-d", "Ubuntu-24.04", "--", "/ABSOLUTE/PATH/TO/medbridge-mcp/.venv/bin/python", "-m", "medbridge.server"]
    }
  }
}
```

Replace `Ubuntu-24.04` with your distro name from `wsl.exe -l -v` if it differs.

Once connected, Claude Desktop lists the six tools under its tools/search indicator and prompts for approval on first use of each.

### Any MCP client

Nothing here is Claude-specific beyond the config file format. Any client that speaks the MCP stdio transport — an MCP-compatible IDE, a custom agent script, another chat client — can launch the same command (`/path/to/.venv/bin/python -m medbridge.server`) and connect. The server has no knowledge of which client is on the other end.

For interactive debugging outside any client, the official inspector works too (requires Node):

```bash
npx @modelcontextprotocol/inspector /ABSOLUTE/PATH/TO/medbridge-mcp/.venv/bin/python -m medbridge.server
```

## Design decisions

Outputs are shaped, not proxied. Raw upstream JSON is deeply nested and full of fields no one asking a question needs — a tool response for LLM consumption is an interface design problem, not a pass-through. `get_adverse_events`, for instance, returns ranked reaction counts instead of raw case records, and long free-text fields are cut to stated limits with a truncation flag so the model reading the output knows it's seeing a summary rather than the whole field.

Errors are structured and honest. Every failure maps to exactly one of four types — `not_found`, `upstream_unavailable`, `rate_limited`, `invalid_input` — carried in a small dict a model can act on, instead of a stack trace. Zero legitimate matches is a *success* carrying an empty list and a note; only an actual failure sets `error: true`. That distinction is what lets a tool answer "no recalls found" correctly instead of a model guessing from a bare empty list whether the search worked.

Every response carries provenance: `source` (which upstream API answered) and `retrieved_at` (when). Public health data changes; a model — and the person reading its answer — should know how fresh it is and where it came from.

Caching is a single-process, in-memory TTL dict keyed on URL and sorted query parameters, not an external cache. The server is one process speaking stdio to one client at a time, so there's no second process to share cache state with, and no concurrent writer to guard against — an external cache would be deployment theater at this scale. It exists because public APIs are shared infrastructure and the same question tends to come up more than once in a conversation; repeated identical requests inside the TTL window are served from memory rather than hitting the network again. Retries on 429 and 5xx use exponential backoff for the same reason: a demo that hammers a public API on transient errors fails unpredictably and disrespects rate limits.

`normalize_drug_name`'s candidates carry a `match_source` field (`spelling_suggestion` or `approximate_term`) beyond the minimal `{name, rxcui, score}` shape. This came from testing RxNorm's `approximateTerm` endpoint directly: for a misspelling like "metfromin" it ranks lexically similar but wrong concepts (e.g. "merbromin") above the intended drug and never surfaces it in a usable number of results. `spellingsuggestions` does return "metformin" for that input. Both endpoints answer genuinely different questions — one corrects a typo, the other finds lexically similar concepts — so both are consulted and merged, and each candidate names which one produced it.

## Testing

```bash
pytest -q
```

60 tests, entirely offline — every upstream call is intercepted with [respx](https://github.com/lundberg/respx) against real response payloads captured live from all three APIs and trimmed to the fields the code actually reads (`tests/fixtures/`). Coverage spans the HTTP layer (success paths, openFDA's 404-means-empty convention, retry-then-succeed on 5xx, exhausted retries on 429 mapping to `rate_limited`, timeout mapping to `upstream_unavailable`), the pure shaping functions (exact output shapes, truncation flags, adverse-event aggregation), input validation (malformed identifiers, out-of-range counts, blank strings), and tool-level contracts (every success path carries `source` and `retrieved_at`; every failure path returns a structured error instead of raising).

## Data sources and their terms

| Source | Base URL | Notes |
|---|---|---|
| [ClinicalTrials.gov](https://clinicaltrials.gov/data-api/api) | `clinicaltrials.gov/api/v2` | U.S. government public data; no key required. See [terms and conditions](https://clinicaltrials.gov/about-site/terms-conditions). |
| [openFDA](https://open.fda.gov) | `api.fda.gov` | No key required; optional key raises the rate limit. openFDA explicitly disclaims the data as not for real-time clinical or production decision-making without independent verification — see [openFDA terms](https://open.fda.gov/terms/). |
| [RxNorm (NLM RxNav)](https://lhncbc.nlm.nih.gov/RxNav/APIs/RxNormAPIs.html) | `rxnav.nlm.nih.gov/REST` | No key required for API access. RxNorm draws on source vocabularies that fall under the UMLS Metathesaurus; broader use beyond simple normalization lookups may require a free [UMLS Metathesaurus License](https://www.nlm.nih.gov/databases/umls.html). |

## Limitations and future work

- **No drug-drug interaction tool.** NLM's interaction API was retired; interaction data would need a different, licensed source, so this scope was cut rather than faked with a weaker substitute.
- **No pagination.** Search tools return up to `max_results` (max 25) in one call; there's no cursor or next-page token for walking a full result set.
- **stdio transport only.** No HTTP/SSE server mode, so MedBridge currently only runs as a locally spawned subprocess, not as a remote service multiple clients could share.
- **Tool layer, not yet an agent.** MedBridge exposes these six tools to any MCP client today; wiring the same server into an autonomous agent loop that chains calls (search a trial, then check the drug's recalls, then normalize a name it wasn't sure about) is the natural next step.

## License

MIT — see [LICENSE](LICENSE).