MCP Clinical Research Server
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., "@MCP Clinical Research ServerSearch ClinicalTrials.gov for phase 3 breast cancer trials"
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.
MCP Clinical Research Server
This project exposes an MCP server that can:
search ClinicalTrials.gov for studies by condition or keyword
fetch the latest stock price for a company ticker from Yahoo Finance
Local setup
Run with Docker Compose
Copy the example env file:
cp .env.example .env
Update the values in
.envwith a real PAT if you want to use one:MCP_PAT=your_personal_access_token MCP_ACCESS_URL=http://localhost:8000/mcp MCP_PUBLIC_URL=http://localhost MCP_HOST=0.0.0.0 MCP_PORT=8000
Start the app with Docker Compose:
docker compose up --build
The server will be available at:
Run directly with Python
Create a virtual environment if you want one.
Install dependencies:
python -m pip install -e .
Set your PAT and the server endpoint:
export MCP_PAT="your_personal_access_token" export MCP_ACCESS_URL="http://localhost:8000/mcp" export MCP_HOST="0.0.0.0" export MCP_PORT="8000"
Start the server:
python -m mcp_clinical.server
or:
mcp-clinical
Related MCP server: Clinical Trials MCP Server
Access pattern
Your MCP client should connect to:
URL: http://localhost:8000/mcp (or your EC2 HTTPS URL)
Auth header: Authorization: Bearer
The server expects the same token in the MCP_PAT environment variable when it starts. When MCP_PAT is set, requests without the bearer token are rejected with a 401 response.
VS Code MCP config
Create a .vscode/mcp.json file with a PAT-based config like this:
{
"servers": {
"clinicalmcp-local": {
"type": "http",
"url": "http://localhost:8000/mcp",
"headers": {
"Authorization": "Bearer ${input:clinicalmcp_pat}"
}
},
"clinicalmcp-ec2": {
"type": "http",
"url": "https://your-ec2-host.example.com/mcp",
"headers": {
"Authorization": "Bearer ${input:clinicalmcp_pat}"
}
}
},
"inputs": [
{
"id": "clinicalmcp_pat",
"type": "promptString",
"description": "ClinicalMCP PAT"
}
]
}Use the EC2 entry when the app is deployed on your VM.
Available tools
server_access_info()clinical_trials_search(condition, max_results=5)get_company_price(ticker, range_name="1d")
Example calls
ClinicalTrials.gov
condition: "breast cancer"
max_results: 5
Market ticker
ticker: "AAPL"
range_name: "1d"
Public EC2 deployment
The app is deployed to EC2 as a Docker container fronted by NGINX:
EC2 instance with a public IP or Elastic IP
security group allowing
22,80, and443inbound (port8000stays closed)the container publishes only to
127.0.0.1:8000, so it is not reachable directlyNGINX listens on
80and proxies to127.0.0.1:8000MCP_PATis supplied through a.envfile written by the deploy workflowGitHub Actions runs the tests, then SSHes into the instance and redeploys on pushes to
main
Deployment is handled entirely by .github/workflows/deploy-ec2.yml. It installs Docker and NGINX if they are missing, so a bare Ubuntu instance needs no manual preparation beyond SSH access and the security group rules.
One-time setup
Launch an Ubuntu EC2 instance.
Open inbound ports
22,80, and443. Do not open8000.Add the repository secrets listed below.
Push to
main(or run the workflow manually via Actions -> Run workflow).
Required repository secrets
Secret | Required | Purpose |
| yes | Public DNS name or IP of the instance |
| yes | SSH user, |
| yes | Full contents of the private key, including the BEGIN/END lines |
| yes | Bearer token clients must send; the deploy fails if this is empty |
| yes | Public base URL, e.g. |
| no | Preferred market price source |
| no | Fallback price source, defaults to |
| no | Comma-separated browser origins; empty refuses all of them |
| no | Shared header value that locks the origin to CloudFront (see below) |
MCP_ACCESS_URL is derived automatically as ${MCP_PUBLIC_URL}/mcp and should not be set separately.
What the workflow does
test job: installs the package and runs
pytest. A failing test blocks the deploy.deploy job:
installs
docker.io,docker-compose-v2,git, andnginxif absentclones or fast-forwards the repo at
/home/ubuntu/ClinicalMCPover HTTPSwrites
.env(mode600) from the repository secretsruns
docker compose up -d --buildand prunes dangling imagesinstalls the NGINX proxy config as the
default_serveron port80smoke tests the app directly, then through NGINX, and asserts that an unauthenticated request is rejected with
401
The deploy is idempotent: it runs git reset --hard origin/main, so the instance always
matches main. .env is gitignored and survives the reset.
Operating the deployed instance
ssh -i /path/to/key.pem ubuntu@<EC2_HOST>
cd /home/ubuntu/ClinicalMCP
sudo docker compose ps # container status and health
sudo docker compose logs -f # follow application logs
sudo docker compose restart # restart without rebuilding
sudo docker compose up -d --build # rebuild after a code changeHTTPS via CloudFront
CloudFront gives you TLS on its own *.cloudfront.net certificate, so no custom domain
is required. Let's Encrypt cannot issue a certificate for an *.amazonaws.com hostname,
which is why certbot on the instance is not an option here.
Understand the trade-off first. CloudFront encrypts the client-to-CloudFront leg, but the CloudFront-to-EC2 leg stays plain HTTP, because CloudFront validates origin certificates against a public CA and an EC2 public hostname cannot have one. So the PAT is protected across the client's own network — coffee shop Wi-Fi, ISP, corporate proxy — but is still cleartext on the hop inside AWS. Steps 4 and 5 below reduce that to a CloudFront-only path rather than the open internet. If you need end-to-end TLS, use a real domain and certbot instead.
Do these in order, so you never lock yourself out of a working endpoint:
Pick a secret for the origin guard:
openssl rand -hex 32.Create the distribution (CloudFront console -> Create distribution):
Origin domain: your
EC2_HOSTProtocol: HTTP only, port 80
Add a custom header:
X-Origin-Secret= the secret from step 1Viewer protocol policy: Redirect HTTP to HTTPS
Allowed methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
Cache policy: CachingDisabled
Origin request policy: AllViewer
AllViewermatters: without it CloudFront strips theAuthorizationheader and every request returns 401.CachingDisabledmatters because MCP responses are per-session and must never be served from cache.Add the GitHub secrets and redeploy: set
CLOUDFRONT_ORIGIN_SECRETto the value from step 1, and changeMCP_PUBLIC_URLtohttps://<id>.cloudfront.net. Pushing tomainturns on the nginx origin guard and regeneratesMCP_ACCESS_URL.Verify
https://<id>.cloudfront.net/mcpanswers, and that a direct request tohttp://<EC2_HOST>/mcpnow returns 403 because it lacks the secret header.Restrict the security group: change the port 80 rule's source from
0.0.0.0/0to the AWS-managed prefix listcom.amazonaws.global.cloudfront.origin-facing. After this the origin is unreachable except through CloudFront.
The deploy already trusts X-Forwarded-For only from AWS's published CloudFront
origin-facing ranges, refreshed from ip-ranges.amazonaws.com on every run. Without that,
the rate limit would count every request as coming from a handful of edge IPs and throttle
all users collectively instead of per client.
If long tool calls ever cut off, raise the distribution's origin response timeout (default 30s). CloudFront streams responses fine, but will drop a stream that sits idle past that window.
Security model
Every request needs the PAT. The check is fail-closed and applies to all paths, so a route added later is protected by default rather than exposed by omission.
The server refuses to start when
MCP_PATis empty, instead of silently accepting anonymous callers. SetMCP_ALLOW_UNAUTHENTICATED=trueto override this locally.Tokens are compared with
hmac.compare_digest, so the PAT cannot be recovered a byte at a time by timing the responses.Requests carrying an unrecognised
Originheader are refused with 403. This blocks DNS-rebinding attacks from a web page. Non-browser MCP clients send noOriginand are unaffected; list trusted origins inMCP_ALLOWED_ORIGINSif you need browser access.NGINX rate limits each client IP to 10 req/s (
burst=20, returning429), so the PAT cannot be brute forced.The container listens only on
127.0.0.1:8000and runs as an unprivileged user. Port8000is not open in the security group; all traffic goes through NGINX.SSH is key-only (
PasswordAuthentication no,PermitRootLogin prohibit-password), andfail2banbans an IP for an hour after 5 failed attempts in 10 minutes. Port22stays open to0.0.0.0/0because GitHub-hosted runners deploy over SSH from rotating IP ranges that cannot be expressed in a security group.Origin guard: when
CLOUDFRONT_ORIGIN_SECRETis set, NGINX returns 403 to any request lacking the matchingX-Origin-Secretheader, so CloudFront cannot be bypassed.
Notes
The NGINX config disables
proxy_bufferingand uses a longproxy_read_timeout. MCP streamable HTTP holds SSE connections open, and the NGINX defaults would truncate tool responses mid-stream.Requests are served over plain HTTP, so the PAT crosses the network in cleartext. For anything beyond testing, put TLS in front: point a DNS name at the instance and run
sudo certbot --nginx, then updateMCP_PUBLIC_URLto thehttps://URL.Keep the PAT in GitHub secrets and never commit it to source control.
infra/clinicalmcp.serviceis left over from the earlier systemd-based deployment and is no longer used by the workflow.
This server cannot be deployed
Maintenance
Related MCP Connectors
Clinical trial search and status from ClinicalTrials.gov
Search and read ClinicalTrials.gov studies, sites and outcomes.
Search and analyze ClinicalTrials.gov: trials by keyword/condition/drug/status/phase, one study's…
Search 36M+ PubMed biomedical articles and ClinicalTrials.gov studies.
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-
- AlicenseBqualityDmaintenanceEnables searching and querying clinical trials from ClinicalTrials.gov with intelligent filtering for recruiting studies, geographic search, and detailed trial information including contacts and eligibility criteria.37 npm6MIT
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to search and access clinical trial data from ClinicalTrials.gov, including searching trials by keywords, retrieving detailed trial metadata by NCT ID, and managing trial data in CSV format for research and analysis.16-
- FlicenseNot gradedqualityDmaintenanceEnables searching and retrieving detailed clinical trial information from ClinicalTrials.gov via the official API.1-