Unified Trial Search MCP Service
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., "@Unified Trial Search MCP ServiceSearch all six trial channels for pancreatic cancer and report source coverage and cutoffs."
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.
Unified Trial Search MCP Service
One MCP endpoint that searches six clinical-trial channels at once — and never lies about what it did not find.
💚 This project exists thanks to Sam, contributor to the 小胰宝 (XiaoYiBao) community. His care and hard work made it real.
doctor tells you the truth about every channel — and never prints a cookie:

Why this exists
Anyone who has searched for clinical trials knows the drill: five browser tabs, four different registry search boxes, three different identifier formats, and no way to tell whether "0 results" meant the trial does not exist or the index just doesn't cover it.
Existing trial MCP servers solve this one registry at a time. That is exactly the wrong shape for the question people actually ask — "is there a trial for my patient, anywhere?" — because the answer is only meaningful if you can prove you looked everywhere and say honestly where you could not.
This service is the union of six channels behind a single sealed tool surface, built around one uncompromising rule:
Never silently convert "I could not look" into "it is not there."
That single rule turns out to drive almost every design decision below.
Related MCP server: ClinicalTrials.gov MCP Server
Why it is different
Typical single-registry MCP | This service | |
Channels | 1 | 6 (online + offline corpora) |
Cross-source dedup | ✗ | ✅ merges only on confirmed registration identity |
"0 results" | returned bare | ✅ never alone — carries the source's own coverage caveat |
Failure reporting | generic error | ✅ 10 terminal states, each machine-readable |
Partial coverage | invisible | ✅ |
Caller-supplied URLs / timeouts | common | ✅ impossible by construction (closed registry) |
Secrets in config | often | ✅ separate |
New-environment setup | manual, 5 services | ✅ one registration + |
The six channels
Source ID | Channel | Kind | What it is good at |
| WHO ICTRP | online aggregator | global breadth; multi-registry by design |
| Veeva CTV | online + local FTS | CT.gov depth, fast local index |
| ChiCTR | online | Chinese registered trials, live |
| ChiCTR corpus | offline SQLite | 468 pancreatic records + raw HTML, data cutoff 2026-10-04 |
| ChinaDrugTrials | controlled scrape | contains PI / investigator data |
| XYB archive | offline package | 139 pancreatic records, structured sections, data cutoff 2026-09-28 |
Together these cover the global registries, the Chinese registries, and the CDE drug-trial platform — including the NMPA/ChinaDrugTrials investigator information that no other MCP surfaces.
Offline snapshots declare their own data cutoff
The two offline sources are snapshots, not live data. Each reports its data cutoff explicitly, because a trial registered after that date is invisible, not absent — which is the entire reason this project exists:
"freshness": {
"kind": "offline_archive",
"dataCutoff": "2026-09-28T07:59:24.275394+08:00", // nothing in the package is newer
"cutoffSource": "per-record scrape_time and the summary.json declaration (newer wins); coverage: every record read",
"updateHint": "...maintained by the Xiaoyibao community; point configure --xyb-archive at a newer package..."
}Three commitments:
The cutoff is derived from the data, never hardcoded — a hardcoded date becomes a lie the moment somebody refreshes the data without touching the code;
Record-level times outrank the declared summary — measured on the shipped packages,
summary.jsonclaims2026-09-29while the newest of the 139 records is2026-09-28, and the earliest reaches back to2026-08-13; that window is reported rather than smoothed into a single tidy timestamp;dataCutofftravels withsearch_trialsas well asget_source_status— a zero-result answer has to carry where its snapshot stops, or it looks exactly like a zero result from a live registry.
The data is maintained by the Xiaoyibao community and depends on people updating it. Run doctor to see the
current cutoff at any time.
Cross-source identity that actually holds up
Searches fan out, then results are merged — but only when registration identity is confirmed:
ctv:UTN000552084 ┐
├─► NCT:NCT07066098 (one trial, two views)
ictrp:NCT07066098 ┘Two records with identical titles but different registry numbers are never merged. Verified
against real data: 15 raw hits → 13 canonical records, with the 2 merges manually checked. Every
merged record keeps mergedFrom, sourceLabels and perSource, so the merge is always auditable.
Reading the status codes
search_trials returns one terminal state per source. Only SUCCESS and NO_RESULTS mean that source was actually queried; the other eight all mean no result came back from that channel, and none of them supports any conclusion about whether a trial exists.
State | How a caller should read it |
| Queried and returned results. Whether they are complete is a separate |
| Queried, and genuinely nothing matched. Read it together with |
| Switched off in this deployment; no request was made. |
| Missing dependency, path, runtime, or session, so it cannot be queried. |
| No request was ever attempted (usually the overall deadline ran out). Distinct from "tried and failed", so it can be counted separately. |
| The request went out but did not return in time. The result is unknown. |
| The upstream asked for human verification. This service does not bypass captchas or WAFs. |
| Throttled by the upstream. Retrying later may succeed. |
| Access refused by the source (permissions or an expired session). |
| Attempted and errored. |
Every record also carries attempted: false means no request was made at all. Both NO_RESULTS and TIMEOUT can look like "nothing found", but only the former means the upstream confirmed an empty result.
Three channels need particular care when reading them:
CTV is a local index. A miss means it is not in the local SQLite/FTS store, not that it is absent from ClinicalTrials.gov:
仅表示本地 SQLite/FTS 索引未命中,不代表 ClinicalTrials.gov 上不存在该试验。
ICTRP is a lower bound. It is permanently flagged
isLowerBound: true; its documented silent gaps mean a small or empty result cannot support "this does not exist":上游标注结果不完整,估计缺失 N 条;ICTRP 导出存在已知静默缺口,零/少结果不能作为"不存在"的结论。
The two offline snapshots declare a data cutoff (see the next section). A trial registered after that cutoff is invisible in the snapshot, which is not the same as not existing.
The seven MCP tools
Query (read-only — never refreshes, never mutates an index):
Tool | Required | Purpose |
| ≥1 of | bounded-concurrency fan-out; per-source terminal states, coverage, completeness |
|
| one record; |
|
| raw evidence: |
| — | per-source readiness, freshness, diagnostics |
Maintenance (explicit only, dry-run by default):
Tool | Purpose |
| refresh ICTRP cache/snapshots |
| rebuild the CTV local index |
| incrementally archive ChinaDrugTrials |
The closed registry guarantee
search_trials accepts only registered source IDs. No tool accepts a URL, an upstream tool name,
a custom timeout, a filesystem path, a cookie, or a secret. Callers cannot redirect this service at
an arbitrary endpoint — enforced by schema and covered by tests that scan every tool's properties for
forbidden names (url/path/timeout/cookie/token/secret/credential/command/tool).
Install
Requires Node ≥ 22.13. No Python required for the offline-corpus path.
The floor is set by node:sqlite (the ChiCTR corpus adapter): the module exists from
Node 22.5.0 and stops requiring --experimental-sqlite at 22.13.0. On anything older
the service still starts and every other source keeps working — only the ChiCTR corpus
reports NEEDS_SETUP (NODE_SQLITE_UNAVAILABLE) with the version to upgrade to.
git clone https://github.com/opencare-skillhub/unified-trial-search-mcp-service.git
cd unified-trial-search-mcp-service
npm install
npm run buildRegister with your MCP client
{
"mcpServers": {
"unified-trial-mcp": {
"command": "node",
"args": ["/absolute/path/unified-trial-search-mcp-service/dist/src/cli/main.js", "serve"],
"env": { "UNIFIED_TRIAL_CONFIG_DIR": "/absolute/path/.unified-trial-mcp" }
}
}
}One registration. That is the whole integration. The service talks to the other channels itself.
Five-minute setup
# 1) Diagnose: per-source runtime, dependencies, data paths, freshness, session
node dist/src/cli/main.js doctor
# 2) Mount local assets (writes <configDir>/unified-trial-mcp.config.json, mode 0600)
node dist/src/cli/main.js configure \
--chictr-corpus /absolute/path/chictr_pancreatic.db \
--xyb-archive /absolute/path/xyb-chinadrugtrials-data/output \
--ictrp-bundle /absolute/path/ictrp-mcp-service \
--ctv-mcp-server /absolute/path/ctv-mcp-server \
--chictr-mcp-server /absolute/path/chictr_trials
# 3) Explicit bootstrap (dry-run by default, prints a plan)
node dist/src/cli/main.js bootstrap
node dist/src/cli/main.js bootstrap --apply
# 4) Acquire the ChinaDrugTrials session (automatic; guides you if it fails)
node dist/src/cli/main.js configure --cookie-from-entry-page
# 5) Re-check
node dist/src/cli/main.js doctordoctor exit codes: 0 fully ready · 1 degraded but usable · 2 no usable source.
If anything is missing, doctor and bootstrap print the exact next command to copy — never a
bare "not ready".
Configuration
Priority | Mechanism |
1 (highest) | CLI flags |
2 | environment variables |
3 | config file |
4 | defaults |
Two files in the config directory, deliberately separated:
File | Contents | Mode |
| paths only — never secrets, safe to copy or commit |
|
| session cookie, written by |
|
Real environment variables override cookie.env, so an explicit export always wins. A missing
cookie.env means "not configured" — never a fabricated credential.
About the ChinaDrugTrials session
New environments can acquire it without touching a browser:
unified-trial-mcp configure --cookie-from-entry-pageThis performs one ordinary GET of the site's public landing page, receives the two anonymous
anti-bot tickets the site hands out (FSSBBIl1UgzbN7N80S / ...T), then proves them with a real
read-only search before saving. If the site needs a logged-in session, the manual fallback is:
unified-trial-mcp configure --cookie-from-curl '<browser "Copy as cURL" command>'This is not a WAF bypass, and the distinction is load-bearing. The site gives us those tickets
for a normal visit: no CAPTCHA is solved, no challenge is defeated, no rate limit is evaded, no
credential is forged. That is categorically different from defeating an access control — which this
service never does. doctor and bootstrap never acquire a cookie themselves; they only print
the command and explain what failed. The service never reads another project's config file or a
browser profile.
Safety boundaries
Does not bypass cookies, access controls, robots, WAF, CAPTCHAs, or other anti-automation measures.
Does not auto-obtain secrets or controlled data; explicit commands only.
Never prints a cookie value (only field names and a length fingerprint).
Secret-key redaction in every log and error path.
Evidence paths are confined to an allowlist of configured roots.
Does not convert "not found" into "does not exist" — see "Reading the status codes" above.
Project layout
src/
core/ types, registry, config, orchestrator, merger, normalizer, logger, MCP transport
adapters/ one per channel: ictrp, ctv, chictr-online, chictr-pancreatic,
chinadrugtrials, xyb-archive
tools/ 7 MCP tools: schemas, handlers, server
cli/ single entry: serve / doctor / bootstrap / configure; cookie acquisition
test/ 46 tests: unit, adapters, orchestrator, tools
scripts/ CI guards (closed-registry invariant)
docs/ operations manual, test reports, diagram assets
.github/ CI workflow: typecheck, build, 3 test runs, invariant guardsTesting
npm test# tests 46
# pass 46
# fail 0One test is opt-in because it needs a real ChiCTR corpus (a snapshot CI does not ship). Without it
the suite reports 45 pass / 1 skip; run it against your own copy with:
UNIFIED_TRIAL_TEST_CHICTR_CORPUS=/path/to/chictr_pancreatic.db npm testCoverage includes: record identity and normalization, merge-only-on-confirmed-identity, adapter contracts against real data, the 4-concurrency / 75-second deadline orchestrator (including the distinction between "never started" and "timed out"), all 7 tool schemas and error contracts, cookie acquisition and masking, and challenge-page detection.
Continuous integration
.github/workflows/ci.yml runs on every push and pull request across
Node 22.13 and 24 on Ubuntu and macOS: typecheck → build → the suite three times (this
project has deadline- and concurrency-sensitive paths, so one green run is weak evidence) → CLI smoke
test → two invariant guards:
No committed secrets — fails if
cookie.envor a config file is ever tracked.Closed registry —
scripts/check-closed-registry.mjsre-checks the built schemas from outside the test process, failing the build if any tool property would let a caller name an endpoint, path, timeout, or credential.
Verified end-to-end against live sources: search_trials across all six channels, cross-source
merges, detail and evidence retrieval, maintenance dry-runs, and the full new-environment cookie flow
with zero environment variables.
Known data caveats
Documented rather than hidden — see docs/operations.md:
XYB archive
summary.jsonscrape_timeconflicts with individual records.ChinaDrugTrials flattened
detailsis unreliable (申请人名称 = "12"); nestedsectionsis authoritative.31 historic ChiCTR records have
registration_number = NULL—project_idis the real identity.ICTRP has documented silent gaps, so a low count never proves absence.
License
MIT. See LICENSE.
⚠️ Public source material may contain personal information. Returned data reflects its source; respect each registry's terms of use and applicable law, and do not treat public accessibility as unrestricted redistribution. Never commit
cookie.env.
致谢 / Acknowledgements
小胰宝社区,依托AI+人文,全心全意为患者/家属服务!
本项目由 小胰宝(XiaoYiBao)社区 贡献者 Sam 的 ❤️ 付出促成 —— 感谢他的用心与坚持。
This project was made possible by the ❤️ care and hard work of Sam, contributor to the 小胰宝 (XiaoYiBao) community. Thank you.
小胰宝社区,依托AI+人文,全心全意为患者/家属服务!
This server cannot be deployed
Maintenance
Related MCP Connectors
Clinical trial search and status from ClinicalTrials.gov
Search 36M+ PubMed biomedical articles and ClinicalTrials.gov studies.
- AmassOAuthtech.amass
Linked life-science search: 43M+ papers, 1.2M+ trials, drugs, genes, FDA/EMA approvals, patents.
Multi-engine search for AI agents. Trust scoring, local corpus, MCP-native. Self-hostable, BYOK.
Related MCP Servers
- AlicenseAqualityAmaintenanceEmpowers AI agents with direct access to the official ClinicalTrials.gov database, enabling programmatic searching, retrieval, and analysis of clinical study data through a Model Context Protocol interface.71,552 npm96Apache 2.0
- 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 querying clinical trial information from the Chinese Clinical Trial Registry (ChiCTR) by searching trials with keywords, registration numbers, or years, and retrieving detailed trial information.476 npm5Apache 2.0
- FlicenseNot gradedqualityDmaintenanceEnables AI agents to search and analyze clinical trial data from ClinicalTrials.gov using both structured SQL queries for filtering trials by status, phase, and conditions, and semantic vector search for exploring detailed protocol information like exclusion criteria.-