search-reader-mcp
search-reader-mcp
扩展 Jina Reader 镜像的项目:在 Jina Reader 基础上添加自定义搜索(bocha) 与 MCP 服务,以单端口整合 HTTP 服务器对外提供 read / search / mcp / sse 能力。
单服务、单端口:一个进程承载全部能力(默认 18081),
docker-compose仅用于便捷启动复用镜像环境:进程内复用 jina 镜像的 Chrome 抓取、依赖与运行环境,不另起炉灶
MCP 双传输:streamable HTTP 与 legacy SSE 兼容新旧客户端
read 增强:Jina 全量路由挂载(含
POST上传解析)、抓取结果缓存、MCPread工具分片续读/引擎/超时控制
目录
介绍
本服务把三件事合进一个端口:
能力 | 端点 | 说明 |
|
| 网页/PDF → Markdown,进程内复用 jina 抓取 |
|
| 文件上传解析(multipart |
|
| bocha AI 语义搜索(总结+参考源+模态卡+追问) |
|
| bocha 网页搜索(长摘要列表) |
|
| MCP 服务(streamable HTTP),工具 |
|
| MCP legacy SSE 传输(兼容老客户端) |
对 MCP 客户端(Claude Code 等),本服务暴露 search(联网搜索)与 read(读取网页/PDF,支持分片续读)两个工具,详见 MCP。
快速开始
docker compose(推荐)
# 前提:宿主已有环境变量 BOCHA_API_KEY(搜索必需)
docker compose up -d --build search-reader-mcp
# 访问 http://localhost:18081命令拆解(docker compose up [选项] [服务名]):
部分 | 说明 |
| 启动服务并在后台运行;容器由 Docker daemon(Docker Desktop)托管,CLI 关闭后仍继续运行,直到 |
| 启动前重新构建镜像。改了 |
| 服务名。 |
就绪:容器内 headless Chrome 启动不稳定且较慢(常遇 jina 内部 puppeteer 10s 超时,进程退出后由
restart: unless-stopped自动拉起,通常 1-2 次后成功),需 30-60s,以下方「启动后验证」的 health 返回 200 为准。停止用docker compose down(持久数据在宿主~/.search_reader_mcp/,不随容器删除)。默认值集中在docker-compose.yml,按需手工修改(端口、URL、路径等)。
docker run
docker build -t search-reader-mcp .
docker run -d --name srm -p 18081:18081 -e BOCHA_API_KEY search-reader-mcp启动后验证:
curl http://localhost:18081/health
# {"service":"search-reader-mcp","status":"ok"}配置(环境变量)
变量 | 默认值 | 说明 |
|
| 监听端口(容器内外一致) |
|
| 监听地址 |
|
| jina 镜像应用根目录 |
| — | 必填(搜索);开发/compose 共用标准环境变量 |
|
| bocha API base-url |
|
| 持久化数据目录(compose 外挂宿主 |
|
| sqlite 缓存库路径 |
|
| 日志目录(按天滚动) |
|
| read 缓存 TTL(秒,默认 10 分钟),命中后滑动续期;见 缓存与超时 |
|
| HTTP 层整体超时兜底(秒),超时 504;见 缓存与超时 |
|
| 服务端对外地址,用于上传解析提示词模板渲染;云部署改为公网地址 |
| 内建描述 | MCP 工具/参数描述 env 覆盖,见下 |
持久化:sqlite 库、缓存文件(<dataDir>/read-cache/)与 .log/ 日志外挂宿主 ~/.search_reader_mcp/,容器重建后数据仍在。
MCP 描述 env 化(MCP_*)
工具与参数描述可经环境变量覆盖,缺省为内建描述。模式:MCP_<TOOL>_DESC = 工具描述,MCP_<TOOL>_<PARAM> = 参数描述;env 有值即覆盖,未设置沿用内建。
env | 对应 |
|
|
|
|
|
|
|
|
API
read(URL → Markdown)
/read/** 全量透传 jina 原生路由,/r 与 /read 完全同义,支持任意 method。
路由 | method | 行为 |
| GET | 网页/PDF → Markdown 全文,接入缓存 |
| GET | 透传 jina 原生根路径(等价 jina |
| POST | 文件上传解析(multipart |
| 任意 | 透传 jina,不缓存 |
基本用法:
# 路径即 url;url 含 query string 时原样保留(不丢失)
curl "http://localhost:18081/r/https://example.com"
curl "http://localhost:18081/read/https://example.com?a=1&b=2"要点:
路径即 url:URL 需 URL 编码(如
http://与/编码为%3A、%2F),避免路径歧义;?query部分照常追加在路径后,服务端改写转发时保留原始 query string(不丢失?foo=bar)。返回全文 Markdown(
text/markdown);分片续读是 MCP 工具能力,HTTP 层始终返回全文。engine:请求头
X-Engine: browser(浏览器渲染,适合动态页面)或X-Engine: curl(轻量无 JS);缺省auto(后端默认组合策略)。不同 engine 各自独立缓存。timeout:请求头
X-Read-Timeout: <秒>控制本次请求整体超时(硬,超时 504);缺省走 envREAD_TIMEOUT(默认 90s)。缓存命中瞬时返回,不消耗超时预算。详见 缓存与超时。非 http(s) 资源(本地文件等):服务端不直接抓取,请用 文件解析(上传) 的
POST /read上传解析;MCPread工具遇到非 http(s) uri 会返回可执行的上传引导模板。
search(bocha)
GET(路径即 query,支持 query string 高级参数):
# ai 语义搜索(快捷方式 /s,或 /search/ai)
curl "http://localhost:18081/s/今天天气如何?count=5"
curl "http://localhost:18081/search/ai/今天天气如何?count=5&freshness=oneDay"
# web 网页搜索
curl "http://localhost:18081/search/web/hello%20world?count=10&exclude=spam.com"POST(标准 JSON body,GET/POST 交叉):
curl -X POST http://localhost:18081/search/ai \
-H 'Content-Type: application/json' \
-d '{"query":"今天天气如何","count":5,"freshness":"oneDay","include":"weather.com"}'高级参数(web 与 ai 略有差异):
参数 | 说明 |
| 条数上限,默认 20,越界自动钳制到 1..50 |
|
|
| 限定站点,多个用 |
| 排除站点;仅 |
| 是否返回长摘要 / AI 总结(布尔,默认透传官网) |
响应(结构化 JSON):
ai:{ "summary", "webPages": [...], "modalCards": [...], "followUpQuestions": [...] }web:{ "webPages": [...] }(webPages[]含name/url/siteName/snippet/summary)
MCP(工具 search、read)
streamable HTTP:
POST /mcp(协议:JSON-RPC,无状态模式:不生成/不要求Mcp-Session-Id,每次请求独立,天然支持多客户端)legacy SSE:
GET /sse建流,POST /messages发请求(连接级会话,每连接独立)
Claude Code 接入示例(~/.claude.json 或项目 .mcp.json):
{
"mcpServers": {
"search-reader-mcp": {
"type": "http",
"url": "http://localhost:18081/mcp"
}
}
}工具:
工具 | 参数 | 说明 |
|
| 格式化文本:AI 总结/模态卡/编号网页来源/追问 |
|
| 网页/PDF → Markdown 切片,支持分片续读 |
read 工具参数:
参数 | 类型 | 默认 | 校验 |
| string | 必填 | http(s) 资源由服务端抓取;其他 scheme 返回上传引导模板 |
| int |
| 非负;跳过开头字符数,用于分片续读 |
| int |
|
|
| enum |
|
|
| int(秒) | 见下 | 正整数,≤600 |
read 行为要点:
返回全文的
[skip, skip+length)纯文本切片;切片恰好到文末(或全文不足)时不加提示,被截断时(精确判断skip+length < 全文长度)尾部追加提示:[内容已截断:全文约 N 字符,当前返回 a-b。可增大 length 或调 skip 续读剩余部分]。engine 映射:
direct→X-Engine: curl、browser→X-Engine: browser、auto→ 不传;与 HTTP 层缓存键一致。timeout 默认链:
timeout参数 > envREAD_TIMEOUT> 内建 90s;缓存命中不消耗预算。一次一个 uri,不支持并行(需要并行时由客户端自行开 subagent)。
非 http(s) uri(
file:/ftp:/s3:/data:等)返回自包含上传引导模板(含可执行 curl 示例),不尝试抓取;模板权威依据见 文件解析(上传)。解析行为锚定:保留页面中所有链接与图片 URL(markdown 形式),不递归嵌套解析(不展开链接指向的页面)。
health
curl http://localhost:18081/health
# {"service":"search-reader-mcp","status":"ok"}/ 与 /health 等价;/read/** 之外的路径均归本服务。
文件解析(上传)
非 http(s) 资源(本地文件/内网文件等)由服务端直接抓取不可行,改为自行下载后上传解析:以 multipart/form-data 上传文件,POST /read 即 jina 原生文件解析端点,字段名固定 file,支持 PDF / Word / Excel / PPT / HTML / 纯文本。
curl -X POST http://localhost:18081/read \
-F 'file=@<本地文件路径>' \
-H 'x-engine: auto' \
-H 'x-retain-links: all' \
-H 'x-retain-images: all'提示:云部署时把
http://localhost:18081换成服务端公网地址(SERVER_URLenv,默认http://localhost:18081)。
参数解释(与服务端 read 工具功能对齐):
项 | 说明 |
| 上传解析走 POST |
| 服务端上传解析端点; |
| 以 multipart/form-data 上传文件,字段名固定 |
| 解析引擎,对应 |
| 保留页面中所有链接 URL(markdown 形式),默认全保留 |
| 保留页面中所有图片 URL(markdown 形式),默认全保留 |
响应:返回该资源的 Markdown 正文,所有链接与图片 URL 均以 markdown 保留;内容不递归嵌套解析(不展开链接指向的页面)。
可选 body 字段:
字段 | 说明 |
| PDF 选页 |
| raw HTML 的 base url |
上传解析不缓存(一次性语义,缓存键依赖文件内容,保持简单)。
缓存与超时
read 缓存(一级)
只缓存解析后的 Markdown 全文(不缓存原始字节),HTTP 直连与 MCP read 工具共用同一缓存层。
项 | 行为 |
缓存键 |
|
TTL |
|
清理 | 惰性删除(访问到过期即删重抓)+ 每小时定时兜底清理(仅删过期行) |
in-flight 去重 | 同键并发只抓一次(共享进行中 Promise),完成后移除;不同 engine 互不等待 |
只缓存成功响应 | jina 非 200 / 超时不写缓存,避免缓存坏结果 |
缓存命中 | 瞬时返回,不占用 timeout 预算 |
落盘 | 库文件同目录 |
超时(三层)
层 | 机制 | 语义 |
jina 内部抓取 |
| 软:等网络空闲或到点,超时返回已有内容 |
HTTP 层整体 |
| 硬:整体预算(加载 + 解析 + 返回),超时 504 |
MCP 参数 |
| 硬:超时返回可读错误文本 |
要点:
MCP
timeout语义 = 「加载 web + 解析」整体预算(一次 http(s) 读取);默认解析链:timeout参数 > envREAD_TIMEOUT> 内建 90s。透传映射:整体预算 clamp 到 180 后同时作为 jina
x-timeout传入,让 jina 内部预算与整体对齐(不出现 jina 还挂着、我们先 504 的错位)。HTTP 层:
X-Read-Timeoutheader 为 per-request 超时(MCP self-call 与 REST 通用);REST 调用方亦可用curl --max-time自控客户端等待。超时 → 不写缓存;缓存命中瞬时返回不消耗预算;timeout 不参与缓存键。
开发
开发/构建/测试在宿主完成(代码改动走 git worktree 隔离,容器仅用于冒烟验证,见 冒烟测试):
npm install # 宿主开发/测试依赖(koa/supertest 等已在 devDependencies)
npm test # tsc + HTTP 契约测试 + 纯逻辑单测
npm run build # 构建到 dist/测试:npm test 覆盖 HTTP 契约(单一 seam,supertest 打 koa app,mock bocha 与 jina 桥接)与 MCP read 纯逻辑(切片/截断提示/模板渲染/参数 schema/engine·timeout 映射)。read/、mcp/、sse/ 依赖 jina 镜像运行时(Chrome 抓取、MCP 传输握手),需容器内冒烟验证:docs/smoke-test.md(构建→启动→就绪→各端点断言),MCP 工具验证 scripts/mcp-smoke.mjs。
目录结构
src/
index.ts 入口(加载配置 → jina 桥接 → 服务器 → 监听)
server.ts 整合服务器(路由分发 + read 全量挂载/缓存/timeout + 请求·错误日志)
config.ts 环境变量配置(含 mcpDesc 描述 env 化)
bocha/ bocha 能力层(客户端 + VO 类型)
mcp/ MCP 服务层(server.ts 工具、read-tools.ts 纯逻辑:切片/模板/schema)
jina/ jina koaApp 桥接(复用抓取)
cache/ sqlite 缓存基础设施(read_cache 表 + 读写/续期/清理 + in-flight 去重)
log/ 按天滚动文件日志
test/ HTTP 契约测试 + 纯逻辑单测(单一 seam)
docs/ 术语(CONTEXT.md)、ADR、冒烟流程
scripts/ 冒烟辅助脚本参考
CONTEXT.md — 领域术语表
docs/adr/ — 架构决策(单端口整合、search 独立、read 复用、全量路由挂载、缓存、非 http(s) 处理、报错即 prompt)
docs/roadmap.md — 演进路线
docs/smoke-test.md — 容器冒烟流程
docs/agents/ — agent 技能文档(issue tracker / triage labels / domain)