chictr-mcp-server
v3.0.2
Published
MCP Server for querying ChiCTR clinical trials
Maintainers
Readme
ChiCTR MCP Server
基于 Model Context Protocol (MCP) 的中国临床试验注册中心 (ChiCTR) 查询服务。
站点对 /searchproj.html(搜索)与 /showproj.html(详情)施加了阿里盾(阿里云 WAF)保护:
直连返回 405,换出口 IP 则变成一页需要执行 JavaScript 才能通过的挑战页。
本项目用「Node MCP 服务器 + Python sidecar 子进程」的两段式架构解决它 —— sidecar 过一次挑战
拿到 cookie,之后全程走纯 HTTP。
当前版本: v3.0.2 · MCP 工具: 10 个 · 详情字段: 76 个 · 默认数据通道: ChiCTR 原站直连
📍 目录
🧩 Scrapling 是什么
Scrapling 是一个第三方 Python 爬虫库(pip install scrapling,本项目用的 scrapling[fetchers]
实测版本 0.4.15)。它提供 Fetcher / StealthyFetcher / StealthySession 等一把好用的
抓取 API,把「伪装 TLS 指纹」「跑无头浏览器」「过挑战」收敛成几行调用。
⚠️ 关键结论:解决阿里盾 405 的不是 Scrapling 这个「框架」
这是本项目最重要的一条认知,也是「开箱即用」代价的来源:
真正过盾的是 Scrapling 捆绑的两个底层组件 ——
curl_cffi和patchright。 Scrapling 只是把它们包成了好用的 API。
| 组件 | 角色 | 在过盾中的实际作用 |
|---|---|---|
| curl_cffi | TLS 指纹伪装(impersonate) | 让 Python HTTP 请求的 TLS 指纹(JA3/JA4)看起来像真 Chrome。它只把「硬 405」降级成「挑战页」,本身解不了挑战。 |
| patchright | Chromium 反自动化补丁(Playwright 的 fork) | 让阿里盾的挑战脚本能真正跑完并算出 acw_sc__v2 cookie。这才是拿到真实页面的那一步。 |
| Scrapling(Fetcher / StealthyFetcher) | 调用这两个组件的封装层 | 提供 API。它自带的 Cloudflare solver(solve_cloudflare=True)对阿里盾不起作用,只会打印 No Cloudflare challenge found. |
三层证据(dev/browser-removal/SCRAPLING_SOLUTION.md,2026-10-04 本机实测):
| 层级 | 手段 | 结果 |
|---|---|---|
| 0 | 裸 curl / 裸 Node 直连 | ❌ 405 硬拦截 |
| 1 | Fetcher(curl_cffi TLS 指纹伪装) | ⚠️ 200,但只返回 17,136 字节的挑战页(含 acw_sc__v2 / aliyunwaf) |
| 2 | StealthyFetcher(patchright + 真实导航) | ✅ 200,42,178 字节真实页面(共 13125、retrieve.js),挑战通过 |
| 2' | 同样的活交给原生 Playwright + page.setContent() | ❌ 挑战脚本死循环,跑不完 |
推论:StealthyFetcher 与原生 Playwright 的差别就在 patchright 的指纹补丁;
正是这个补丁让挑战脚本得以走完。Cloudflare solver 与本case无关。
代价:依赖很重(约 1 GB)
「框架能力很强」的反面是依赖体积。因为要同时带上「TLS 伪装库」和「带补丁的 Chromium 内核」:
| 项目 | 实测体积 |
|---|---|
| .venv(Python 依赖,其中 playwright 134 MB + patchright 134 MB) | 325 MB |
| ~/Library/Caches/ms-playwright(浏览器内核) | 557 MB |
| Node 侧 node_modules | 数百 MB |
| 合计 | ≈ 1 GB |
而且会存在两套 Chromium 内核:Node 侧 Playwright 要 chromium-1234,Python 侧 patchright 要
chromium-1243。这正是本项目不能做成「一个安装包全搞定」、必须把 Python 侧做成
运行时按需自举的根本原因(见 部署)。
🏗️ 架构与部署形态
运行时架构
MCP 客户端(Cherry Studio / Claude Desktop / …)
│ stdio(MCP 协议,stdout 专属通道)
▼
Node MCP server(dist/index.js) ← 对外协议、缓存、状态机、工具编排
│ CHICTR_USE_SIDECAR=1 时优先走这里
│ HTTP JSON → 127.0.0.1:8848
▼
Python sidecar(sidecar/chictr_sidecar.py) ← 过盾 + 抓取 + 解析
├─ curl_cffi 纯 HTTP + TLS 指纹伪装 ─────── 常规请求路径(约 1.5s/页)
└─ patchright StealthySession 无头浏览器 ── 仅 cookie 过期时过盾(约每小时 1 次)
▼
https://www.chictr.org.cn职责边界很清楚:sidecar 只负责「把 HTML 拿回来并解析成 JSON」,Node 侧负责一切对外契约。
三种部署形态
| 形态 | 组成 | 浏览器依赖 | 适用 |
|---|---|---|---|
| A. sidecar 形态(推荐) | Node MCP server + Python sidecar | 仅过盾时拉一次 Chromium | 本机开发、桌面 App、长期驻留服务 |
| B. 纯 Playwright 形态(默认/回退) | 只有 Node MCP server | 每次查询都要 Chromium | 已有浏览器环境、sidecar 不可用时的兜底 |
| C. ICTRP 形态(规划中) | 只有 Node MCP server,走 WHO ICTRP | 无 | 轻量部署、批量检索;见 dev/browser-removal/DUAL_CHANNEL_ARCHITECTURE.md |
当前实现落地的是 A + B:sidecar 可用则走 A,任一环节失败自动回退 B,对外契约不变。 形态 C 仍是设计稿,尚未实现。
打包形态(形态 A 打成 dmg/exe 时):
你的 App.app / your-app.exe
├── node_modules/ # Node 依赖,打包器收集(几十 MB)
├── dist/ # TS 编译产物
├── sidecar/chictr_sidecar.py # 随包分发(package.json 的 files 已含 "sidecar")
└── (首次运行时在用户目录创建)
└── .venv/ # 325 MB,由 setup 创建,不进安装包关键约束:打成 dmg/exe 后安装器不会执行 package.json 的 postinstall(打包器只收集文件,
钩子被丢掉)。所以依赖准备必须由主程序在运行时驱动 —— 这就是 env-probe.ts + bootstrap.ts
存在的原因。
⚙️ sidecar 设计
接口(极小的 HTTP JSON)
sidecar 用标准库 ThreadingHTTPServer 暴露三个只读端点,没有框架依赖:
| 方法 | 路径 | 参数 | 返回 |
|---|---|---|---|
| GET | /health | — | {ok, solver:{fresh, age_seconds, solve_count, last_error, cookie_names}} |
| GET | /search | title、regno、createyear、page、pages | {ok, source, query, total, total_pages, returned, pages_fetched, results[]} |
| GET | /detail | proj(project id)、raw(1/true/yes 时额外返回 raw_html) | {ok, source, project_id, url, registration_number, fields{}, field_count, raw_text, raw_text_length} |
错误语义:过盾后仍拿到挑战页 → 503 {"error":"challenge_unsolved"};缺 proj → 400;
其他异常 → 502。默认监听 127.0.0.1:8848(--host / --port / CHICTR_SIDECAR_HOST /
CHICTR_SIDECAR_PORT 可覆盖,另有 CHICTR_SIDECAR_LOG 控制日志级别)。
/search 的 results[] 元素:
{
"project_id": "245719",
"registration_number": "ChiCTR2600133371",
"title": "阿得贝利单抗联合NALIRIFOX用于可切除胰腺癌新辅助治疗",
"institution": "浙江大学医学院附属第一医院",
"study_type": "干预性研究",
"registration_date": "2026/09/24",
"detail_url": "https://www.chictr.org.cn/showproj.html?proj=245719"
}核心机制:cookie 复用,浏览器降级为「低频刷新器」
这是整个设计的地基 —— 挑战解出的 cookie 可以脱离浏览器复用。
- 过一次挑战:
ChallengeSolver._solve()启动StealthySession(headless=True), 导航到/searchproj.html,从session.context.cookies()取出acw_sc__v2/acw_tc/ssxmod_itna/ssxmod_itna2四个 cookie。 - 纯 HTTP 复用:
ChictrClient.get_html()用FetcherSession(impersonate="chrome")带上这组 cookie 请求,不再触碰浏览器。 - 实测摊薄效果:
solve_count = 1支撑了 ≥13 次请求(含 3 并发 + 10 并发分页), 期间未触发重新过盾。一次过盾约 4.7–6.0s,此后约 1.5s/页。
设计上还有几处关键细节:
| 机制 | 实现 | 原因 |
|---|---|---|
| 双重检查锁 | ensure() 先无锁查 is_fresh(),未命中再进 threading.Lock 复查后才 _solve() | 多线程并发首次请求时只跑一次浏览器 |
| 常驻 HTTP 会话 | FetcherSession 是工厂式 context manager,构造时进入一次并保持常驻,由 close() 退出 | 避免每个请求重建连接池 |
| 底层串行化 | self._lock 包住 self._http.get(...) | curl_cffi 的底层 session 并非线程安全,并发是「排队」而非真并行 |
| 内容检测兜底 | _is_challenge() 检测响应里是否只有 acw_sc__v2、缺少 retrieve.js/myPagination.js/showproj | 不信任 TTL,信任响应内容 |
| 一次重试 | 命中挑战页或请求异常 → solver.invalidate(reason) → 重试一次(_retry=False 防递归) | 恢复路径短且不会打转 |
⚠️ 重要发现:cookie 实际只有约 37.5 分钟,不是 1 小时
代码里 COOKIE_TTL_SECONDS = 55 * 60(55 分钟,比 acw_sc__v2 的 1 小时 expires 留了 5 分钟余量)。
但实测 cookie 实际存活约 37.5 分钟,TTL 判断偏乐观。
这个偏差不会造成故障,因为兜底不在 TTL 而在内容检测:
cookie 提前失效时,请求会返回挑战页而非报错,_is_challenge() 立刻识别出来 →
invalidate() 清空 cookie → 重新过盾 → 重试。实测约 5.7s 恢复,调用方只感知到一次稍慢的请求。
结论:fresh 只是「提前刷新」的优化信号,真正的正确性由响应内容检测保证。
运维上不应把 solver.fresh == false 当成故障(见监控)。
Node 侧客户端(src/runtime/sidecar-client.ts)
| 行为 | 实现 |
|---|---|
| 默认关闭 | isSidecarEnabled() 只在 CHICTR_USE_SIDECAR 为 1/true/yes/on 时返回 true |
| 失败即回退 | 任何失败(不可达、非 JSON、非 2xx、payload.error、0 条有效结果)返回 null,由 src/index.ts 回退 Playwright |
| 快速失败 | 请求带 AbortController + CHICTR_SIDECAR_TIMEOUT_MS(默认 30000ms) |
| 状态可见 | getSidecarStatus() → {enabled, available, baseUrl, lastError},经 get_runtime_metrics 暴露 |
get_trial_detail 需要先有 project_id:客户端先查缓存里的「注册号 → project_id」映射,
没有就先用 /search?regno= 查一次拿 project_id,再调 /detail?proj=。
惰性初始化浏览器(关键修复)
原 src/index.ts 的 CallToolRequestSchema handler 在 switch 之前无条件执行
new BrowserManager() + initialize() —— 后果是即使 sidecar 命中也会先拉起 Chromium,
sidecar 的全部价值被这一行抵消。
现已改为 ensureBrowser() 惰性函数,只有真正会走 Playwright 的分支(含 sidecar 回退)
才初始化 Chromium。决定性验证:在一台刻意不安装 Playwright 浏览器内核的机器上
(Executable doesn't exist at .../chromium_headless_shell-1234/...),修复前 search_trials
直接抛错,修复后同样调用返回真实数据 —— 证明全程未触碰浏览器。
🚀 部署
用户视角:三步走
# 1. 体检(只读,不改任何东西;退出码 1 = 需要处理)
chictr-mcp-server doctor
# 2. 一键修复(创建 venv + 装依赖;联网,首次约 40s ~ 3min)
chictr-mcp-server setup
# 3. 启动 sidecar(前台运行,首次会过一次盾)
chictr-mcp-server sidecar然后让 MCP 客户端走 HTTP 路径:
export CHICTR_USE_SIDECAR=1
chictr-mcp-server --transport=stdio
setup默认不下载浏览器内核(省 560 MB)。sidecar 的纯 HTTP 常规路径不需要它; 只有「首次过盾」才需要 Chromium。需要时加--browser显式授权下载。也可以把这三步交给 AI —— MCP 工具
check_environment会返回同样的体检结论和修复建议。
退出码约定
| 码 | 含义 | 调用方应对 |
|---|---|---|
| 0 | 环境就绪 / 操作成功 | 继续 |
| 1 | 未就绪,需要用户动作 | 提示用户跑 setup |
| 2 | 自举失败(网络、磁盘、权限) | 展示错误,可重试 |
自动化的三个层次
- 启动时自检(已实现,无需配置):MCP 服务每次启动做一次体检,结果只写 stderr
(stdout 是 MCP 协议通道,写进去会破坏协议)。体检永不阻塞启动、永不抛异常 ——
环境好坏都能启动服务,只是能力不同。
[chictr-mcp-server] 环境未就绪:未找到 Python 虚拟环境 [chictr-mcp-server] 运行 "chictr-mcp-server setup" 自动修复 - 首次运行自动下载(可选,需显式授权):
setup --browser或代码里bootstrapEnvironment({ installBrowser: true, onProgress })。 刻意不做静默自动下载 —— 560 MB 在用户不知情时下载是冒犯行为。 - 自举健壮性:
bootstrapEnvironment()是幂等的(已存在的 venv 不重建、已装的依赖 不重装,中断后重跑从断点继续);pip 镜像按首选 → 其他 → 官方源自动回退 (默认阿里云;清华镜像对 pip 26 的新版 User-Agent 返回 403,会把错误伪装成Could not find a version that satisfies the requirement scrapling[fetchers] (from versions: none), 极具误导性,故镜像回退是必需而非优化)。
环境变量总表
| 变量 | 默认 | 说明 |
|---|---|---|
| CHICTR_HOME | 自动推断 | 项目根目录(打包后位置非常规时必须设) |
| CHICTR_VENV | <root>/.venv | 虚拟环境目录 |
| CHICTR_USE_SIDECAR | 未设置(关闭) | 设为 1/true/yes/on 启用 sidecar 优先路径 |
| CHICTR_SIDECAR_URL | http://127.0.0.1:8848 | sidecar 地址 |
| CHICTR_SIDECAR_TIMEOUT_MS | 30000 | sidecar 单次调用超时 |
| CHICTR_SIDECAR_HOST / CHICTR_SIDECAR_PORT | 127.0.0.1 / 8848 | sidecar 监听地址(Python 侧) |
| CHICTR_SIDECAR_LOG | INFO | sidecar 日志级别 |
| HTTP_PROXY / HTTPS_PROXY | 未设置 | 代理(可选) |
用户环境要求
| 要求 | 说明 |
|---|---|
| Node.js | ≥ 20(打包版自带,可忽略) |
| Python | ≥ 3.10(setup 自动发现 python3.13/3.12/3.11/3.10/python3) |
| 磁盘 | 纯 HTTP 路径约 400 MB;含浏览器约 1 GB |
| 网络 | 首次 setup 需联网(约 40s ~ 3min) |
如果用户没有 Python:doctor 会报 venv: missing 并提示安装 Python。这是 setup
唯一无法自动解决的依赖。
⚠️
pip install --user在部分 macOS 上会因权限失败 (OSError: [Errno 1] Operation not permitted: .../site-packages/w3lib),必须用 venv。⚠️
.venv默认建在项目根。若安装目录只读(macOS/Applications、WindowsProgram Files),必须设置CHICTR_HOME指向可写目录;isWritable()会在体检报告里提示。
监控与排障
curl -sS http://127.0.0.1:8848/health
# {"ok": true, "solver": {"fresh": true, "age_seconds": 57.0, "solve_count": 1, "last_error": null,
# "cookie_names": ["acw_sc__v2","acw_tc","ssxmod_itna","ssxmod_itna2"]}}solver.solve_count:累计过盾次数。稳态下应长期为 1(约每小时 +1)。 快速上涨说明 cookie 被反复判失效,应检查是否被限流。solver.last_error:最近一次过盾失败原因,正常为null。- MCP 侧可通过
get_runtime_metrics看sidecar: { enabled, available, baseUrl, lastError }。
| 症状 | 原因 | 处理 |
|---|---|---|
| /health 连不上 | sidecar 未启动 | chictr-mcp-server sidecar |
| 日志 ERROR: No Cloudflare challenge found. | 正常噪声 | ChiCTR 用阿里盾不是 CF,可忽略;成功标志是紧随的 INFO 过盾成功,耗时 X.Xs |
| {"error":"challenge_unsolved"} (503) | 过盾后仍返回挑战页 | 通常是 IP 被限流,稍后重试 |
| MCP 返回 Playwright 报错 | sidecar 未启用或不可达 | 确认 CHICTR_USE_SIDECAR=1 且 curl /health 正常 |
| from versions: none | pip 镜像 403 | 已自动回退;或 setup --mirror=aliyun |
| 体检报 venv missing 但目录存在 | venv 损坏 | setup 会检测并重建;或 clean --yes 后重来 |
| 启动即崩 Could not locate the bindings file | better-sqlite3 未针对当前 Node ABI 构建 | 见下 |
# better-sqlite3 原生绑定(Node 22 / ABI v127);npm rebuild 在受限沙箱会因
# 无法写 ~/Library/Caches/node-gyp 而失败,用预编译包绕过
cd node_modules/better-sqlite3 && npx prebuild-install -r node -t 22.19.0全新安装实测记录
在一个「什么都没有」的隔离目录(只有 package.json + dist/ + sidecar/chictr_sidecar.py,
共 592 KB)验证:
| 步骤 | 结果 |
|---|---|
| doctor | ✅ 正确报未就绪,退出码 1,给出 actions |
| setup --mirror=tsinghua | ✅ 清华失败 → 自动回退阿里云成功(2m35s) |
| doctor | ✅ 全部就绪(venv 3.13.14 / 依赖 / 浏览器内核 / 脚本) |
| 启动 sidecar | ✅ 过盾成功 6.7s |
| 真实搜索「胰腺癌」 | ✅ total 469 / total_pages 47 / 返回 10 条 |
| MCP 工具 check_environment | ✅ 22ms(缓存命中) |
即:一台干净机器上,592 KB 的包 + 一条 setup 命令 = 可用。
🎯 应用场景
| 场景 | 怎么用 | 为什么合适 |
|---|---|---|
| 临床试验情报监测 | 按靶点/疾病关键词定时 search_trials,对新增注册号批量 get_trial_detail | 原站是实时数据(WHO ICTRP 为每周同步,有滞后);约 1.5s/页使每日轮询成本可忽略 |
| RAG / 知识库入库 | search_trials 拉列表 → get_trial_detail 拉 55+(最多 76 个)字段的结构化详情 → 写入向量库 | 详情页解析为中英对照的结构化字段,比裸 HTML 更适合切分与检索;v3.0.1 起含纳排标准、联系人、伦理委员会联系方式 |
| 患者匹配与入组筛选 | 用疾病名/基因突变(如 KRAS G12D、胰腺癌)搜索,再用纳入/排除标准过滤 | get_trial_detail 直接返回「纳入标准」「排除标准」「研究实施地点」「招募状态」 |
| AI Agent 的循证工具 | 作为 MCP server 挂到 Cherry Studio / Claude Desktop 等客户端 | 中国临床试验数据对上 AI 助手是通用缺口;10 个工具里有 5 个是给 Agent 自诊断用的 |
| 科研回顾与趋势分析 | 按 year 参数逐年检索,统计某靶点/机构的注册量变化 | 支持按年份、注册号、关键词组合查询,max_results 上限 100 |
| 部署自检(Agent 自助排障) | check_environment → 按返回的 actions 决定是否调 setup | 让 AI 能在用户机器上自行判断「缺 Python 还是缺依赖还是 sidecar 没起」 |
📋 可用工具
共 10 个 MCP 工具(v3.0.0 起新增 check_environment;工具数未变,v3.0.1 扩充的是 get_trial_detail 的字段覆盖)。
| # | 工具 | 用途 |
|---|---|---|
| 1 | search_trials | 按关键词 / 注册号 / 年份搜索 |
| 2 | get_trial_detail | 按注册号查询完整详情 |
| 3 | get_cache_stats | 单层缓存统计(命中率等) |
| 4 | clear_cache | 清除所有缓存 |
| 5 | get_cache_stats_v2 | 双层缓存统计(L1 内存 + L2 SQLite) |
| 6 | get_runtime_metrics | 运行时编排指标 + sidecar 状态 |
| 7 | get_access_state | 访问状态机(NORMAL/SUSPECTED/CHALLENGED/COOLDOWN/RECOVERY) |
| 8 | check_environment | 本机环境体检(Python venv / 依赖 / 浏览器内核 / sidecar),只读,不下载不改文件 |
| 9 | prepare_verification_session | 创建人工验证会话 |
| 10 | resume_after_verification | 人工验证后恢复访问状态 |
search_trials
// 按关键词
{ "name": "search_trials", "arguments": { "keyword": "KRAS", "max_results": 20 } }
// 按注册号
{ "name": "search_trials", "arguments": { "registration_number": "ChiCTR2500111173" } }
// 按年份
{ "name": "search_trials", "arguments": { "year": 2024, "max_results": 20 } }
// 组合
{ "name": "search_trials", "arguments": { "keyword": "KRAS", "year": 2024, "max_results": 10 } }参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| keyword | string | 否 | 注册题目关键词,最长 200,如 KRAS G12C、胰腺癌 |
| registration_number | string | 否 | 注册号,形如 ^ChiCTR\d{8,}$,最长 32 |
| year | integer | 否 | 注册年份,最小 2000,最大为当前年份 + 1 |
| max_results | integer | 否 | 最大返回数,默认 20,范围 1–100 |
get_trial_detail
{ "name": "get_trial_detail", "arguments": { "registration_number": "ChiCTR2500108082" } }返回详情页的全部结构化字段。v3.0.1 起解析器改为按 HTML 表格结构抽取,字段数由 29 个提升到 76 个、平均 58.6 字段/条(对 468 个真实页面实测),并已逐字段回查 HTML 原文校验(全库 28809 个字段值逐一回查,0 处不符、0 处伪造)。
主要字段分组:
| 分组 | 字段 |
|---|---|
| 注册信息 | 注册号 注册号状态 注册时间 最近更新日期 注册题目 研究课题的正式科学名称 研究课题代号(代码) 在二级注册机构或其它机构的注册号 |
| 联系人 | 申请注册联系人 申请注册联系人电话 申请注册联系人传真 申请注册联系人电子邮件 申请注册联系人通讯地址 申请注册联系人邮政编码,以及 研究负责人 对应的同一套 6 个字段 |
| 伦理委员会 | 是否获伦理委员会批准 伦理委员会批件文号 批准本研究的伦理委员会名称 伦理委员会批准日期 伦理委员会联系人 伦理委员会联系地址 伦理委员会联系人电话 伦理委员会联系人邮箱 |
| 单位与来源 | 申请人所在单位 研究负责人所在单位 研究实施负责(组长)单位 经费或物资来源 国家 省(直辖市) 市(区县) 单位(医院) 具体地址 单位级别 |
| 设计与目的 | 研究类型 研究所处阶段 研究设计 研究目的 药物成份或治疗方案详述 研究实施时间 征募观察对象时间 |
| 干预与样本 | 干预措施 组别 样本量 干预措施代码 |
| 结局指标 | 指标中文名 指标类型 测量时间点 测量方法 主要终点 次要终点 |
| 招募与共享 | 征募研究对象情况 年龄范围 性别 随机方法 盲法 是否公开试验完成后的统计结果 是否共享原始数据 共享原始数据的方式(说明 数据采集和管理(说明 数据与安全监察委员会 注册人 |
| 标本 | 采集人体标本 标本中文名 标本去向 |
全量 468 条中 100% 覆盖的字段有 37 个:
注册号注册号状态注册时间最近更新日期注册题目研究课题的正式科学名称研究疾病研究类型研究设计研究目的研究所处阶段研究实施时间征募观察对象时间征募研究对象情况随机方法(请说明由何人用什么方法产生随机序列)年龄范围性别指标中文名指标类型说明是否获伦理委员会批准是否公开试验完成后的统计结果申请人所在单位申请注册联系人申请注册联系人电话申请注册联系人电子邮件申请注册联系人通讯地址研究负责人研究负责人电话研究负责人电子邮件研究负责人通讯地址研究实施负责(组长)单位研究实施负责(组长)单位地址经费或物资来源注册人纳入标准排除标准。覆盖率差异是页面本身留空导致的,不是解析失败:
次要研究终点1/468、研究疾病代码5/468、研究负责人网址(自愿提供)6/468、干预措施代码7/468、测量方法60/468 均已抽查确认原页面为空。
check_environment
{ "name": "check_environment", "arguments": { "refresh": false } }refresh: true 忽略 30 秒缓存强制重新探测。返回 root / node / venv / pythonDeps / browser /
playwrightBrowser / sidecarScript / sidecar / summary / actions / canRunBrowserless。
canRunBrowserless 是判断「能否免浏览器检索」的关键信号。
缓存与状态工具
{ "name": "get_cache_stats", "arguments": {} }
{ "name": "clear_cache", "arguments": {} }
{ "name": "get_cache_stats_v2","arguments": {} }
{ "name": "get_runtime_metrics","arguments": {} }
{ "name": "get_access_state", "arguments": {} }人工验证会话
{ "name": "prepare_verification_session",
"arguments": { "target_url": "https://www.chictr.org.cn/searchproj.html", "timeout_ms": 300000 } }
{ "name": "resume_after_verification", "arguments": { "verification_id": "verify_xxx" } }📡 MCP 配置说明
最简配置(npx)
{
"mcpServers": {
"chictr": {
"command": "npx",
"args": ["-y", "chictr-mcp-server@latest"]
}
}
}全局安装后
npm install -g chictr-mcp-server{ "mcpServers": { "chictr": { "command": "chictr-mcp-server" } } }启用 sidecar(推荐)
{
"mcpServers": {
"chictr": {
"command": "node",
"args": ["/Users/you/chictr_trials/dist/index.js"],
"env": { "CHICTR_USE_SIDECAR": "1" }
}
}
}前置:先另开一个进程跑 chictr-mcp-server sidecar。
带代理
{
"mcpServers": {
"chictr": {
"command": "npx",
"args": ["-y", "chictr-mcp-server"],
"env": { "HTTP_PROXY": "http://your-proxy-server:port" }
}
}
}通信方式
| 方式 | 状态 | 说明 |
|---|---|---|
| stdio | ✅ 支持(默认) | 标准输入输出 |
| http | 计划中 | http://localhost:3000/mcp |
| sse | 计划中 | http://localhost:3000/mcp |
测试 MCP 服务
npx @modelcontextprotocol/inspector npx -y chictr-mcp-server🛠️ CLI 命令行工具
package.json 提供两个 bin:chictr-mcp-server(dist/index.js)与 chictr-setup
(dist/cli/setup-cli.js)。
npm install -g chictr-mcp-server # 全局安装
npx -y chictr-mcp-server # 或直接 npx(推荐)| 命令 | 说明 |
|---|---|
| chictr-mcp-server | 启动 STDIO MCP 服务 |
| chictr-mcp-server doctor | 环境体检(只读,不修改任何文件) |
| chictr-mcp-server setup | 安装缺失依赖(幂等,可重复运行) |
| chictr-mcp-server setup --browser | 同时下载浏览器内核(约 560 MB,需联网) |
| chictr-mcp-server setup --mirror=aliyun | 指定 PyPI 镜像(aliyun 默认 / tencent / official / tsinghua) |
| chictr-mcp-server setup --timeout=1800 | 单步超时(秒,默认 1800) |
| chictr-mcp-server sidecar [--port=8848] | 前台启动 sidecar(自动带 --warmup,透传 -- 参数) |
| chictr-mcp-server clean [--yes] | 删除虚拟环境以便重装 |
npm scripts 等价入口:npm run doctor / npm run setup / npm run sidecar。
📈 性能对比
| 路径 | 搜索耗时 | 说明 |
|---|---|---|
| 旧 Playwright 路径(回退形态) | 45s 超时上限 + 每页 5–10s 人为延迟(另有页间 3–6s、翻页前 2–4s 随机延迟) | src/services/search.ts 原逻辑 |
| sidecar 路径(推荐形态) | 约 1.5s/页,搜索端到端 ~1.5–2.1s,详情 ~2s | 纯 HTTP + cookie 复用,无浏览器 |
端到端响应时间下降约一个数量级,且去掉了原有的随机延迟规避逻辑。
其他基线(实测):
| 指标 | 数值 |
|---|---|
| 冷启动过盾 | 4.7 / 5.9 / 6.0 / 6.7s(多次冷启动) |
| 热态单页检索(regno) | 1.46s |
| 热态单页检索(title) | ~1.5s |
| 详情页 | ~2s |
| 一次过盾支撑请求数 | ≥ 13(solve_count 保持 1) |
| 并发 | 3 并发 3/3 成功;10 并发 10/10 成功(共 100 条),期间未重新过盾 |
| cookie 实际存活 | 约 37.5 分钟(TTL 配置 55 分钟偏乐观,靠内容检测兜底,5.7s 恢复) |
| 缓存 | 搜索结果 5 分钟,详情 10 分钟;双层 L1 内存 + L2 SQLite |
服务端返回示例
{
"results": [
{
"registration_number": "ChiCTR2500108082",
"title": "谷氨酰胺联合奥沙利铂、卡培他滨(XELOX)和贝伐珠单抗一线治疗KRAS G12D基因突变型晚期结直肠癌的单臂Ⅱ期探索性研究",
"study_type": "干预性研究",
"registration_date": "2025/08/25",
"institution": "浙江大学医学院附属第二医院"
}
]
}🔔 版本更新
v3.0.2 (2026-10-04)
- ✅ 重写详情页解析器:字段数 29 → 76,平均 21.9 → 58.6 字段/条。根因是旧实现把整页压平成一串文本再用正则猜边界,而详情页实际是标准表格(标签在
<td class="left_title">,值在紧邻的下一个<td>)。改为按表格结构解析后,英文标签不再混入值、值也不再越过相邻字段 - ✅ 补上此前完全缺失的字段:联系人/电话/传真/邮箱/通讯地址/邮编(申请注册联系人与研究负责人各一套)、伦理委员会联系人/地址/电话/邮箱、研究负责人所在单位、国家/省/市/单位(医院)/具体地址、研究所处阶段、组别、标本信息、数据共享与安全监察等
- ✅ 修复 900 字上限导致超长字段静默丢弃:实测「纳入标准」2242 字、「排除标准」1410 字、「研究实施地点」6964 字,旧正则
(.{0,900}?)匹配不到 → 字段整个消失。上限放宽至 60000 - ✅ 修复老式注册号漏匹配:2018 年前的编号形如
ChiCTR-IIR-17013424,旧正则ChiCTR\d{6,}抓不到。改为ChiCTR(?:-[A-Z]{2,4})?-?\d{4,} - ✅ 修复区块标题被当作字段:
测量指标/采集人体标本/研究实施地点/申办者/干预措施的值在嵌套子表格里,朴素配对会抽到'组别'、'国家'、'Outcomes'这类伪值 - ✅ 修复空值被兜底正则伪造:页面留空的字段(
研究课题代号(代码)等)会从扁平文本抽到下个标签名,产生'Study subject ID'假值 - ✅ 修复 HTML 实体未解码:部分单元格用字面
 /'书写,导致「具体地址」入库为'上海市 普陀区…' - ✅ 修复导航栏同名标签误匹配:页面顶部「按经费或物资来源统计」早于正文真实字段(偏移 141 vs 3364),正文起点现定位到「注册号:」之后
- ✅ 修复复合结局指标块串味:
主要终点旧值会吞掉后续子标签(Measure time point of outcome: …) - ✅ 校验:对 468 个真实页面逐字段回查 HTML 原文,全库 28809 个字段值 0 处不符、0 处伪造值;重抓 468/468 成功
- ✅ 修复**「纳入标准」「排除标准」整字段丢失**(发布后复查发现,影响全部记录):两个标签从未进入字段白名单,结构解析直接跳过——尽管单元格配对本身正确。实测覆盖率 0/468,修复后 468/468
- ✅ 修复**
_TAG_RE吞掉正文中的小于号**:旧正则r"<[^>]+>"把'转导效率<10%…'里的<10%当成标签起点,一路删到后面某个>,导致 9 条记录正文被静默抹掉。收紧为r"</?[A-Za-z!][^>]*>" - ✅ 修复英文行空值覆盖中文行真实值:同一标签在页面出现两次(中文行 + 英文行),英文行值为空,旧代码的无条件赋值会覆盖中文行的真实值
- ✅
tools/chictr_crawl.py随包分发(npm install后位于node_modules/chictr-mcp-server/tools/):批量抓取入库脚本,支持--keyword换关键词、--list-years预览规模、--export-json/--export-html导出、按project_id去重、幂等续跑
v3.0.0 (2026-04-10)
- ✅ 新增 Python sidecar 通道:用 Scrapling(curl_cffi TLS 指纹 + patchright 反检测内核)过一次阿里盾挑战拿到 cookie,之后全程纯 HTTP。搜索从「每页 5–10s」降到 ~1.5s,详情 ~2s
- ✅ 新增环境自举:
chictr-mcp-server doctor / setup / clean三个 CLI 子命令,一条setup命令即可在干净机器上创建 venv、装依赖、按需装浏览器内核 - ✅ 新增 MCP 工具
check_environment:Agent 可自查本机环境(Python / 依赖 / 浏览器内核 / sidecar 在线状态),按返回的actions决定是否调用setup - ✅ 修复浏览器惰性初始化:原先任何工具调用都会先拉起 Chromium,导致 sidecar 命中也要付浏览器的代价;现改为仅在真正走 Playwright 时才初始化
- ✅ npm 包内分发 sidecar 脚本(
sidecar/chictr_sidecar.py),装上即具备 sidecar 能力 - ✅ 修复 Python 版本下限从未生效的问题(
MIN_PYTHON此前未被使用,低版本 Python 会被误判为「就绪」) - ✅ 缓存清除工具不再附带旧的随机延时(sidecar 通道下不再需要人为降速)
v2.0.2 (2026-04-09)
- ✅ 修复详情空对象缓存命中问题(自动失效并重查)
- ✅ 进一步提升详情查询的有效内容获取稳定性
v2.0.1 (2026-04-09)
- ✅ 修复 Cherry Studio 场景下
./cache路径导致的启动失败(ENOENT) - ✅ 默认缓存路径调整为
~/.chictr/cache/chictr_cache.db - ✅ 增加
/tmp/chictr/cache/chictr_cache.db兜底路径
v2.0.0 (2026-04-09)
- ✅ 新增请求编排层(限速/重试/熔断)
- ✅ 新增 Session 池化与生命周期回收
- ✅ 新增挑战状态机与恢复工具(get_access_state / prepare_verification_session / resume_after_verification)
- ✅ 新增双层缓存(L1 内存 + L2 SQLite)与 get_cache_stats_v2
v1.2.1 (2025-01-17)
- ✅ 更新 README,添加多维度搜索示例
- ✅ 添加版本升级指南
- ✅ 提供 Cherrystudio 缓存清除方案
v1.2.0 (2025-01-17)
- ✅ 新增按注册号搜索(registration_number 参数)
- ✅ 新增按年份搜索(year 参数,默认当前年份)
- ✅ 所有搜索参数改为可选
- ✅ 修复详情查询 400 错误(使用正确的 project_id)
v1.1.0 (2025-01-17)
- ✅ 修复分页功能,支持多页结果获取
- ✅ 支持代理配置(HTTP_PROXY/HTTPS_PROXY)
- ✅ 增加验证码检测与友好错误提示
🛠️ 技术栈
| 层 | 技术 |
|---|---|
| Node 侧 | TypeScript、MCP SDK、Cheerio、node-cache、better-sqlite3、Playwright |
| Python sidecar | Python ≥ 3.10、Scrapling(scrapling[fetchers])、curl_cffi、patchright、标准库 ThreadingHTTPServer |
⚠️ 已知限制
诚实记录,未解决或未验证的项:
- Node 侧 Playwright 回退路径在本机不可用,且补装内核也无意义:Node playwright 1.62.1
需要
chromium-1234,而缓存里只有chromium-1243(Python 侧 patchright 用的)。 但真正过不了阿里盾的是原生 Playwright,不是缺内核 ——dev/browser-removal/WAF_RESEARCH.md记录的「真实 Chromium 同样卡死」正是用原生 Playwright +page.setContent()得到的; 能过盾的是 patchright(反检测补丁)+ 真实导航。因此不建议补装 Node 侧内核(多约 359MB 换来一个仍会失败的路径,还会引入两套 Chromium 内核互相冲突),正确做法是把 sidecar 视为唯一路径。doctor里该项显示 ❌ 属预期,不影响退出码(src/runtime/env-probe.ts:391注明 Playwright 是「可选回退路径,不是故障」)。 - cookie 过期边界:实测存活约 37.5 分钟 < TTL 配置的 55 分钟。靠内容检测 + 重试恢复 (实测 5.7s),但 TTL 只是一个乐观的提前刷新信号。
- 并发上限未探明:只测到 10 并发(10/10 成功)。
ChictrClient._lock串行化了底层 HTTP 会话(curl_cffi session 非线程安全),所以并发是「排队」而非真并行。请勿盲目加大。 - 长驻稳定性未验证:
StealthySession长时间运行的稳定性未测(当前每次过盾新建一次 浏览器上下文并退出)。 - 请求频率上限未测:阿里盾在更高 QPS 下是否二次拦截未知。
研究实施时间字段含中英对照噪音:值为'从 \n From \n 2024-01-01 00:00:00 至 \n To \n 2027-01-01 00:00:00'形态,中英标签夹在值中间。已不再是「粘连下一个字段」,但含英文对照 文本,需要时可自行剥离From/To。- ICTRP 双通道尚未实现:
dev/browser-removal/DUAL_CHANNEL_ARCHITECTURE.md是设计稿; 本项目当前没有 WHO ICTRP 通道,也没有source=参数。 - headless 模式下无法手动处理验证码:频繁请求仍可能触发滑动验证码。
⚖️ 合规说明
- ChiCTR 在 re3data 登记为数据访问
restricted,站点页脚为All rights reserved.。 - 无官方 API、无 bulk 下载通道。
- 因此:「可获得 ≠ 可再分发」。 本项目定位为受控环境下的个人研究/情报用途,
不做数据再分发。若要长期依赖 ChiCTR 直连,建议同时推进对
[email protected]的正式授权问询。 - 绕过 WAF 是与站点访问决策的对抗,属政策判断而非技术判断 —— 技术结论是
「访问可行」,合规判断请自行确认。相关背景见
dev/browser-removal/SCRAPLING_SOLUTION.md§6 与SIDECAR_RUNBOOK.md。
📄 许可证
MIT License
🙏 致谢
本项目使用中国临床试验注册中心 (ChiCTR) 的公开数据,感谢 ChiCTR 为医学研究做出的贡献。 特别感谢小胰宝和小x宝社区的❤️贡献与付出,用爱心与人工智能为癌症/罕见病患者及其家庭提供支持!
📞 相关文档
| 文件 | 说明 |
|---|---|
| dev/browser-removal/SCRAPLING_SOLUTION.md | 过盾可行性三层实测证据(含复现命令) |
| dev/browser-removal/SIDECAR_RUNBOOK.md | sidecar 运维手册 |
| dev/browser-removal/SIDECAR_VERIFICATION.md | sidecar 端到端验证报告与实测数据 |
| dev/browser-removal/DEPLOYMENT.md | 开箱即用部署指南(dmg / exe) |
| dev/browser-removal/DUAL_CHANNEL_ARCHITECTURE.md | 双通道降级架构(设计稿) |
| sidecar/chictr_sidecar.py | sidecar 实现(过盾 + 解析 + HTTP 服务) |
| src/runtime/sidecar-client.ts | Node 侧客户端(默认关闭,失败自动回退) |
如有问题,请提交 GitHub Issue。
