Skip to main content
Glama
opencare-skillhub

Unified Trial Search MCP Service

Unified Trial Search MCP Service

ไธญๆ–‡ English CI License Node TypeScript MCP Tests Sources Tools

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.

Documentation map

What you want to know

Where to look

How to deploy to a fresh environment (three commands / one script)

Install in this file

Full download URLs, sha256 digests, and the update/publish workflow for all three corpora

docs/corpus-lifecycle.md

Day-to-day operations, troubleshooting, data-cutoff semantics

docs/operations.md

Design decisions and boundaries (ADRs)

SPEC.md

Architecture: one sealed MCP tool surface over six channels, orchestrated with a closed source registry and a per-source terminal state

doctor tells you the truth about every channel โ€” and never prints a cookie:

Terminal output of unified-trial-mcp doctor showing six sources ready and exit code 0


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

โœ… coverage + completeness + truncation flags

Caller-supplied URLs / timeouts

common

โœ… impossible by construction (closed registry)

Secrets in config

often

โœ… separate cookie.env, mode 0600, never printed

New-environment setup

manual, 5 services

โœ… one registration + doctor + guided bootstrap

The six channels

Source ID

Channel

Kind

What it is good at

ictrp

WHO ICTRP

online aggregator

global breadth; multi-registry by design

ctv

Veeva CTV

online + local FTS

CT.gov depth, fast local index

chictr_online

ChiCTR

online

Chinese registered trials, live

chictr_pancreatic_archive

ChiCTR corpus

offline SQLite

468 pancreatic records + raw HTML, data cutoff 2026-10-04

chinadrugtrials

ChinaDrugTrials

controlled scrape

contains PI / investigator data

xyb_chinadrugtrials_archive

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.

Getting the offline corpora

Three offline corpora ship as release assets, so a cold start does not require hunting for files. Everything below is public; nothing needs a token, a session or a VPN.

The three assets

Corpus

Download

Size

sha256 (first 16)

Mount flag

Basis

chictr_pancreatic

chictr_pancreatic.tar.gz

25,381,264 B

abbfc53ba346741b

--chictr-corpus

upstream_public

xyb_cde_pancreatic

xyb_cde_pancreatic.tar.gz

11,032,155 B

d1ccdac0ef5462a6

--xyb-archive

community_owned

ctv_index

ctv_index.tar.gz

23,964,370 B

41f07ea5a0b7c1b8

--ctv-database

community_owned

Full URLs, ready to paste into curl -O, a mirror script, or an offline download tool:

chictr_pancreatic    https://github.com/opencare-skillhub/unified-trial-search-mcp-service/releases/download/chictr_pancreatic-2026-10-05/chictr_pancreatic.tar.gz
xyb_cde_pancreatic   https://github.com/opencare-skillhub/unified-trial-search-mcp-service/releases/download/xyb_cde_pancreatic-2026-10-05/xyb_cde_pancreatic.tar.gz
ctv_index            https://github.com/opencare-skillhub/unified-trial-search-mcp-service/releases/download/ctv_index-2026-10-05/ctv_index.tar.gz
# Fetch all three at once, then verify each against the manifest
curl -LO https://github.com/opencare-skillhub/unified-trial-search-mcp-service/releases/download/chictr_pancreatic-2026-10-05/chictr_pancreatic.tar.gz
curl -LO https://github.com/opencare-skillhub/unified-trial-search-mcp-service/releases/download/xyb_cde_pancreatic-2026-10-05/xyb_cde_pancreatic.tar.gz
curl -LO https://github.com/opencare-skillhub/unified-trial-search-mcp-service/releases/download/ctv_index-2026-10-05/ctv_index.tar.gz
shasum -a 256 *.tar.gz    # compare one by one with corpora/manifest.json

Full digests are in corpora/manifest.json, which is the contract fetch-corpus enforces. Release pages: chictr ยท xyb_cde ยท ctv_index.

Checksums are verified, and a failure leaves your existing data byte-identical:

unified-trial-mcp fetch-corpus --corpus chictr_pancreatic --apply
unified-trial-mcp configure --chictr-corpus ~/.unified-trial-mcp/corpora/chictr_pancreatic/chictr_pancreatic.db

unified-trial-mcp fetch-corpus --corpus xyb_cde_pancreatic --apply
unified-trial-mcp configure --xyb-archive ~/.unified-trial-mcp/corpora/xyb_cde_pancreatic

unified-trial-mcp fetch-corpus --corpus ctv_index --apply
unified-trial-mcp configure --ctv-database ~/.unified-trial-mcp/corpora/ctv_index/ctv.db

Drop --apply first to see a dry run (URL, byte count, digest, basis) without downloading anything. unified-trial-mcp bootstrap walks the whole cold start interactively and can chain these steps.

Manual (mirrors, air-gapped hosts, or if a release is unreachable)

Download the three links above by any means, verify each digest against corpora/manifest.json, extract, then mount the extracted paths:

# 1. verify (example: chictr)
shasum -a 256 chictr_pancreatic.tar.gz   # must equal the manifest's sha256

# 2. extract โ€” note the resulting layout differs per corpus
tar -xzf chictr_pancreatic.tar.gz -C ~/.unified-trial-mcp/corpora/
tar -xzf xyb_cde_pancreatic.tar.gz -C ~/.unified-trial-mcp/corpora/
tar -xzf ctv_index.tar.gz -C ~/.unified-trial-mcp/corpora/

# 3. mount the extracted paths
unified-trial-mcp configure \
  --chictr-corpus ~/.unified-trial-mcp/corpora/chictr_pancreatic/chictr_pancreatic.db \
  --xyb-archive   ~/.unified-trial-mcp/corpora/xyb_cde_pancreatic \
  --ctv-database  ~/.unified-trial-mcp/corpora/ctv_index/ctv.db

unified-trial-mcp doctor   # every ready source prints OK

Or point fetch-corpus at your own mirror instead of copying by hand โ€” the digest check still applies:

unified-trial-mcp fetch-corpus --corpus ctv_index --url https://your-mirror/ctv_index.tar.gz --apply
unified-trial-mcp fetch-corpus --corpus ctv_index --url file:///srv/airgap/ctv_index.tar.gz --apply

Extraction is not uniform, and getting it wrong fails at query time rather than at install time. Two flags name a file (--chictr-corpus, --ctv-database), so the payload sits directly under the corpus directory; --xyb-archive names the parent of packages โ€” the adapter scans it for subdirectories holding summary.json โ€” so that package keeps one directory level:

corpora/chictr_pancreatic/chictr_pancreatic.db   <- file directly inside
corpora/ctv_index/ctv.db                         <- file directly inside
corpora/xyb_cde_pancreatic/่ƒฐ่…บ็™Œ/summary.json    <- package stays nested

Bases for redistribution (why these three are distributable)

Those two bases are deliberately kept distinct (ADR-009 in the spec). Only the first rests on "the data is public". The other two are the community's own work product: the fact that the upstream CDE site needs a credentialed session, and that ctv.veeva.com/robots.txt forbids crawling /study-search, is irrelevant to that basis and must never be used to justify it. Conflating the two would quietly dissolve the rule that this service never fetches credentialed data for you.

The ctv_index corpus carries the index only โ€” not the upstream ctv-mcp-server code. It still needs its own checkout (see Requirements); fetch-corpus prints that requirement rather than implying the source is ready.

Two of those flags name a file (--chictr-corpus, --ctv-database), so the payload lands directly under the install directory; --xyb-archive names the parent of packages (the adapter scans it for subdirectories holding summary.json), so that package stays one level down. Each corpus is verified against the table its own adapter reads (trials vs studies) โ€” a .db that merely opens is not proof of a working install.

The CTV index has a second half no download can supply: the upstream ctv-mcp-server checkout it queries. fetch-corpus prints that requirement rather than pretending the source is ready. Owning the index means a cold start needs no crawl โ€” but a zero result from it still only ever means "not in this index", never "this trial does not exist".

Four rules this command follows:

  • sha256 is the contract. Every byte count and digest comes from corpora/manifest.json; a mismatch fails the whole operation;

  • A failed download never damages what you have. The archive is fetched to a temp file, verified, extracted to a temp directory, and only then swapped in. Verified by tampering with a single byte: the install aborts with a checksum error and the previous corpus is byte-identical afterwards;

  • Nothing credentialed is ever fetched. You are never handed access you do not have: sessions, controlled archives and anything behind a challenge stay manual (ADR-006). Each shipped corpus declares its own basis (upstream_public or community_owned) and an asset without one is refused at install time (ADR-009);

  • Use --url for a mirror. --url https://โ€ฆ or --url file:///โ€ฆ handles intranets and air-gapped hosts.

The data remains its originators'. ChiCTR's corpus is a public snapshot transported read-only; the Xiaoyibao CDE archive and CTV index are the community's own work, and registering trials may be revised upstream at any time, so treat the packaged copies as dated snapshots rather than a source of truth.

Publishing a new data version? See docs/corpus-lifecycle.md for the full lifecycle โ€” packaging, uploading a release, the automated deploy path (download โ†’ verify โ†’ extract โ†’ atomic swap โ†’ mount โ†’ query), how to publish an updated version every couple of months, troubleshooting, and the redistribution grounds. It also lists every URL and digest in full.

โš ๏ธ The easiest mistake when updating a database: corpora/manifest.json ships inside the npm package, and fetch-corpus reads the copy in its own package โ€” there is no remote manifest fetch. So pushing git without publishing a new npm version means already-installed users never see the new database. Publish both lines together: upload the release asset โ†’ commit the manifest โ†’ npm version patch && npm publish. See docs/corpus-lifecycle.md ยง4.2.1 for the step diagram and the measured evidence.

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.json claims 2026-09-29 while the newest of the 139 records is 2026-09-28, and the earliest reaches back to 2026-08-13; that window is reported rather than smoothed into a single tidy timestamp;

  • dataCutoff travels with search_trials as well as get_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

SUCCESS

Queried and returned results. Whether they are complete is a separate completeness question.

NO_RESULTS

Queried, and genuinely nothing matched. Read it together with coverage.zeroResultMeaning, which says what an empty result means for that source.

NOT_ENABLED

Switched off in this deployment; no request was made.

NEEDS_SETUP

Missing dependency, path, runtime, or session, so it cannot be queried. fixHint gives the fix.

NOT_QUERIED

No request was ever attempted (usually the overall deadline ran out). Distinct from "tried and failed", so it can be counted separately.

TIMEOUT

The request went out but did not return in time. The result is unknown.

CHALLENGE_REQUIRED

The upstream asked for human verification. This service does not bypass captchas or WAFs.

RATE_LIMITED

Throttled by the upstream. Retrying later may succeed.

DENIED

Access refused by the source (permissions or an expired session).

FAILED

Attempted and errored. reasonCode and explanation say why.

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

search_trials

โ‰ฅ1 of keyword/keywords/condition/terms

bounded-concurrency fan-out; per-source terminal states, coverage, completeness

get_trial_detail

recordId

one record; recordId is <sourceId>:<sourceRecordId>

get_record_evidence

recordId

raw evidence: raw_html/source_json/source_word/raw_text/field_excerpt

get_source_status

โ€”

per-source readiness, freshness, diagnostics

Maintenance (explicit only, dry-run by default):

Tool

Purpose

refresh_ictrp

refresh ICTRP cache/snapshots

sync_ctv_index

rebuild the CTV local index

sync_chinadrugtrials

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.

Published on npm, so it can be installed directly:

npm install -g unified-trial-mcp
unified-trial-mcp doctor        # per-source diagnostics

Or build from source:

git clone https://github.com/opencare-skillhub/unified-trial-search-mcp-service.git
cd unified-trial-search-mcp-service
npm install
npm run build

When building from source, npm run build sets the executable bit on the compiled entry. It matters: tsc writes 0644, and without that bit the command installed by npm install -g fails with "permission denied" - a failure that running node dist/... locally never reveals.

Register 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.

Deploying to a fresh environment: three commands

After installing the npm package, getting all three offline corpora in place takes three commands:

npm install -g unified-trial-mcp          # 1) install

unified-trial-mcp bootstrap               # 2) see the plan first (dry-run, touches nothing)
unified-trial-mcp bootstrap --apply       #    then run it

unified-trial-mcp doctor                  # 3) re-check per-source status

bootstrap --apply really installs and mounts all three corpora, running the whole chain in one go: download โ†’ verify byte count โ†’ verify sha256 โ†’ extract to staging โ†’ verify contents โ†’ atomic replace โ†’ write the config. Afterwards doctor should report:

OK   chictr_pancreatic_archive      corpus usable, 468 pancreatic-cancer records.
OK   xyb_chinadrugtrials_archive    1 data package available, keyword: ่ƒฐ่…บ็™Œ.
SET  ctv                            CTV database configured, but no upstream MCP service directory.

If any step fails it leaves already-installed data untouched and is safe to retry; a sha256 mismatch aborts and keeps the previous data.

Or one shell script

scripts/deploy-corpus.sh wraps the same flow as an idempotent script, for deployment pipelines:

./scripts/deploy-corpus.sh                     # dry run: prints what it would do
./scripts/deploy-corpus.sh --apply             # execute
./scripts/deploy-corpus.sh --apply --corpus ctv_index        # just one corpus
./scripts/deploy-corpus.sh --apply --mirror https://mirror/  # behind a firewall

It adds two things bootstrap --apply does not: a per-corpus dry run (each prints its URL, byte count and sha256 before downloading) and mirror support (--mirror swaps the GitHub prefix, and also accepts file:// for a local tarball). Exit code 0 means the script's own deployment steps succeeded โ€” it deliberately does not pass through doctor's exit code, because a fresh machine has no online sources configured and that would break && chains that actually worked.

What still needs a human

bootstrap only does what is mechanically preparable. It deliberately leaves the following alone; skipping them does not affect the installed offline corpora โ€” a search reports NOT_QUERIED / NEEDS_SETUP rather than pretending to have found nothing.

Source

What it needs

Command

ChinaDrugTrials (online)

a session Cookie, once

configure --cookie-from-entry-page (automatic) or configure --cookie-from-curl '<cURL from your browser>' (manual)

ICTRP (online)

an upstream Python service

configure --ictrp-bundle <ictrp-mcp-service dir>

ChiCTR (online)

an upstream service

configure --chictr-mcp-server <chictr_trials dir>

CTV (online)

an upstream service

configure --ctv-mcp-server <ctv-mcp-server dir>, then npm install && npm run build there

These upstreams are independent third-party / community services with their own runtimes and dependencies. This service will not git clone or npm install them for you โ€” letting a search service execute upstream code on your machine is a far bigger risk than the few minutes it saves.

doctor 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".

Full download URLs, sha256 digests, and the update/publish workflow for all three corpora: docs/corpus-lifecycle.md.

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

unified-trial-mcp.config.json

paths only โ€” never secrets, safe to copy or commit

0600

cookie.env

session cookie, written by configure

0600

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-page

This 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.

Manual configuration (instead of interactive bootstrap)

If you would rather not run bootstrap, or it misdetects something, point configure at each asset directly. Paths must be absolute and inside the allowlist; non-ASCII paths need no escaping:

unified-trial-mcp configure \
  --chictr-corpus   ~/.unified-trial-mcp/corpora/chictr_pancreatic/chictr_pancreatic.db \
  --xyb-archive     ~/.unified-trial-mcp/corpora/xyb_cde_pancreatic \
  --ctv-database    ~/.unified-trial-mcp/corpora/ctv_index/ctv.db \
  --ctv-mcp-server  /path/to/ctv-mcp-server \
  --chictr-mcp-server /path/to/chictr_trials \
  --ictrp-bundle    /path/to/ictrp-mcp-service

Configuration lives in ~/.unified-trial-mcp/unified-trial-mcp.config.json and never contains a secret; the ChinaDrugTrials cookie is kept separately in cookie.env (mode 0600) in the same directory. All three offline corpora are optional: a missing one only makes its own source report NEEDS_SETUP with a fix command, and never blocks the others.

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/           67 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 guards

Testing

npm test
# tests 67
# pass 66
# fail 0
# skipped 1

One test is opt-in because it needs a real ChiCTR corpus (a snapshot CI does not ship). Without it the suite reports 66 pass / 1 skip; run it against your own copy with:

UNIFIED_TRIAL_TEST_CHICTR_CORPUS=/path/to/chictr_pancreatic.db npm test

Coverage 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.env or a config file is ever tracked.

  • Closed registry โ€” scripts/check-closed-registry.mjs re-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.json scrape_time conflicts with individual records.

  • ChinaDrugTrials flattened details is unreliable (็”ณ่ฏทไบบๅ็งฐ = "12"); nested sections is authoritative.

  • 31 historic ChiCTR records have registration_number = NULL โ€” project_id is 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+ไบบๆ–‡๏ผŒๅ…จๅฟƒๅ…จๆ„ไธบๆ‚ฃ่€…/ๅฎถๅฑžๆœๅŠก๏ผ

Related MCP Connectors

Related MCP Servers