About clinicaltrialsgov-mcp-server
clinicaltrialsgov-mcp-server exposes ClinicalTrials.gov search and study data to MCP clients, including results and candidate-trial matching by patient profile. It suits clinical-trial discovery and landscape analysis, with field discovery and compact responses alongside detailed records.
Key features
- 01Search studies with query and filters
- 02Retrieve protocols and posted results
- 03Inspect field definitions and values
- 04Match profiles to recruiting trials
- 05Support geographic site searches
Use cases
- 01Find trials by condition, phase, or location
- 02Review completed-study outcomes and adverse events
- 03Explore study counts and registry fields
- 04Identify candidate trials for a patient profile
MCP support
Provides seven tools, one JSON resource, and an analysis prompt to MCP clients. Supports stdio and Streamable HTTP transports.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://clinicaltrials.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-clinicaltrialsgov https://clinicaltrials.caseyjhand.com/mcp{"mcpServers":{"cyanheads-clinicaltrialsgov":{"type":"http","url":"https://clinicaltrials.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-clinicaltrialsgov":{"type":"http","url":"https://clinicaltrials.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-clinicaltrialsgov":{"type":"http","url":"https://clinicaltrials.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-clinicaltrialsgov":{"serverUrl":"https://clinicaltrials.caseyjhand.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
7| Parameter | Type | Required | Description |
|---|---|---|---|
| sort | string | No | Sort order. Format: FieldName:asc or FieldName:desc. E.g., "LastUpdatePostDate:desc", "EnrollmentCount:desc". Max 2 fields comma-separated. For "largest trials" queries, pair EnrollmentCount:desc with advancedFilter "AREA[StudyType]INTERVENTIONAL" — the top enrollment counts are observational registry/claims studies enrolling tens of millions. Enrollment counts are sponsor-reported and not validated upstream beyond the unknown-enrollment sentinel exclusion. Use clinicaltrials_get_field_definitions to find sortable field names. |
| query | string | No | General free-text search across all fields. Runs the 57-field relevance search ClinicalTrials.gov publishes for this parameter — NCTId, NCTIdAlias, OrgStudyId, SecondaryId, Acronym, BriefTitle, OfficialTitle, Condition, InterventionName, InterventionOtherName, Phase, StdAge, StudyType, BriefSummary, outcome measures and their descriptions, LeadSponsorName, CollaboratorName, the Location* fields, the Design* fields, and the ConditionAncestorTerm/InterventionAncestorTerm MeSH umbrellas — so a hit need not carry your term in the field you had in mind. Plain words plus AND, OR, NOT. `[ ]` are valid only inside an AREA[FieldName]value or RANGE[min, max] expression — those work here as well as in advancedFilter, so AREA[Phase]PHASE2 is accepted in this parameter; a stray bracket fails. `( )` group sub-expressions and work when matched; `,` acts as AND. The dedicated *Query parameters (conditionQuery, interventionQuery, etc.) scope a search to one field. |
| fields | array | No | PascalCase leaf names to return; strongly recommended since full records are ~70KB. Omit for the compact index projection — an empty list is rejected, not treated as omission. A comma- or pipe-joined entry ("NCTId,BriefTitle") is split into separate names. Common leaves: NCTId, BriefTitle, BriefSummary, OverallStatus, Phase, LeadSponsorName, Condition. Call clinicaltrials_get_field_definitions with a concept query (e.g., "adverse events", "eligibility") to find the exact leaf for any concept. |
| nctIds | any | No | Filter to specific NCT IDs for batch lookups. Omit to search every study — an empty list is rejected, not treated as "no filter". Supplying this lifts the default unknown-enrollment exclusion, so an ID you name is never filtered out of its own lookup. |
| pageSize | integer | No | Results per page, 1–200. |
| geoFilter | string | No | Geographic proximity filter. Format: distance(lat,lon,radius), where radius carries a `mi` or `km` suffix — e.g. "distance(47.6062,-122.3321,50mi)" for studies within 50 miles of Seattle. The suffix is required: a radius with no unit is rejected, as are a non-positive radius, a latitude outside [-90, 90], and a longitude outside [-180, 180]. When set, each study's locations are re-sorted by proximity to the center so the nearest matched site leads, annotated with its distance in miles; the full location list is preserved. |
| pageToken | string | No | Pagination cursor: a previous response's nextPageToken, passed back unchanged with the same criteria. Tokens expire when ClinicalTrials.gov refreshes its data. |
| countTotal | boolean | No | Include total study count in response. Only computed on the first page. |
| titleQuery | string | No | Search within study titles and acronyms only. Matches Acronym, BriefTitle, and OfficialTitle. Plain words plus AND/OR/NOT. `[ ]` are valid only inside an AREA[FieldName]value or RANGE[min, max] expression, which this parameter accepts; a stray bracket fails. `( )` group sub-expressions when matched; `,` acts as AND. |
| phaseFilter | any | No | Filter by trial phase. Omit to search all phases — an empty list is rejected, not treated as "no filter". Values: EARLY_PHASE1, PHASE1, PHASE2, PHASE3, PHASE4, NA. Case and spacing variants ("Phase 3", "early phase 1", "Not Applicable") are accepted, and a list or a comma- or pipe-joined string matches any of the phases. Each entry must name a phase — put search expressions (OR, NOT, AREA[…]) in advancedFilter. |
| outcomeQuery | string | No | Search within outcome measure fields. Matches PrimaryOutcomeMeasure, SecondaryOutcomeMeasure, OtherOutcomeMeasure, and OutcomeMeasureTitle, plus their description counterparts PrimaryOutcomeDescription, SecondaryOutcomeDescription, OtherOutcomeDescription, OutcomeMeasureDescription, and OutcomeMeasurePopulationDescription — so a term appearing only in outcome prose still matches. Plain words plus AND/OR/NOT. `[ ]` are valid only inside an AREA[FieldName]value or RANGE[min, max] expression, which this parameter accepts; a stray bracket fails. `( )` group sub-expressions when matched; `,` acts as AND. |
| sponsorQuery | string | No | Sponsor/collaborator name search. Matches LeadSponsorName, CollaboratorName, and OrgFullName. Plain words plus AND/OR/NOT. `[ ]` are valid only inside an AREA[FieldName]value or RANGE[min, max] expression, which this parameter accepts; a stray bracket fails. `( )` group sub-expressions when matched; `,` acts as AND. |
| statusFilter | any | No | Filter by study status. Omit to search all statuses — an empty list is rejected, not treated as "no filter". Values: RECRUITING, COMPLETED, ACTIVE_NOT_RECRUITING, NOT_YET_RECRUITING, ENROLLING_BY_INVITATION, SUSPENDED, TERMINATED, WITHDRAWN, UNKNOWN, WITHHELD, NO_LONGER_AVAILABLE, AVAILABLE, APPROVED_FOR_MARKETING, TEMPORARILY_NOT_AVAILABLE. |
| locationQuery | string | No | Location search — city, state, country, or facility name. Matches LocationCity, LocationState, LocationCountry, LocationFacility, and LocationZip; a study matches when any of its sites does. Plain words plus AND/OR/NOT. `[ ]` are valid only inside an AREA[FieldName]value or RANGE[min, max] expression, which this parameter accepts; a stray bracket fails. `( )` group sub-expressions when matched; `,` acts as AND. |
| advancedFilter | string | No | Advanced filter using AREA[FieldName]value syntax. Examples: "AREA[StudyType]INTERVENTIONAL", "AREA[EnrollmentCount]RANGE[100, 1000]", "AREA[Phase]PHASE2 AND AREA[StudyType]INTERVENTIONAL", "(AREA[Phase]PHASE3 OR AREA[Phase]PHASE4) AND AREA[StudyType]INTERVENTIONAL". "AREA[HasResults]true" restricts to studies with posted results. AND/OR/NOT join complete AREA[FieldName]value expressions; parentheses group them. Call clinicaltrials_get_field_definitions to find AREA[]-compatible field names. |
| conditionQuery | string | No | Condition/disease-specific search. E.g., "Type 2 Diabetes", "non-small cell lung cancer". Matches Condition, BriefTitle, OfficialTitle, ConditionMeshTerm, ConditionAncestorTerm, Keyword, and NCTId. ConditionAncestorTerm is the MeSH umbrella above the conditions a study itself lists, so results run broader than those lists — a study can match a parent term it never names. Plain words plus AND/OR/NOT. `[ ]` are valid only inside an AREA[FieldName]value or RANGE[min, max] expression, which this parameter accepts; a stray bracket fails. `( )` group sub-expressions when matched; `,` acts as AND. |
| interventionQuery | string | No | Intervention/treatment search. E.g., "pembrolizumab", "cognitive behavioral therapy". Matches InterventionName, InterventionType, ArmGroupType, InterventionOtherName, BriefTitle, OfficialTitle, ArmGroupLabel, InterventionMeshTerm, Keyword, InterventionAncestorTerm, InterventionDescription, and ArmGroupDescription. InterventionAncestorTerm is the MeSH umbrella above the interventions a study itself lists, so results run broader than those lists. Plain words plus AND/OR/NOT. `[ ]` are valid only inside an AREA[FieldName]value or RANGE[min, max] expression, which this parameter accepts; a stray bracket fails. `( )` group sub-expressions when matched; `,` acts as AND. |
| includeUnknownEnrollment | boolean | No | Include studies whose EnrollmentCount is the upstream "unknown" sentinel (99999999). Excluded by default — the sentinel pollutes RANGE[N, MAX] queries and EnrollmentCount:desc sorts. Set true for data-quality audits or when targeting unknown-enrollment studies specifically. |
Details
From the project READMEclinicaltrialsgov-mcp-server
Search ClinicalTrials.gov trials, retrieve study details and results, and match patients to candidate trials via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://clinicaltrials.caseyjhand.com/mcp
Overview
Search 600K+ studies on ClinicalTrials.gov, the US National Library of Medicine's trial registry. Fetch full study records and posted results, discover fields and valid values, and match a patient's profile to recruiting trials. No API key required.
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 candidate recruiting trials, without checking inclusion/exclusion criteria |
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/phaseFilterenums (case and spacing variants accepted),advancedFilter(AREA[FieldName]value/RANGE[min, max]), andgeoFilter(distance(lat,lon,radius)with ami/kmsuffix);pageSize1–1000 (default 10; above the server's per-page cap, 200 by default, a page is served at the cap withtruncated/shown/cap),pageTokencursor,sorton up to 2 fields - Returns a compact per-study index by default (
nctId,briefTitle,overallStatus,phases,enrollmentCount,hasResults, …);fields(PascalCase leaves) selects a full-fidelity projection — full records run ~70KB - Typed errors:
blank_value,ids_not_found,field_invalid,enum_invalid,query_parse_error,geo_invalid,sort_invalid,page_token_invalid,rate_limited
clinicaltrials_get_study_record tool
- Full protocol record by
nctId, with optionallocationLimit,outcomeLimit/referenceLimit, andnearLocation(lat,lon,radiusMi) to bound and sort locations — none has an upper bound; a value past the list returns all of it filtersAppliedreports upstream totals when a cap trims a list;resultsSectionis replaced byresultsSummarycounts. Typed errors:study_not_found,rate_limited
clinicaltrials_get_study_count tool
- Same query and filter surface as
clinicaltrials_search_studies, returning onlytotalCount— no study data fetched - Typed errors:
blank_value,field_invalid,enum_invalid,query_parse_error,rate_limited
clinicaltrials_get_field_values tool
- One or more PascalCase
fields(e.g.OverallStatus,Phase) — returns each field's type and top values with study counts (capped at 250 by the API), ormin/max/avgfor numeric and date fields andtrueCount/falseCountfor booleans - STRING fields add their
longestvalue; past 2,000 characters it is a preview flaggedtruncated, with the fulllengthand a notice naming theclinicaltrials_search_studiescall that returns the whole text. Free-text fields the API does not count distinct values on carryuniqueValuesCounted: false, marking theiruniqueValuesCountof 0 as no count multiValuedflags fields whose per-value counts can sum above the study total. Typed errors:blank_value,field_invalid,rate_limited
clinicaltrials_get_field_definitions tool
mode:search(keywordquery,limitdefault 20, no upper bound),drill(dot-notationpath), oroverview— resolves the PascalCase names accepted byfields,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 distinct
nctIdsfetched per call, with any further IDs (up to 500 entries) returned indeferredNctIds;sections(outcomes,adverseEvents,participantFlow,baseline,moreInfo),summaryto condense a result set that can exceed 500KB per study, andoutcomeLimit/adverseEventLimitwith resumableoutcomeOffset/seriousEventOffset/otherEventOffset - Per-study
fetchErrors/studiesWithoutResultsinstead of a whole-batch failure, andcanonicalNctIdwhen an alias ID resolves to another study. Typed errors:blank_value,offset_not_applicable,rate_limited
clinicaltrials_find_eligible tool
age(whole number 0–120) with optionalageUnit(yearsdefault,months,weeks,days) for infants and young children,sex(FEMALE/MALE/ALL),conditions[],location(countryrequired,state/cityoptional),healthyVolunteer,recruitingOnly(default true),maxResults(up to 1000; each call returns at most 50, disclosing the rest viatruncated/shown/capand a replayablesearchCriteria),locationLimit(no upper bound; a value past the matched sites returns them all)- Matches on age, sex, location, recruitment status, and a fuzzy condition search;
funnelreports match counts per filter stage, and anoticesays inclusion/exclusion criteria were not checked (useclinicaltrials_get_study_recordto review them) - 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 and HTML error pages
- Auto-corrects field names passed to
fields/sort(case, whitespace, legacy aliases likeRecruitmentStatus→OverallStatus) and logs each correction - Geographic proximity search and nearest-site re-ranking, with no geocoding dependency
- Excludes the "unknown" enrollment sentinel (
99999999) from searches and counts by default;includeUnknownEnrollmentbrings it back
Agent-friendly output:
- Provenance — search, count, and eligibility tools echo
searchCriteria;clinicaltrials_get_study_resultsnamescanonicalNctIdwhen an alias ID resolves to another study - Graceful partial failure —
clinicaltrials_get_study_resultsreturns per-studyfetchErrors/studiesWithoutResultsinstead of failing the whole batch - Typed errors — each tool returns
reasoncodes (study_not_found,blank_value, …), and bounded lists carry anext*Offsetonly when more remains - Response shaping — search and eligibility return a compact index by default instead of the ~70KB full 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; a positive integer. | 30000 |
CT_MAX_PAGE_SIZE | Most studies one clinicaltrials_search_studies page returns; an integer ≥ 1, values above 1000 clamp to 1000. | 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 |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP base URL for traces and metrics. | — |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | OTLP endpoint for log export. | — |
LOG_TOOL_FAILURE_PAYLOADS | Log each failed tool call's arguments and result, redacted by key name and capped at LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES (default 16384). A secret inside a free-form value is not redacted. | 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/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.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.