Skip to main content
Glama
Sairajiv-13

Clinical Trials MCP Server

by Sairajiv-13

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.

CI Python License MCP

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

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 nested protocolSection modules 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

condition

string, optional

Condition or disease, matched by the API's own search

intervention

string, optional

Drug, device, or procedure name

location

string, optional

City, state, or country text matched against study sites

phases

list of string, optional

For example ["Phase 2", "Phase 3"]

statuses

list of string, optional

For example ["RECRUITING"]

page_size

int

1 to 50, default 10

page_token

string, optional

From a previous result's next_page_token

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

nct_id

string, required

For example NCT04354155

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

nct_id

string, required

Study to check against

age

number, optional

Patient age in years (0.5 means 6 months)

sex

string, optional

female or male

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

nct_id

string, required

Study to list sites for

city

string, optional

Only sites in this city

state

string, optional

Only sites in this state or region

country

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 stdio

make 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-server

The 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.py

Configuration

All configuration is through environment variables. Defaults work against the public API with nothing set. See .env.example.

Variable

Default

Meaning

CT_MCP_BASE_URL

https://clinicaltrials.gov/api/v2

API base URL

CT_MCP_TIMEOUT_SECONDS

15

Per-request timeout

CT_MCP_CACHE_TTL_SECONDS

300

How long a successful GET is cached; 0 disables the cache

CT_MCP_MAX_RETRIES

2

Extra attempts after a 5xx response or a network error

Testing

make test

The 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 meets verdict 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 tools
eligibility_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.

ParametersJSON Schema
NameRequiredDescriptionDefault
ageNoPatient age in years. Fractional values are fine (0.5 = 6 months).
sexNoPatient sex: 'female' or 'male'.
nct_idYesNCT number, e.g. 'NCT04354155'.

Output Schema

ParametersJSON Schema
NameRequiredDescription
noteNo
checksNo
nct_idYes
verdictYes
criteria_textNoFull free-text criteria from the study, for human review.

TDQS

A4.1/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
nct_idYesNCT number, e.g. 'NCT04354155'.

Output Schema

ParametersJSON Schema
NameRequiredDescription
nct_idYes
phasesNo
statusYes
conditionsNo
start_dateNo
brief_titleYes
eligibilityNo
lead_sponsorNo
brief_summaryNo
collaboratorsNo
interventionsNo
official_titleNo
completion_dateNo
locations_countNo
enrollment_countNo
last_update_postedNo
primary_completion_dateNo
recruiting_locations_countNo

TDQS

A3.7/5.0
Behavior3/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines3/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
cityNoOnly sites in this city.
stateNoOnly sites in this state or region.
nct_idYesNCT number, e.g. 'NCT04354155'.
countryNoOnly sites in this country.

Output Schema

ParametersJSON Schema
NameRequiredDescription
sitesNo
nct_idYes
study_statusYes
sites_returnedYes
total_sites_listedYes

TDQS

A3.5/5.0
Behavior3/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
phasesNoPhase filter, e.g. ['Phase 2', 'Phase 3']. Omit for all phases.
locationNoCity, state, or country text matched against study sites.
statusesNoStatus filter, e.g. ['RECRUITING']. Other values include NOT_YET_RECRUITING, ACTIVE_NOT_RECRUITING, ENROLLING_BY_INVITATION, COMPLETED, SUSPENDED, TERMINATED, WITHDRAWN.
conditionNoCondition or disease, e.g. 'type 2 diabetes'.
page_sizeNoResults per page, 1 to 50.
page_tokenNoToken from a previous result's next_page_token, for paging.
interventionNoDrug, device, or procedure name, e.g. 'metformin'.

Output Schema

ParametersJSON Schema
NameRequiredDescription
trialsNo
page_sizeYes
total_countNoTotal studies matching the query, when the API reports it.
returned_countYes
next_page_tokenNoPass back as page_token to fetch the next page; null on the last page.

TDQS

A4/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines4/5

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.

  1. 4 tool updatesv0.1.0
    • First observedeligibility_check
    • First observedget_trial
    • First observedrecruiting_sites
    • First observedsearch_trials

TDQS

A3.9/5.0

Scored across 4 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count4/5

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.

Completeness4/5

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers