Files of Clinicaltrialsgov MCP server
cyanheads/
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.
Public Hosted Server: https://clinicaltrials.caseyjhand.com/mcp
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
queryplus field-specificconditionQuery/interventionQuery/locationQuery/sponsorQuery/titleQuery/outcomeQuery;statusFilter(case- and separator-insensitive, registry display labels included:"Active, not recruiting"works) /phaseFilterenums,advancedFilter(AREA[FieldName]value/RANGE[min, max]syntax), andgeoFilter(distance(lat,lon,radius)with ami/kmsuffix) 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); passfields(PascalCase leaves) for a full-fidelity projection — full records run ~70KB pageSize1–CT_MAX_PAGE_SIZE(default 10; the cap is 200 unless overridden), cursor pagination viapageToken,sorton up to 2 fields- Excludes the upstream "unknown" enrollment sentinel (
99999999) by default —includeUnknownEnrollmentto include it, or automatically lifted whennctIdsis 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), andnearLocation(lat,lon,radiusMidefault 50) to bound and sort locations; upstream totals reported infiltersAppliedonly when a cap actually trims the list resultsSectionis replaced by compactresultsSummarycounts — fetch full results viaclinicaltrials_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 onlytotalCount— no study data fetched - Excludes the unknown-enrollment sentinel by default (
includeUnknownEnrollmentto 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/formatsinstead of top values; boolean fields reporttrueCount/falseCount multiValuedflags 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,limitup to 100, default 20),drill(dot-notationpathinto a section),overview(top-level sections, no other args) - Resolves the canonical PascalCase field names accepted by
fields,advancedFilter,sort, andclinicaltrials_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
hasResultsis 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 supportsoutcomeLimit(≤100) andadverseEventLimit(≤500), resumable viaoutcomeOffset/seriousEventOffset/otherEventOffsetsectionsfilters tooutcomes,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(countryrequired,state/cityoptional),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 (withdistanceMi), kept to the requested country when a site there recruits, or the first in match order when coordinates are missing funnelreports 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
resultsSummarycounts;truncatedandfiltersApplieddisclose what was capped, withretrievalnaming the tools that fetch the full data - Typed errors:
study_not_found,rate_limited
analyze_trial_landscape prompt
- Arguments:
topicrequired;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_eligibleechosearchCriteriaon every call, includingsentinelFilterActivewhen the default unknown-enrollment exclusion applies, andclinicaltrials_get_study_resultsnamescanonicalNctIdwhen a previous (alias) ID resolves to a different study - Graceful partial failure —
clinicaltrials_get_study_resultsreturns per-studyfetchErrors/studiesWithoutResultsrows instead of failing the whole batch when one ID is malformed or lacks results - Discriminated output — typed error
reasoncodes per tool (study_not_found,blank_value,offset_not_applicable, …), and bounded lists (filtersApplied,locationSummary) carry anext*Offsetonly when more remains, so callers branch on presence instead of parsing text - Response shaping —
clinicaltrials_search_studiesandclinicaltrials_find_eligiblereturn a compact per-study index or location-bounded set by default instead of the ~70KB full record, escalating to full fidelity only viafieldsorclinicaltrials_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
- Bun v1.4.0 or higher (or Node.js v24+).
Installation
- Clone the repository:
git clone https://github.com/cyanheads/clinicaltrialsgov-mcp-server.git
- Navigate into the directory:
cd clinicaltrialsgov-mcp-server
- 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:stdioRun 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/catchin tool logic - Use
ctx.logfor request-scoped logging, noconsolecalls - 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]](./CHANGELOG.md) [![License]](./LICENSE) [![Docker]](https://github.com/users/cyanheads/packages/container/package/clinicaltrialsgov-mcp-server) [![MCP SDK]](https://modelcontextprotocol.io/) [![npm]](https://www.npmjs.com/package/clinicaltrialsgov-mcp-server) [![TypeScript]](https://www.typescriptlang.org/) [![Bun]](https://bun.sh/) |
| 11 | |
| 12 | </div> |
| 13 | |
| 14 | <div align="center"> |
| 15 | |
| 16 | [![Install in Claude Desktop]](https://github.com/cyanheads/clinicaltrialsgov-mcp-server/releases/latest/download/clinicaltrialsgov-mcp-server.mcpb) [![Install in Cursor]](https://cursor.com/en/install-mcp?name=clinicaltrialsgov-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNsaW5pY2FsdHJpYWxzZ292LW1jcC1zZXJ2ZXIiXX0=) [![Install in VS Code]](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://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] |
| 25 | |
| 26 | </div> |
| 27 | |
| 28 | |
| 29 | |
| 30 | ## Overview |
| 31 | |
| 32 | 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. |
| 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 | |
| 139 | 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. |
| 140 | |
| 141 | ClinicalTrials.gov-specific: |
| 142 | |
| 143 | Type-safe client for the [ClinicalTrials.gov REST API v2] — 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 | |
| 149 | Agent-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 | |
| 160 | A public instance is available at `https://clinicaltrials.caseyjhand.com/mcp` — no installation required. Point any MCP client at it via Streamable HTTP: |
| 161 | |
| 162 | |
| 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 | |
| 175 | Add the following to your MCP client configuration file. |
| 176 | |
| 177 | |
| 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 | |
| 193 | Or with npx (no Bun required): |
| 194 | |
| 195 | |
| 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 | |
| 211 | Or with Docker: |
| 212 | |
| 213 | |
| 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 | |
| 225 | For Streamable HTTP, set the transport and start the server: |
| 226 | |
| 227 | |
| 228 | MCP_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] or higher (or Node.js v24+). |
| 235 | |
| 236 | ### Installation |
| 237 | |
| 238 | **Clone the repository:** |
| 239 | |
| 240 | |
| 241 | git clone https://github.com/cyanheads/clinicaltrialsgov-mcp-server.git |
| 242 | |
| 243 | |
| 244 | **Navigate into the directory:** |
| 245 | |
| 246 | |
| 247 | cd clinicaltrialsgov-mcp-server |
| 248 | |
| 249 | |
| 250 | **Install dependencies:** |
| 251 | |
| 252 | |
| 253 | bun install |
| 254 | |
| 255 | |
| 256 | ## Configuration |
| 257 | |
| 258 | All 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 | |
| 273 | See [`.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 | |
| 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 | |
| 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 | |
| 302 | docker build -t clinicaltrialsgov-mcp-server . |
| 303 | docker run --rm -p 3010:3010 clinicaltrialsgov-mcp-server |
| 304 | |
| 305 | |
| 306 | 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. |
| 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 | |
| 322 | See [`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 | |
| 331 | Issues are welcome. Run checks and tests before submitting: |
| 332 | |
| 333 | |
| 334 | bun run devcheck |
| 335 | bun run test |
| 336 | |
| 337 | |
| 338 | ## License |
| 339 | |
| 340 | Apache-2.0 — see [LICENSE] for details. |
| 341 |
Discussion
Alternatives
Browse more free AI agents or everything in Development.