Clinicaltrialsgov MCP server agent

Search ClinicalTrials.gov trials, retrieve study details and results, and match patients to eligible trials via MCP.

by cyanheads·Apache-2.0 license·★ 96 Stars on the repo·GitHub ↗

Files of Clinicaltrialsgov MCP server

cyanheads/main1 file
README.md
Show the full text341 lines

clinicaltrialsgov-mcp-server

Search ClinicalTrials.gov trials, retrieve study details and results, and match patients to eligible trials via MCP. STDIO or Streamable HTTP.

7 Tools • 1 Resource • 1 Prompt

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework


Overview

Clinical trial data from the ClinicalTrials.gov REST API v2 — the US National Library of Medicine's registry of 600K+ clinical trial studies. Search trials, fetch full study records and posted results, discover field names and valid values, and match patient demographics to eligible recruiting trials. Public, read-only, no authentication required. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.

Tools
Tool Description
clinicaltrials_search_studies Search studies with full-text and field-specific queries, status/phase/geographic filters, pagination, sorting, and field selection
clinicaltrials_get_study_record Fetch a single study by NCT ID — full protocol record with optional location/outcome/reference caps
clinicaltrials_get_study_count Fast total study count for a query, without fetching data
clinicaltrials_get_field_values Discover valid values for API fields, with per-value study counts
clinicaltrials_get_field_definitions Resolve valid field names — keyword search, path drill-down, or top-level overview
clinicaltrials_get_study_results Fetch posted results — outcomes, adverse events, participant flow, baseline — for completed studies
clinicaltrials_find_eligible Match patient demographics and conditions to eligible recruiting trials
Resources
Resource Description
clinicaltrials://{nctId} Fetch a single clinical study by NCT ID as JSON, with capped lists and results replaced by counts
Prompts
Prompt Description
analyze_trial_landscape Guides a data-driven clinical trial landscape analysis using the count and search tools

Capability reference

clinicaltrials_search_studies tool
  • Free-text query plus field-specific conditionQuery / interventionQuery / locationQuery / sponsorQuery / titleQuery / outcomeQuery; statusFilter (case- and separator-insensitive, registry display labels included: "Active, not recruiting" works) / phaseFilter enums, advancedFilter (AREA[FieldName]value / RANGE[min, max] syntax), and geoFilter (distance(lat,lon,radius) with a mi/km suffix) for proximity search with nearest-site re-ranking
  • Returns a compact per-study index by default (nctId, briefTitle, overallStatus, phases, enrollmentCount, leadSponsor, conditions, hasResults, startDate, primaryCompletionDate, a bounded locations summary); pass fields (PascalCase leaves) for a full-fidelity projection — full records run ~70KB
  • pageSize 1–CT_MAX_PAGE_SIZE (default 10; the cap is 200 unless overridden), cursor pagination via pageToken, sort on up to 2 fields
  • Excludes the upstream "unknown" enrollment sentinel (99999999) by default — includeUnknownEnrollment to include it, or automatically lifted when nctIds is supplied
  • Typed errors: blank_value, ids_not_found, field_invalid, enum_invalid, query_parse_error, geo_invalid, sort_invalid, rate_limited

clinicaltrials_get_study_record tool
  • Full protocol record by NCT ID — identification, status, sponsor, conditions, design, arms/interventions, outcomes, eligibility, contacts/locations
  • Optional locationLimit (≤500), outcomeLimit / referenceLimit (≤100), and nearLocation (lat, lon, radiusMi default 50) to bound and sort locations; upstream totals reported in filtersApplied only when a cap actually trims the list
  • resultsSection is replaced by compact resultsSummary counts — fetch full results via clinicaltrials_get_study_results
  • Typed errors: study_not_found, rate_limited

clinicaltrials_get_study_count tool
  • Same query/filter surface as clinicaltrials_search_studies (free-text and field-specific queries, status/phase filters, advancedFilter) but returns only totalCount — no study data fetched
  • Excludes the unknown-enrollment sentinel by default (includeUnknownEnrollment to include it)
  • Typed errors: blank_value, field_invalid, enum_invalid, query_parse_error, rate_limited

clinicaltrials_get_field_values tool
  • One or more PascalCase field names (e.g. OverallStatus, Phase, LeadSponsorClass) — returns each field's type, unique-value count, and top values with study counts (capped at 250 by the API)
  • Numeric/date fields report min / max / avg / formats instead of top values; boolean fields report trueCount / falseCount
  • multiValued flags fields where a study can carry several values, so per-value study counts can sum above the study total
  • Typed errors: blank_value, field_invalid, rate_limited

clinicaltrials_get_field_definitions tool
  • Three modes: search (keyword, ranked matches, limit up to 100, default 20), drill (dot-notation path into a section), overview (top-level sections, no other args)
  • Resolves the canonical PascalCase field names accepted by fields, advancedFilter, sort, and clinicaltrials_get_field_values
  • Typed errors: blank_value, mode_mismatch, mode_requires, path_not_found, rate_limited

clinicaltrials_get_study_results tool
  • Up to 20 NCT IDs per call; only returns data for studies where hasResults is true — outcome measures, adverse events, participant flow, baseline characteristics, and results metadata
  • summary (default false) condenses a full result set — which can exceed 500KB per study — to a few KB; full mode supports outcomeLimit (≤100) and adverseEventLimit (≤500), resumable via outcomeOffset / seriousEventOffset / otherEventOffset
  • sections filters to outcomes, adverseEvents, participantFlow, baseline, moreInfo
  • A previous (alias) NCT ID resolves to its canonical study, named in canonicalNctId
  • Typed errors: blank_value, offset_not_applicable, rate_limited

clinicaltrials_find_eligible tool
  • Takes age, sex (FEMALE / MALE / ALL), conditions[], location (country required, state / city optional), healthyVolunteer, recruitingOnly (default true), maxResults (≤50)
  • Re-ranks results so studies whose own condition list names a requested condition surface above tangential MeSH-umbrella matches from the upstream fuzzy search
  • Bounds each candidate's locations to the sites matching the requested location (capped by locationLimit, ≤500) instead of every registered site, adding one recruiting site when none of the matched ones is open — the one nearest the matched sites by published coordinates (with distanceMi), kept to the requested country when a site there recruits, or the first in match order when coordinates are missing
  • funnel reports match counts at each filter stage (condition → +location → +demographics) to show where the query narrowed to zero
  • Typed errors: blank_value, rate_limited

clinicaltrials://{nctId} resource
  • Full protocol record as application/json, with locations, secondary/other outcomes, and references each capped at 50 — fixed server-side, no arguments
  • Results data is replaced by resultsSummary counts; truncated and filtersApplied disclose what was capped, with retrieval naming the tools that fetch the full data
  • Typed errors: study_not_found, rate_limited

analyze_trial_landscape prompt
  • Arguments: topic required; focusAreas (comma-separated) optional
  • Returns one user message pointing the agent at the count, search, field-discovery, and results tools for a data-driven landscape analysis

Features

Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.

ClinicalTrials.gov-specific:

  • Type-safe client for the ClinicalTrials.gov REST API v2 — public, no authentication or API keys required
  • Serialized request queue enforcing ClinicalTrials.gov's ~1 req/sec rate limit, with retry and exponential backoff on 429/5xx responses
  • Auto-corrects field names passed to fields/sort — case/whitespace fixes and known legacy aliases (e.g. RecruitmentStatus → OverallStatus) — before validating, logging every correction
  • Detects upstream HTML error pages returned with a JSON content-type and retries rather than parsing them as data
  • Geographic proximity search and nearest-site re-ranking, with no geocoding dependency

Agent-friendly output:

  • Provenance — clinicaltrials_search_studies / clinicaltrials_get_study_count / clinicaltrials_find_eligible echo searchCriteria on every call, including sentinelFilterActive when the default unknown-enrollment exclusion applies, and clinicaltrials_get_study_results names canonicalNctId when a previous (alias) ID resolves to a different study
  • Graceful partial failure — clinicaltrials_get_study_results returns per-study fetchErrors / studiesWithoutResults rows instead of failing the whole batch when one ID is malformed or lacks results
  • Discriminated output — typed error reason codes per tool (study_not_found, blank_value, offset_not_applicable, …), and bounded lists (filtersApplied, locationSummary) carry a next*Offset only when more remains, so callers branch on presence instead of parsing text
  • Response shaping — clinicaltrials_search_studies and clinicaltrials_find_eligible return a compact per-study index or location-bounded set by default instead of the ~70KB full record, escalating to full fidelity only via fields or clinicaltrials_get_study_record

Getting started

Public Hosted Instance

A public instance is available at https://clinicaltrials.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:

{
  "mcpServers": {
    "clinicaltrialsgov-mcp-server": {
      "type": "streamable-http",
      "url": "https://clinicaltrials.caseyjhand.com/mcp"
    }
  }
}
Self-Hosted / Local

Add the following to your MCP client configuration file.

{
  "mcpServers": {
    "clinicaltrialsgov-mcp-server": {
      "type": "stdio",
      "command": "bunx",
      "args": ["clinicaltrialsgov-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info"
      }
    }
  }
}

Or with npx (no Bun required):

{
  "mcpServers": {
    "clinicaltrialsgov-mcp-server": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "clinicaltrialsgov-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info"
      }
    }
  }
}

Or with Docker:

{
  "mcpServers": {
    "clinicaltrialsgov-mcp-server": {
      "type": "stdio",
      "command": "docker",
      "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/clinicaltrialsgov-mcp-server:latest"]
    }
  }
}

For Streamable HTTP, set the transport and start the server:

MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
Installation
  1. Clone the repository:
git clone https://github.com/cyanheads/clinicaltrialsgov-mcp-server.git
  1. Navigate into the directory:
cd clinicaltrialsgov-mcp-server
  1. Install dependencies:
bun install

Configuration

All configuration is optional — the server works with defaults and no API keys.

Variable Description Default
CT_API_BASE_URL ClinicalTrials.gov API base URL. https://clinicaltrials.gov/api/v2
CT_REQUEST_TIMEOUT_MS Per-request timeout in milliseconds. 30000
CT_MAX_PAGE_SIZE Maximum page size cap. 200
MCP_TRANSPORT_TYPE Transport: stdio or http. stdio
MCP_HTTP_PORT Port for HTTP server. 3010
MCP_SESSION_MODE HTTP session mode: stateless, stateful, or auto. stateless
MCP_AUTH_MODE Auth mode: none, jwt, or oauth. none
MCP_LOG_LEVEL Log level (RFC 5424). info
LOGS_DIR Directory for log files (Node.js only). <project-root>/logs
OTEL_ENABLED Enable OpenTelemetry tracing. false

See .env.example for the full list of optional overrides.

Running the server

Local development
  • Build and run:

    # One-time build
    bun run rebuild
    
    # Run the built server
    bun run start:http
    # or
    bun run start:stdio
    
  • Run checks and tests:

    bun run devcheck   # Lint, format, typecheck, and security audit
    bun run test       # Vitest test suite
    bun run lint:mcp   # Validate MCP definitions against spec
    
Docker
docker build -t clinicaltrialsgov-mcp-server .
docker run --rm -p 3010:3010 clinicaltrialsgov-mcp-server

The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/clinicaltrialsgov-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.

Project structure

Directory Purpose
src/index.ts createApp() entry point — registers tools/resources/prompts and inits the ClinicalTrials.gov service.
src/config Server-specific environment variable parsing and validation with Zod.
src/mcp-server/tools Tool definitions (*.tool.ts).
src/mcp-server/resources Resource definitions (*.resource.ts).
src/mcp-server/prompts Prompt definitions (*.prompt.ts).
src/services/clinical-trials ClinicalTrials.gov REST API v2 client — retry, rate limiting, field search, types.
tests/ Unit and integration tests.

Development guide

See CLAUDE.md for development guidelines and architectural rules. The short version:

  • Handlers throw, framework catches — no try/catch in tool logic
  • Use ctx.log for request-scoped logging, no console calls
  • Register new tools and resources via the barrels in src/mcp-server/*/definitions/index.ts
  • Validate raw API responses, normalize to domain types, and never fabricate missing fields

Contributing

Issues are welcome. Run checks and tests before submitting:

bun run devcheck
bun run test

License

Apache-2.0 — see LICENSE for details.

1<div align="center">
2 <h1>clinicaltrialsgov-mcp-server</h1>
3 <p><b>Search ClinicalTrials.gov trials, retrieve study details and results, and match patients to eligible trials via MCP. STDIO or Streamable HTTP.</b>
4 <div>7 Tools • 1 Resource • 1 Prompt</div>
5 </p>
6</div>
7 
8<div align="center">
9 
10[![Version](https://img.shields.io/badge/Version-2.9.10-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/clinicaltrialsgov-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/clinicaltrialsgov-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/clinicaltrialsgov-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
11 
12</div>
13 
14<div align="center">
15 
16[![Install in Claude Desktop](https://img.shields.io/badge/Install_in-Claude_Desktop-D97757?style=for-the-badge&logo=anthropic&logoColor=white)](https://github.com/cyanheads/clinicaltrialsgov-mcp-server/releases/latest/download/clinicaltrialsgov-mcp-server.mcpb) [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=clinicaltrialsgov-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNsaW5pY2FsdHJpYWxzZ292LW1jcC1zZXJ2ZXIiXX0=) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=for-the-badge&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22clinicaltrialsgov-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22clinicaltrialsgov-mcp-server%22%5D%7D)
17 
18[![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
19 
20</div>
21 
22<div align="center">
23 
24**Public Hosted Server:** [https://clinicaltrials.caseyjhand.com/mcp](https://clinicaltrials.caseyjhand.com/mcp)
25 
26</div>
27 
28---
29 
30## Overview
31 
32Clinical trial data from the [ClinicalTrials.gov REST API v2](https://clinicaltrials.gov/data-api/api) — the US National Library of Medicine's registry of 600K+ clinical trial studies. Search trials, fetch full study records and posted results, discover field names and valid values, and match patient demographics to eligible recruiting trials. Public, read-only, no authentication required. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
33 
34### Tools
35 
36| Tool | Description |
37|:---|:---|
38| `clinicaltrials_search_studies` | Search studies with full-text and field-specific queries, status/phase/geographic filters, pagination, sorting, and field selection |
39| `clinicaltrials_get_study_record` | Fetch a single study by NCT ID — full protocol record with optional location/outcome/reference caps |
40| `clinicaltrials_get_study_count` | Fast total study count for a query, without fetching data |
41| `clinicaltrials_get_field_values` | Discover valid values for API fields, with per-value study counts |
42| `clinicaltrials_get_field_definitions` | Resolve valid field names — keyword search, path drill-down, or top-level overview |
43| `clinicaltrials_get_study_results` | Fetch posted results — outcomes, adverse events, participant flow, baseline — for completed studies |
44| `clinicaltrials_find_eligible` | Match patient demographics and conditions to eligible recruiting trials |
45 
46### Resources
47 
48| Resource | Description |
49|:---|:---|
50| `clinicaltrials://{nctId}` | Fetch a single clinical study by NCT ID as JSON, with capped lists and results replaced by counts |
51 
52### Prompts
53 
54| Prompt | Description |
55|:---|:---|
56| `analyze_trial_landscape` | Guides a data-driven clinical trial landscape analysis using the count and search tools |
57 
58## Capability reference
59 
60### `clinicaltrials_search_studies` <sub>tool</sub>
61 
62- Free-text `query` plus field-specific `conditionQuery` / `interventionQuery` / `locationQuery` / `sponsorQuery` / `titleQuery` / `outcomeQuery`; `statusFilter` (case- and separator-insensitive, registry display labels included: `"Active, not recruiting"` works) / `phaseFilter` enums, `advancedFilter` (`AREA[FieldName]value` / `RANGE[min, max]` syntax), and `geoFilter` (`distance(lat,lon,radius)` with a `mi`/`km` suffix) for proximity search with nearest-site re-ranking
63- Returns a compact per-study index by default (`nctId`, `briefTitle`, `overallStatus`, `phases`, `enrollmentCount`, `leadSponsor`, `conditions`, `hasResults`, `startDate`, `primaryCompletionDate`, a bounded locations summary); pass `fields` (PascalCase leaves) for a full-fidelity projection — full records run ~70KB
64- `pageSize` 1–`CT_MAX_PAGE_SIZE` (default 10; the cap is 200 unless overridden), cursor pagination via `pageToken`, `sort` on up to 2 fields
65- Excludes the upstream "unknown" enrollment sentinel (`99999999`) by default — `includeUnknownEnrollment` to include it, or automatically lifted when `nctIds` is supplied
66- Typed errors: `blank_value`, `ids_not_found`, `field_invalid`, `enum_invalid`, `query_parse_error`, `geo_invalid`, `sort_invalid`, `rate_limited`
67 
68---
69 
70### `clinicaltrials_get_study_record` <sub>tool</sub>
71 
72- Full protocol record by NCT ID — identification, status, sponsor, conditions, design, arms/interventions, outcomes, eligibility, contacts/locations
73- Optional `locationLimit` (≤500), `outcomeLimit` / `referenceLimit` (≤100), and `nearLocation` (`lat`, `lon`, `radiusMi` default 50) to bound and sort locations; upstream totals reported in `filtersApplied` only when a cap actually trims the list
74- `resultsSection` is replaced by compact `resultsSummary` counts — fetch full results via `clinicaltrials_get_study_results`
75- Typed errors: `study_not_found`, `rate_limited`
76 
77---
78 
79### `clinicaltrials_get_study_count` <sub>tool</sub>
80 
81- Same query/filter surface as `clinicaltrials_search_studies` (free-text and field-specific queries, status/phase filters, `advancedFilter`) but returns only `totalCount` — no study data fetched
82- Excludes the unknown-enrollment sentinel by default (`includeUnknownEnrollment` to include it)
83- Typed errors: `blank_value`, `field_invalid`, `enum_invalid`, `query_parse_error`, `rate_limited`
84 
85---
86 
87### `clinicaltrials_get_field_values` <sub>tool</sub>
88 
89- One or more PascalCase field names (e.g. `OverallStatus`, `Phase`, `LeadSponsorClass`) — returns each field's type, unique-value count, and top values with study counts (capped at 250 by the API)
90- Numeric/date fields report `min` / `max` / `avg` / `formats` instead of top values; boolean fields report `trueCount` / `falseCount`
91- `multiValued` flags fields where a study can carry several values, so per-value study counts can sum above the study total
92- Typed errors: `blank_value`, `field_invalid`, `rate_limited`
93 
94---
95 
96### `clinicaltrials_get_field_definitions` <sub>tool</sub>
97 
98- Three modes: `search` (keyword, ranked matches, `limit` up to 100, default 20), `drill` (dot-notation `path` into a section), `overview` (top-level sections, no other args)
99- Resolves the canonical PascalCase field names accepted by `fields`, `advancedFilter`, `sort`, and `clinicaltrials_get_field_values`
100- Typed errors: `blank_value`, `mode_mismatch`, `mode_requires`, `path_not_found`, `rate_limited`
101 
102---
103 
104### `clinicaltrials_get_study_results` <sub>tool</sub>
105 
106- Up to 20 NCT IDs per call; only returns data for studies where `hasResults` is true — outcome measures, adverse events, participant flow, baseline characteristics, and results metadata
107- `summary` (default false) condenses a full result set — which can exceed 500KB per study — to a few KB; full mode supports `outcomeLimit` (≤100) and `adverseEventLimit` (≤500), resumable via `outcomeOffset` / `seriousEventOffset` / `otherEventOffset`
108- `sections` filters to `outcomes`, `adverseEvents`, `participantFlow`, `baseline`, `moreInfo`
109- A previous (alias) NCT ID resolves to its canonical study, named in `canonicalNctId`
110- Typed errors: `blank_value`, `offset_not_applicable`, `rate_limited`
111 
112---
113 
114### `clinicaltrials_find_eligible` <sub>tool</sub>
115 
116- Takes `age`, `sex` (`FEMALE` / `MALE` / `ALL`), `conditions[]`, `location` (`country` required, `state` / `city` optional), `healthyVolunteer`, `recruitingOnly` (default true), `maxResults` (≤50)
117- Re-ranks results so studies whose own condition list names a requested condition surface above tangential MeSH-umbrella matches from the upstream fuzzy search
118- Bounds each candidate's locations to the sites matching the requested location (capped by `locationLimit`, ≤500) instead of every registered site, adding one recruiting site when none of the matched ones is open — the one nearest the matched sites by published coordinates (with `distanceMi`), kept to the requested country when a site there recruits, or the first in match order when coordinates are missing
119- `funnel` reports match counts at each filter stage (condition → +location → +demographics) to show where the query narrowed to zero
120- Typed errors: `blank_value`, `rate_limited`
121 
122---
123 
124### `clinicaltrials://{nctId}` <sub>resource</sub>
125 
126- Full protocol record as `application/json`, with locations, secondary/other outcomes, and references each capped at 50 — fixed server-side, no arguments
127- Results data is replaced by `resultsSummary` counts; `truncated` and `filtersApplied` disclose what was capped, with `retrieval` naming the tools that fetch the full data
128- Typed errors: `study_not_found`, `rate_limited`
129 
130---
131 
132### `analyze_trial_landscape` <sub>prompt</sub>
133 
134- Arguments: `topic` required; `focusAreas` (comma-separated) optional
135- Returns one user message pointing the agent at the count, search, field-discovery, and results tools for a data-driven landscape analysis
136 
137## Features
138 
139Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
140 
141ClinicalTrials.gov-specific:
142 
143- Type-safe client for the [ClinicalTrials.gov REST API v2](https://clinicaltrials.gov/data-api/api) — public, no authentication or API keys required
144- Serialized request queue enforcing ClinicalTrials.gov's ~1 req/sec rate limit, with retry and exponential backoff on 429/5xx responses
145- Auto-corrects field names passed to `fields`/`sort` — case/whitespace fixes and known legacy aliases (e.g. `RecruitmentStatus` → `OverallStatus`) — before validating, logging every correction
146- Detects upstream HTML error pages returned with a JSON content-type and retries rather than parsing them as data
147- Geographic proximity search and nearest-site re-ranking, with no geocoding dependency
148 
149Agent-friendly output:
150 
151- Provenance — `clinicaltrials_search_studies` / `clinicaltrials_get_study_count` / `clinicaltrials_find_eligible` echo `searchCriteria` on every call, including `sentinelFilterActive` when the default unknown-enrollment exclusion applies, and `clinicaltrials_get_study_results` names `canonicalNctId` when a previous (alias) ID resolves to a different study
152- Graceful partial failure — `clinicaltrials_get_study_results` returns per-study `fetchErrors` / `studiesWithoutResults` rows instead of failing the whole batch when one ID is malformed or lacks results
153- Discriminated output — typed error `reason` codes per tool (`study_not_found`, `blank_value`, `offset_not_applicable`, …), and bounded lists (`filtersApplied`, `locationSummary`) carry a `next*Offset` only when more remains, so callers branch on presence instead of parsing text
154- Response shaping — `clinicaltrials_search_studies` and `clinicaltrials_find_eligible` return a compact per-study index or location-bounded set by default instead of the ~70KB full record, escalating to full fidelity only via `fields` or `clinicaltrials_get_study_record`
155 
156## Getting started
157 
158### Public Hosted Instance
159 
160A public instance is available at `https://clinicaltrials.caseyjhand.com/mcp` — no installation required. Point any MCP client at it via Streamable HTTP:
161 
162```json
163{
164 "mcpServers": {
165 "clinicaltrialsgov-mcp-server": {
166 "type": "streamable-http",
167 "url": "https://clinicaltrials.caseyjhand.com/mcp"
168 }
169 }
170}
171```
172 
173### Self-Hosted / Local
174 
175Add the following to your MCP client configuration file.
176 
177```json
178{
179 "mcpServers": {
180 "clinicaltrialsgov-mcp-server": {
181 "type": "stdio",
182 "command": "bunx",
183 "args": ["clinicaltrialsgov-mcp-server@latest"],
184 "env": {
185 "MCP_TRANSPORT_TYPE": "stdio",
186 "MCP_LOG_LEVEL": "info"
187 }
188 }
189 }
190}
191```
192 
193Or with npx (no Bun required):
194 
195```json
196{
197 "mcpServers": {
198 "clinicaltrialsgov-mcp-server": {
199 "type": "stdio",
200 "command": "npx",
201 "args": ["-y", "clinicaltrialsgov-mcp-server@latest"],
202 "env": {
203 "MCP_TRANSPORT_TYPE": "stdio",
204 "MCP_LOG_LEVEL": "info"
205 }
206 }
207 }
208}
209```
210 
211Or with Docker:
212 
213```json
214{
215 "mcpServers": {
216 "clinicaltrialsgov-mcp-server": {
217 "type": "stdio",
218 "command": "docker",
219 "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/clinicaltrialsgov-mcp-server:latest"]
220 }
221 }
222}
223```
224 
225For Streamable HTTP, set the transport and start the server:
226 
227```sh
228MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
229# Server listens at http://localhost:3010/mcp
230```
231 
232### Prerequisites
233 
234- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
235 
236### Installation
237 
2381. **Clone the repository:**
239 
240```sh
241git clone https://github.com/cyanheads/clinicaltrialsgov-mcp-server.git
242```
243 
2442. **Navigate into the directory:**
245 
246```sh
247cd clinicaltrialsgov-mcp-server
248```
249 
2503. **Install dependencies:**
251 
252```sh
253bun install
254```
255 
256## Configuration
257 
258All configuration is optional — the server works with defaults and no API keys.
259 
260| Variable | Description | Default |
261|:---|:---|:---|
262| `CT_API_BASE_URL` | ClinicalTrials.gov API base URL. | `https://clinicaltrials.gov/api/v2` |
263| `CT_REQUEST_TIMEOUT_MS` | Per-request timeout in milliseconds. | `30000` |
264| `CT_MAX_PAGE_SIZE` | Maximum page size cap. | `200` |
265| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
266| `MCP_HTTP_PORT` | Port for HTTP server. | `3010` |
267| `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. | `stateless` |
268| `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth`. | `none` |
269| `MCP_LOG_LEVEL` | Log level (RFC 5424). | `info` |
270| `LOGS_DIR` | Directory for log files (Node.js only). | `<project-root>/logs` |
271| `OTEL_ENABLED` | Enable OpenTelemetry tracing. | `false` |
272 
273See [`.env.example`](./.env.example) for the full list of optional overrides.
274 
275## Running the server
276 
277### Local development
278 
279- **Build and run:**
280 
281 ```sh
282 # One-time build
283 bun run rebuild
284 
285 # Run the built server
286 bun run start:http
287 # or
288 bun run start:stdio
289 ```
290 
291- **Run checks and tests:**
292 
293 ```sh
294 bun run devcheck # Lint, format, typecheck, and security audit
295 bun run test # Vitest test suite
296 bun run lint:mcp # Validate MCP definitions against spec
297 ```
298 
299### Docker
300 
301```sh
302docker build -t clinicaltrialsgov-mcp-server .
303docker run --rm -p 3010:3010 clinicaltrialsgov-mcp-server
304```
305 
306The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `/var/log/clinicaltrialsgov-mcp-server`. OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
307 
308## Project structure
309 
310| Directory | Purpose |
311|:---|:---|
312| `src/index.ts` | `createApp()` entry point — registers tools/resources/prompts and inits the ClinicalTrials.gov service. |
313| `src/config` | Server-specific environment variable parsing and validation with Zod. |
314| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). |
315| `src/mcp-server/resources` | Resource definitions (`*.resource.ts`). |
316| `src/mcp-server/prompts` | Prompt definitions (`*.prompt.ts`). |
317| `src/services/clinical-trials` | ClinicalTrials.gov REST API v2 client — retry, rate limiting, field search, types. |
318| `tests/` | Unit and integration tests. |
319 
320## Development guide
321 
322See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
323 
324- Handlers throw, framework catches — no `try/catch` in tool logic
325- Use `ctx.log` for request-scoped logging, no `console` calls
326- Register new tools and resources via the barrels in `src/mcp-server/*/definitions/index.ts`
327- Validate raw API responses, normalize to domain types, and never fabricate missing fields
328 
329## Contributing
330 
331Issues are welcome. Run checks and tests before submitting:
332 
333```sh
334bun run devcheck
335bun run test
336```
337 
338## License
339 
340Apache-2.0 — see [LICENSE](LICENSE) for details.
341 

Discussion

Alternatives