ClinicalTrials.gov MCP Server — OncoHub
A production-quality MCP server that wraps the ClinicalTrials.gov v2 API, providing structured, TTL-cached access to trial data worldwide and Turkey via 4 MCP tools.
README
ClinicalTrials.gov MCP Server — OncoHub
A production-quality MCP (Model Context Protocol) server that wraps the ClinicalTrials.gov v2 API. Designed for the OncoHub Tumor Council as the data source for clinical_trial_matching_module v2 / A2.3.
What this server does: structured, TTL-cached access to CT.gov trial data (worldwide + Turkey) via 4 MCP tools. What it does not do: clinical eligibility assessment, scoring, or any form of clinical judgment — that is the consuming system's responsibility.
Why Python + FastMCP?
- CT.gov v2 is a plain JSON REST API — no special SDK needed
- FastMCP's HTTP transport satisfies claude.ai's remote connector requirement (STDIO won't work there)
httpx+asynciogive clean async retry/backoff- SQLite cache requires zero infrastructure beyond the process itself
Project Structure
CT-MSP/
├── server.py # FastMCP server + 4 MCP tools (entry point)
├── ctgov_client.py # Async httpx client, rate limiting, retry
├── normalize.py # Raw CT.gov JSON → flat OncoHub schema
├── cache.py # SQLite TTL cache (no external deps)
├── models.py # Pydantic models (schema documentation)
├── tests/
│ ├── conftest.py # Shared fixtures + sample API responses
│ ├── test_normalize.py
│ ├── test_client.py
│ ├── test_cache.py
│ ├── test_tools.py # MCP tool integration tests (mocked)
│ └── test_smoke.py # Live API smoke tests (opt-in)
├── Dockerfile
├── render.yaml # One-click Render deployment
├── pyproject.toml
└── .env.example
Local Setup
Requirements: Python 3.11+, pip
# 1. Clone / navigate to project directory
cd CT-MSP
# 2. Create virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3. Install dependencies
pip install -e ".[dev]"
# 4. Configure environment
cp .env.example .env
# Edit .env if needed (defaults work for local development)
# 5. Run the server
python server.py
# Server starts on http://localhost:8000
# MCP endpoint: http://localhost:8000/mcp
Test with MCP Inspector
npx @modelcontextprotocol/inspector http://localhost:8000/mcp
This opens a browser UI where you can call tools interactively.
Run tests
pytest # all unit tests (no network)
pytest -m smoke # live CT.gov smoke tests (requires internet)
pytest tests/test_normalize.py # specific module
Public Deployment
Required for claude.ai connector — claude.ai only connects to public HTTPS endpoints.
Option A: Render (recommended — free tier, auto-HTTPS)
- Push this repo to GitHub.
- Go to render.com → New → Web Service → connect your repo.
- Render auto-detects
render.yaml— click Apply. - Wait ~3 min for the first build.
- Your URL:
https://ctgov-mcp-oncohub.onrender.com(or your custom name). - Free tier caveat: the service sleeps after 15 min of inactivity (first request takes ~30s to wake). The SQLite cache resets on restart since
/tmpis ephemeral. Upgrade to Starter ($7/mo) for a persistent disk (/data).
Persistent disk (Starter plan): uncomment the disk: section in render.yaml and set CACHE_PATH=/data/ctgov.db.
Option B: Railway
# Install Railway CLI
npm i -g @railway/cli
railway login
railway init
railway up
Set environment variables via railway variables set STATUS_TTL_DAYS=7 ...
Option C: Fly.io
fly launch # detects Dockerfile automatically
fly deploy
fly secrets set STATUS_TTL_DAYS=7 META_TTL_DAYS=30
Add a volume for persistent cache:
fly volumes create ctgov_cache --size 1
Then set CACHE_PATH=/data/ctgov.db in fly.toml.
Option D: Any Docker host
docker build -t ctgov-mcp .
docker run -p 8000:8000 \
-e STATUS_TTL_DAYS=7 \
-v ctgov_cache:/data \
-e CACHE_PATH=/data/ctgov.db \
ctgov-mcp
Adding to claude.ai as a Custom Connector
Prerequisites:
- Server running at a public HTTPS URL (e.g.
https://ctgov-mcp-oncohub.onrender.com)- claude.ai Pro, Team, or Enterprise plan
- OncoHub Project must be private
Steps (Individual / Pro)
- Go to claude.ai → [Your name] → Settings → Connectors.
- Click "+" → "Add custom connector".
- Name:
ClinicalTrials.gov (OncoHub) - URL:
https://<your-host>/mcp - Auth: leave OAuth fields blank (this server has no auth).
- Click Add.
Steps (Team / Enterprise)
- An Owner goes to Organization Settings → Connectors → Add connector (same fields as above).
- Members connect it via [+] → Connectors in any conversation.
Activating in the Tumor Council Project
- Open the OncoHub project.
- In a conversation, click "+" (bottom-left) → Connectors.
- Toggle ClinicalTrials.gov (OncoHub) ON.
- The system prompt should reference tool names (e.g.
dual_source_search) for the module to call them automatically during report generation.
Note:
claude_desktop_config.json(STDIO transport) does NOT work with claude.ai Projects — only remote HTTP/SSE connectors work there.
MCP Tools Reference
dual_source_search ← Primary council entry point
Searches worldwide AND Turkey in two sequential calls, merges by NCT ID, marks has_turkey_site.
| Parameter | Type | Default | Description |
|---|---|---|---|
condition |
str | required | Disease/condition (maps from A1 tumor field) |
term |
str? | null | Biomarker / keyword (maps from A1 biomarker field) |
intervention |
str? | null | Drug or target (maps from A1 treatment field) |
statuses |
list[str]? | RECRUITING, NOT_YET_RECRUITING | Overall status filter |
phases |
list[str]? | null | Phase filter (maps from A1 treatment line) |
page_size |
int | 20 | Results per sub-query (max 50) |
force_refresh |
bool | false | Skip cache |
Returns: {studies: [...], total_count, turkey_count, next_page_token, note}
search_trials
Parameterized worldwide search.
| Parameter | Type | Default | Description |
|---|---|---|---|
condition |
str | required | Disease/condition |
term |
str? | null | Free-text biomarker or keyword |
intervention |
str? | null | Drug or target |
statuses |
list[str]? | RECRUITING, NOT_YET_RECRUITING | See valid values below |
phases |
list[str]? | null | See valid values below |
country |
str? | null | Country name for location filter |
page_size |
int | 20 | Max 50 recommended |
page_token |
str? | null | Pagination cursor |
force_refresh |
bool | false | Skip cache |
Valid statuses: RECRUITING, NOT_YET_RECRUITING, ACTIVE_NOT_RECRUITING, COMPLETED, TERMINATED, WITHDRAWN, SUSPENDED, ENROLLING_BY_INVITATION, UNKNOWN
Valid phases: EARLY_PHASE1, PHASE1, PHASE2, PHASE3, PHASE4, NA
search_turkey_trials
Convenience wrapper: search_trials with country="Turkey". Same parameters (minus country).
get_trial
Fetch full details for a single study.
| Parameter | Type | Description |
|---|---|---|
nct_id |
str | NCT number, e.g. "NCT05678901" |
force_refresh |
bool | Skip cache |
Returns: single normalized study with full eligibility.criteria_text and all locations.
Normalized Study Schema
Every tool returns studies in this flat schema:
{
"nct_id": "NCT05678901",
"title": "A Study of Sotorasib...",
"status": "RECRUITING",
"status_unknown_flag": false,
"phases": ["PHASE2"],
"study_type": "INTERVENTIONAL",
"conditions": ["Non-Small Cell Lung Cancer"],
"interventions": [{"type": "DRUG", "name": "Sotorasib"}],
"eligibility": {
"criteria_text": "Inclusion Criteria:\n- KRAS G12C...",
"sex": "ALL",
"min_age": "18 Years",
"max_age": "N/A",
"healthy_volunteers": false
},
"locations": [
{"country": "Turkey", "city": "Istanbul", "facility": "IUH", "status": "RECRUITING"}
],
"has_turkey_site": true,
"turkey_sites": [{"city": "Istanbul", "facility": "IUH", "status": "RECRUITING"}],
"lead_sponsor": "Amgen",
"last_update_post_date": "2024-05-01",
"url": "https://clinicaltrials.gov/study/NCT05678901",
"retrieved_at": "2026-06-24T10:30:00+00:00",
"freshness": "live",
"cached_at": null
}
Key fields for clinicians:
status_unknown_flag: true→ status unverified for 2+ years, verify before actingretrieved_at→ when data was fetched from CT.gov (always surface this)freshness: "cached"→ data served from local cache;cached_atshows when it was stored
OncoHub Integration: A1 → A2.3 Field Mapping
| A1 Patient Profile Field | dual_source_search Parameter |
Notes |
|---|---|---|
| Tumor type / primary diagnosis | condition |
Required |
| Biomarker (e.g. KRAS G12C, PD-L1) | term |
Free text, forwarded to query.term |
| Current/target drug | intervention |
Forwarded to query.intr |
| Treatment line / phase preference | phases |
e.g. ["PHASE2","PHASE3"] |
| Eligibility status filter | statuses |
Default: RECRUITING + NOT_YET_RECRUITING |
Module flow:
A1 Patient Card
↓
A2.3 dual_source_search(condition, term, intervention, phases)
↓
Normalized studies[] with has_turkey_site, freshness, retrieved_at
↓
A3 Eligibility Matrix
↓
A5 Council Report Output
The server provides data only. A3 performs eligibility matching; A5 formats the output.
Cache & Freshness Configuration
| Variable | Default | Description |
|---|---|---|
STATUS_TTL_DAYS |
7 | Max age for cached recruiting/status data |
META_TTL_DAYS |
30 | Reserved for future eligibility-only caching |
CACHE_PATH |
.cache/ctgov.db |
SQLite file path |
Why TTL matters for patient safety: Trial status changes frequently. A "RECRUITING" status cached 30 days ago may no longer be accurate. STATUS_TTL_DAYS=7 ensures clinicians see data verified within one week, or the server fetches fresh data.
Cache behavior:
- Hit (fresh): returns stored data with
freshness: "cached"+cached_attimestamp - Miss or expired: fetches from CT.gov, updates cache, returns with
freshness: "live" force_refresh: true: always fetches live, updates cache
Cloud deployments: On free-tier hosts with ephemeral storage, the cache resets on each restart — all requests go live until the cache warms up again. Use a persistent volume for production.
Rate Limiting & CT.gov ToU Compliance
- Minimum 1 second between API requests (
CTGOV_RATE_INTERVAL=1.0) - Automatic exponential-backoff retry on network/5xx errors (max 3 attempts)
- No contact-harvesting: email, phone, and investigator PII are never included in output
- No bulk corpus download:
pageSizecapped at 50 per call User-Agentheader identifies this client to CT.gov
No Authentication Required
CT.gov v2 is a public API — no API key, no OAuth. This server also has no auth on its /mcp endpoint (it serves read-only public data). For production deployments where you want to restrict access, place a reverse proxy with IP allowlisting or a simple bearer token in front.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.