Clinical Trials MCP Server
This MCP server gives an LLM structured, keyless access to the public ClinicalTrials.gov API v2 through four typed tools.
search_trials— Find studies by condition, intervention, location, phase, or status; paginated (1–50 per page) with total count and a next-page token.get_trial— Fetch one study by NCT id in a normalized shape: titles, status, phases, conditions, interventions, eligibility summary with full criteria text, location counts, sponsor, enrollment, and key dates.eligibility_check— Check a patient's age and sex against a study's structured criteria, returningmeets/does_not_meet/cannot_determinewith a plain-language reason per check, plus the unparsed free-text criteria for human review.recruiting_sites— List a study's actively recruiting sites, filterable by city, state, or country, with facility and contact details where the registry provides them.
Caveats: only age and sex are evaluated; free-text criteria are never interpreted, and the verdict is not a final eligibility decision or medical advice.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Clinical Trials MCP ServerFind recruiting Phase 3 trials for breast cancer in Texas"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Clinical Trials MCP Server
An MCP (Model Context Protocol) server that gives an LLM structured access to the public ClinicalTrials.gov API v2. It wraps the registry in four typed tools: search studies, read one study in a normalized shape, check the deterministic part of a study's eligibility criteria for a patient, and list the sites that are actively recruiting. The registry needs no API key, and neither does this server.
Problem
Clinical trial data is public and rich, but it sits behind a registry API that models cannot use directly. Eligibility is the risky part: a tool that guesses at criteria it cannot evaluate can mislead a patient or a researcher.
Related MCP server: Clinical Trials MCP Server
Approach
A FastMCP server over the public ClinicalTrials.gov API v2, with four tools: search trials, fetch one trial, check eligibility from a patient profile, and list recruiting sites. Eligibility is split on purpose. Deterministic fields (age, sex) are decided by the server. The inclusion and exclusion text is returned for the calling model to judge, and the server says which is which. The server runs over stdio for local clients and streamable HTTP for hosted use, with a short TTL cache on upstream calls.
Results
73 tests, 92 percent coverage, ruff clean, green CI
Released as v1.0.0, with a container image published to GHCR: https://github.com/users/Sairajiv-13/packages/container/package/clinical-trials-mcp-server
No API key is needed at any point; the upstream API is public
Tech stack
Python, FastMCP, httpx, pydantic, Docker, GitHub Actions
Architecture
The tool layer is thin. Each tool builds query parameters, calls one async client, and parses the response into a pydantic model. Everything except server.py is plain Python with no framework dependency, so the parsing and eligibility logic can be tested without starting a server.
flowchart LR
Host["MCP host (LLM)"] -->|stdio or HTTP| Server["FastMCP server<br/>server.py"]
Server --> Client["TrialsClient<br/>client.py"]
Client --> Cache[("In-memory TTL cache")]
Client -->|HTTPS, retries on 5xx| API["ClinicalTrials.gov<br/>API v2"]
Server --> Models["Parsers and models<br/>models.py"]
Server --> Elig["Eligibility checks<br/>eligibility.py"]client.py: async httpx client with a timeout, bounded retries on 5xx and network errors, and a small in-memory TTL cache. Upstream failures become typed errors with short messages written for the calling model.models.py: pydantic output models and the parsers that flatten the API's nestedprotocolSectionmodules into them.eligibility.py: age and sex checks only. See the design decisions below for why the scope stops there.errors.py: the error types the client raises, kept separate so tool code never formats raw HTTP failures.
Tools
search_trials
Search the registry. At least one filter should be given; results come back with a total count and a page token for the next page. Page size is capped at 50 to keep responses small enough for a model context.
Input | Type | Notes |
| string, optional | Condition or disease, matched by the API's own search |
| string, optional | Drug, device, or procedure name |
| string, optional | City, state, or country text matched against study sites |
| list of string, optional | For example |
| list of string, optional | For example |
| int | 1 to 50, default 10 |
| string, optional | From a previous result's |
Output: total_count, returned_count, page_size, next_page_token, and trials, where each trial has an NCT id, title, status, phases, conditions, intervention names, lead sponsor, and key dates.
get_trial
Fetch one study by NCT number and return it in a normalized, readable shape.
Input | Type | Notes |
| string, required | For example |
Output: titles, status, a short summary, phases, conditions, interventions with types and descriptions, an eligibility summary including the full criteria text, total and recruiting location counts, lead sponsor and collaborators, enrollment, and start, completion, and last update dates.
eligibility_check
Check the deterministic eligibility criteria of a study for one patient.
Input | Type | Notes |
| string, required | Study to check against |
| number, optional | Patient age in years (0.5 means 6 months) |
| string, optional |
|
Output: a verdict of meets, does_not_meet, or cannot_determine, one result per check with a plain-language reason, and the study's full free-text criteria for human review. Only age and sex are checked, because those are the fields the registry reports in structured form. The verdict is not a final eligibility decision.
recruiting_sites
List the sites of a study that are currently recruiting, with contact details where the registry provides them.
Input | Type | Notes |
| string, required | Study to list sites for |
| string, optional | Only sites in this city |
| string, optional | Only sites in this state or region |
| string, optional | Only sites in this country |
Output: the study status, how many sites the registry lists in total, and the matching sites with facility, city, state, country, status, and contact name, phone, and email where listed. A site with no status of its own counts as recruiting when the study overall is recruiting, which matches how the registry presents it.
Quickstart
Install and run locally (Python 3.11 or newer):
git clone https://github.com/Sairajiv-13/clinical-trials-mcp-server.git
cd clinical-trials-mcp-server
make setup
make test # run the offline test suite
make run # serve over stdiomake run starts the server on stdio. To connect a desktop MCP host, point it at the installed command:
{
"mcpServers": {
"clinical-trials": {
"command": "/path/to/clinical-trials-mcp-server/.venv/bin/clinical-trials-mcp"
}
}
}Run it over HTTP with Docker instead:
docker build -t clinical-trials-mcp-server .
docker run --rm -p 8000:8000 clinical-trials-mcp-serverThe HTTP server listens on port 8000 at the /mcp path. Without Docker, make run-http does the same on 127.0.0.1.
A minimal client that lists the tools and runs one search lives in examples/minimal_client.py:
.venv/bin/python examples/minimal_client.pyConfiguration
All configuration is through environment variables. Defaults work against the public API with nothing set. See .env.example.
Variable | Default | Meaning |
|
| API base URL |
|
| Per-request timeout |
|
| How long a successful GET is cached; 0 disables the cache |
|
| Extra attempts after a 5xx response or a network error |
Testing
make testThe suite runs fully offline. HTTP calls are mocked with respx against response fixtures built from the documented API v2 schema, so no test needs network access or the live registry. The suite currently has 73 tests at 92% coverage of the package, covering search normalization and the page size cap, study parsing, the eligibility cases (below minimum, above maximum, sex mismatch, missing data), site filtering, and the failure paths (upstream 500s, retries, timeouts, unknown NCT numbers).
Design decisions
Two architecture decision records live in docs/adr:
ADR 0001: why FastMCP, and why the server ships both stdio and HTTP transports.
ADR 0002: why eligibility checks stop at the structured fields and return the free-text criteria for human review instead of trying to interpret them.
Limitations
Eligibility free text is returned, never parsed. Diagnosis requirements, lab thresholds, and prior treatment rules are in that text, so a
meetsverdict means the patient passes the age and sex fields only.Registry data is maintained by sponsors and can lag behind a study's actual state. Site recruiting status in particular changes without notice.
The ClinicalTrials.gov API is a public service with rate limits and no uptime commitment. The client's retries and cache reduce load, but sustained heavy use should be discussed with the registry, not worked around.
This server reads public registry data only. It does not handle protected health information, and the tools are not a substitute for a study team's eligibility review. Nothing here is medical advice.
License
MIT. Copyright (c) 2026 Sai Rajiv Nagula. See LICENSE.
Available Tools
4 toolseligibility_checkEligibility CheckA
Check the deterministic eligibility criteria of a study for a patient.
Only age range and sex are checked, because those are the fields the registry reports in structured form. The verdict is 'meets', 'does_not_meet', or 'cannot_determine', with a reason per check. The full free-text criteria are included in the result for human review; they are never interpreted by this tool, and the verdict is not a final eligibility decision.
| Name | Required | Description | Default |
|---|---|---|---|
| age | No | Patient age in years. Fractional values are fine (0.5 = 6 months). | |
| sex | No | Patient sex: 'female' or 'male'. | |
| nct_id | Yes | NCT number, e.g. 'NCT04354155'. |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| checks | No | |
| nct_id | Yes | |
| verdict | Yes | |
| criteria_text | No | Full free-text criteria from the study, for human review. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does it well: it discloses the exact verdict vocabulary ('meets', 'does_not_meet', 'cannot_determine'), that each check carries a reason, that free-text criteria are returned but never interpreted, and that the result is not a final decision. What it omits is behavior when age/sex are left null (presumably 'cannot_determine') and any failure/error behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four compact sentences, front-loaded with purpose and scope, then verdict semantics, then the disclaimer. Nothing is padded, though the closing clause about free-text criteria and non-finality could be tightened slightly.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return-value shape need not be re-described, and the description still adds the crucial verdict semantics and interpretation limits that the schema cannot express. It is complete for correct invocation except for the unspecified behavior when age and sex are omitted.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already defines age (years, fractional allowed) and sex ('female'/'male'), establishing the baseline of 3. The description adds a mild semantic link by stating these are the only two fields actually checked, but gives no guidance on omitting them or on how null values are treated.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Specific verb+resource ('Check the deterministic eligibility criteria of a study for a patient') with explicit scope narrowing: only age range and sex are evaluated. This is unmistakably distinct from siblings search_trials, get_trial, and recruiting_sites, so an agent can route correctly without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It clearly establishes the context of use — deterministic age/sex screening against a study, with the verdict explicitly not a final eligibility decision — which tells the agent this is a preliminary screening step. It never names an alternative tool or states a when-not condition, so it stops short of full routing guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_trialGet TrialA
Get one study by NCT number, normalized into a readable structure.
Returns titles, status, phases, conditions, interventions, an eligibility summary (including the full criteria text), location counts, sponsor, enrollment, and key dates.
| Name | Required | Description | Default |
|---|---|---|---|
| nct_id | Yes | NCT number, e.g. 'NCT04354155'. |
Output Schema
| Name | Required | Description |
|---|---|---|
| nct_id | Yes | |
| phases | No | |
| status | Yes | |
| conditions | No | |
| start_date | No | |
| brief_title | Yes | |
| eligibility | No | |
| lead_sponsor | No | |
| brief_summary | No | |
| collaborators | No | |
| interventions | No | |
| official_title | No | |
| completion_date | No | |
| locations_count | No | |
| enrollment_count | No | |
| last_update_posted | No | |
| primary_completion_date | No | |
| recruiting_locations_count | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden. 'Get' plus the field inventory makes the read-only, single-record nature clear, and it enumerates what is returned, but it says nothing about failure behavior (invalid or unknown NCT number) or any rate limits.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the core purpose and then a compact inventory of returned content. The return-field list is somewhat redundant given an output schema exists, but it is not wasteful enough to hurt readability.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, the description need not explain return values, yet it still names the high-value categories, and the single required parameter is fully covered by the schema. Only the absence of error/not-found behavior keeps it short of complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Only one parameter, and schema description coverage is 100% — the schema already documents nct_id with an example format. The description's 'by NCT number' adds no syntax or semantics beyond that, so the baseline 3 for schema-covered parameters applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb ('Get') and resource ('one study by NCT number') plus the transformation applied ('normalized into a readable structure'). The singular, identifier-keyed nature of the lookup implicitly separates it from the collection-oriented sibling search_trials.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The identifier requirement ('by NCT number') implies the usage context — fetch one known trial — but there is no explicit when-to-use guidance, no statement about when to prefer get_trial over search_trials or eligibility_check, and no prerequisites noted.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
recruiting_sitesRecruiting SitesA
List the sites of a study that are currently recruiting.
A site counts as recruiting when the registry gives it a RECRUITING status, or when it has no status of its own and the study overall is RECRUITING. Contact details are included where the registry lists them for a site.
| Name | Required | Description | Default |
|---|---|---|---|
| city | No | Only sites in this city. | |
| state | No | Only sites in this state or region. | |
| nct_id | Yes | NCT number, e.g. 'NCT04354155'. | |
| country | No | Only sites in this country. |
Output Schema
| Name | Required | Description |
|---|---|---|
| sites | No | |
| nct_id | Yes | |
| study_status | Yes | |
| sites_returned | Yes | |
| total_sites_listed | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full disclosure burden. It does add real value by defining the inclusion rule -- a site qualifies on its own RECRUITING status, or when it has no status and the study is RECRUITING -- but it says nothing about permissions, result limits, or whether contact details can be absent vs. empty.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core action in the first sentence, and the second sentence earns its place by pinning down the non-obvious 'recruiting' predicate. Slightly longer than strictly necessary for a list tool, but no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return structure need not be explained; the description still usefully flags that contact details appear only where the registry supplies them. Combined with the recruiting definition, an agent has enough to call this correctly, with only permission/limit behavior left unstated.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, with each filter (city/state/country) and the required nct_id documented inline, so the baseline is 3. The description adds no syntax, matching behavior, or filter-combination detail beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('List the sites of a study') plus a precise scope qualifier ('currently recruiting'). The resource (sites of a study) is naturally distinct from siblings search_trials/get_trial, which operate on trials themselves, though the description never names an alternative to force the distinction.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the geography filters in the schema (city/state/country) and the recruiting constraint, so an agent can infer this is the tool for enumerating study locations. There is no explicit when-to-use/when-not guidance, nor any mention of when another sibling (e.g. get_trial) would be preferable.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_trialsSearch TrialsA
Search ClinicalTrials.gov for studies matching the given filters.
At least one filter should be supplied; an unfiltered search returns the whole registry one page at a time. Ordering is whatever the API returns for the query; this tool does not re-sort.
| Name | Required | Description | Default |
|---|---|---|---|
| phases | No | Phase filter, e.g. ['Phase 2', 'Phase 3']. Omit for all phases. | |
| location | No | City, state, or country text matched against study sites. | |
| statuses | No | Status filter, e.g. ['RECRUITING']. Other values include NOT_YET_RECRUITING, ACTIVE_NOT_RECRUITING, ENROLLING_BY_INVITATION, COMPLETED, SUSPENDED, TERMINATED, WITHDRAWN. | |
| condition | No | Condition or disease, e.g. 'type 2 diabetes'. | |
| page_size | No | Results per page, 1 to 50. | |
| page_token | No | Token from a previous result's next_page_token, for paging. | |
| intervention | No | Drug, device, or procedure name, e.g. 'metformin'. |
Output Schema
| Name | Required | Description |
|---|---|---|
| trials | No | |
| page_size | Yes | |
| total_count | No | Total studies matching the query, when the API reports it. |
| returned_count | Yes | |
| next_page_token | No | Pass back as page_token to fetch the next page; null on the last page. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the burden and does disclose meaningful behavior: results come back one page at a time, ordering follows the API and is not re-sorted, and unfiltered queries are expensive. It omits result shape (mitigated by the output schema) and any auth/rate-limit notes.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, front-loaded with what it does, then the filter requirement, then the ordering caveat. Nothing is redundant or padded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 7-parameter read tool with an output schema, the description covers the essentials: filter requirement, pagination via pages, and ordering. It stops short of pointing at siblings for single-trial or site lookups, a minor gap given the schema handles return values.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so every filter, including enum-like statuses and page_size bounds, is already documented. The description adds no syntax or format detail beyond the schema, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Search ClinicalTrials.gov for studies') plus the mechanism (matching given filters). It is clearly distinct from get_trial, but the description never names or contrasts those siblings explicitly, so it lands at clear-but-undifferentiated.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives a concrete usage rule: supply at least one filter, because an unfiltered search returns the whole registry page by page. That is real when-to-use context, but no alternative tool (e.g. get_trial for a known ID) is named to route the agent elsewhere.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
4 tool updates
v0.1.0- First observed
eligibility_check - First observed
get_trial - First observed
recruiting_sites - First observed
search_trials
TDQS
Scored across 4 tools
Each tool targets a clearly distinct operation: discovery (search_trials), single-record retrieval (get_trial), structured eligibility verdict (eligibility_check), and site listings (recruiting_sites). The search-vs-get boundary is explicit (filters vs NCT number), so an agent can select unambiguously.
search_trials and get_trial follow a clean verb_noun pattern, but eligibility_check and recruiting_sites are noun-heavy phrases rather than verb-led. All are snake_case and readable, with only a minor stylistic deviation.
Four tools is a focused, cohesive set for a read-only clinical trials lookup server, with no redundant or filler tools. It is slightly thin—no location- or intervention-based filtering beyond the search tool's filters—but each tool earns its place.
The surface covers the natural read-only lifecycle: find studies, inspect a study, check structured eligibility, and locate recruiting sites. Since the registry is public and read-only, the absence of write operations is correct; only minor gaps (e.g. no multi-trial comparison) remain.
Maintenance
Related MCP Connectors
Clinical trial search and status from ClinicalTrials.gov
Search ClinicalTrials.gov: find studies, retrieve results, match patients to candidate trials.
Search and read ClinicalTrials.gov studies, sites and outcomes.
Provide structured access to ClinicalTrials.gov data for searching, retrieving, and analyzing clin…
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables searching and retrieving information from the ClinicalTrials.gov database of over 400,000 clinical studies, including trial details, eligibility criteria, locations, and results across 220+ countries.5-
- AlicenseBqualityDmaintenanceProvides programmatic access to ClinicalTrials.gov API with 18 specialized tools for searching, analyzing, and retrieving detailed information about 400,000+ clinical trials worldwide, including filtering by condition, location, phase, sponsor, eligibility criteria, and outcomes.1710 npm4MIT
- FlicenseNot gradedqualityDmaintenanceEnables searching and retrieving detailed clinical trial information from ClinicalTrials.gov via the official API.1-
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to search and retrieve clinical trial data from ClinicalTrials.gov, supporting filters, study details, metadata, and statistics.-