Skip to main content
Glama
bob56621517

search-reader-mcp

by bob56621517

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 上传解析)、抓取结果缓存、MCP read 工具分片续读/引擎/超时控制

目录

介绍

本服务把三件事合进一个端口:

能力

端点

说明

read

GET /read/<url>(别名 /r/<url>)

网页/PDF → Markdown,进程内复用 jina 抓取

read

POST /read

文件上传解析(multipart file → Markdown)

search

GET /s/<query>(/search/ai/<query>)

bocha AI 语义搜索(总结+参考源+模态卡+追问)

search

GET /search/web/<query>

bocha 网页搜索(长摘要列表)

mcp

POST /mcp

MCP 服务(streamable HTTP),工具 search/read

sse

GET /sse + POST /messages

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 [选项] [服务名]):

部分

说明

up -d

启动服务并在后台运行;容器由 Docker daemon(Docker Desktop)托管,CLI 关闭后仍继续运行,直到 docker compose down

--build

启动前重新构建镜像改了 src/ 源码后必须带(否则复用旧镜像,改动不生效);仅改 docker-compose.yml 环境变量可省略

search-reader-mcp

服务名docker-compose.yml 定义了 search-reader-mcp(主服务)与 dev(开发热重载容器)两个服务;不带服务名会两个都启动,指定名只启一个

就绪:容器内 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"}

配置(环境变量)

变量

默认值

说明

PORT

18081

监听端口(容器内外一致)

HOST

0.0.0.0

监听地址

JINA_APP

/app

jina 镜像应用根目录

BOCHA_API_KEY

必填(搜索);开发/compose 共用标准环境变量

BOCHA_URL

https://api.bochaai.com

bocha API base-url

SEARCH_READER_MCP_DATA

/app/extension/data

持久化数据目录(compose 外挂宿主 ~/.search_reader_mcp/)

SQLITE_PATH

<dataDir>/cache.db

sqlite 缓存库路径

LOG_DIR

<dataDir>/.log

日志目录(按天滚动)

READ_CACHE_TTL

600

read 缓存 TTL(秒,默认 10 分钟),命中后滑动续期;见 缓存与超时

READ_TIMEOUT

90

HTTP 层整体超时兜底(秒),超时 504;见 缓存与超时

SERVER_URL

http://localhost:18081

服务端对外地址,用于上传解析提示词模板渲染;云部署改为公网地址

MCP_*

内建描述

MCP 工具/参数描述 env 覆盖,见下

持久化:sqlite 库、缓存文件(<dataDir>/read-cache/)与 .log/ 日志外挂宿主 ~/.search_reader_mcp/,容器重建后数据仍在。

MCP 描述 env 化(MCP_*)

工具与参数描述可经环境变量覆盖,缺省为内建描述。模式:MCP_<TOOL>_DESC = 工具描述,MCP_<TOOL>_<PARAM> = 参数描述;env 有值即覆盖,未设置沿用内建。

env

对应

MCP_SEARCH_DESC

search 工具描述

MCP_SEARCH_TYPE / MCP_SEARCH_QUERY / MCP_SEARCH_COUNT / MCP_SEARCH_FRESHNESS / MCP_SEARCH_INCLUDE(单数)/ MCP_SEARCH_EXCLUDE

search 各参数

MCP_READ_DESC

read 工具描述

MCP_READ_URI / MCP_READ_SKIP / MCP_READ_LENGTH / MCP_READ_ENGINE / MCP_READ_TIMEOUT

read 各参数

API

read(URL → Markdown)

/read/** 全量透传 jina 原生路由,/r/read 完全同义,支持任意 method

路由

method

行为

GET /read/<url>(/r/<url>)

GET

网页/PDF → Markdown 全文,接入缓存

GET /read(无尾路径)

GET

透传 jina 原生根路径(等价 jina /)

POST /read(无尾路径)

POST

文件上传解析(multipart file → Markdown),不缓存;见 文件解析(上传)

/read/<url> 其余方法

任意

透传 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);缺省走 env READ_TIMEOUT(默认 90s)。缓存命中瞬时返回,不消耗超时预算。详见 缓存与超时

  • 非 http(s) 资源(本地文件等):服务端不直接抓取,请用 文件解析(上传)POST /read 上传解析;MCP read 工具遇到非 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 略有差异):

参数

说明

count

条数上限,默认 20,越界自动钳制到 1..50

freshness

noLimit(默认)/oneDay/oneWeek/oneMonth/oneYearYYYY-MM-DD..YYYY-MM-DD,非法值回退 noLimit

include

限定站点,多个用 |, 分隔;web/ai 均支持

exclude

排除站点;仅 web 生效

summary / answer

是否返回长摘要 / AI 总结(布尔,默认透传官网)

响应(结构化 JSON):

  • ai:{ "summary", "webPages": [...], "modalCards": [...], "followUpQuestions": [...] }

  • web:{ "webPages": [...] }(webPages[]name/url/siteName/snippet/summary)

MCP(工具 searchread)

  • 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"
    }
  }
}

工具:

工具

参数

说明

search

type(默认 ai,可 web)、querycountfreshnessincludeexclude

格式化文本:AI 总结/模态卡/编号网页来源/追问

read

uriskiplengthenginetimeout

网页/PDF → Markdown 切片,支持分片续读

read 工具参数:

参数

类型

默认

校验

uri

string

必填

http(s) 资源由服务端抓取;其他 scheme 返回上传引导模板

skip

int

0

非负;跳过开头字符数,用于分片续读

length

int

5000

1..50000;返回切片长度,越界 schema 拒绝

engine

enum

auto

auto/direct/browser(暂不含 cf-browser-rendering)

timeout

int(秒)

见下

正整数,≤600

read 行为要点:

  • 返回全文的 [skip, skip+length) 纯文本切片;切片恰好到文末(或全文不足)时不加提示,被截断时(精确判断 skip+length < 全文长度)尾部追加提示:[内容已截断:全文约 N 字符,当前返回 a-b。可增大 length 或调 skip 续读剩余部分]

  • engine 映射:directX-Engine: curlbrowserX-Engine: browserauto → 不传;与 HTTP 层缓存键一致。

  • timeout 默认链:timeout 参数 > env READ_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_URL env,默认 http://localhost:18081)。

参数解释(与服务端 read 工具功能对齐):

说明

-X POST

上传解析走 POST

{SERVER_URL}/read

服务端上传解析端点;{SERVER_URL} 即服务端对外地址(默认 http://localhost:18081,云部署为公网地址)

-F 'file=@<本地文件路径>'

以 multipart/form-data 上传文件,字段名固定 file;支持 PDF / Word / Excel / PPT / HTML / 纯文本

-H 'x-engine: auto'

解析引擎,对应 read 工具的 engine 参数:auto(默认,智能选择)/ direct(轻量无 JS)/ browser(浏览器渲染)

-H 'x-retain-links: all'

保留页面中所有链接 URL(markdown 形式),默认全保留

-H 'x-retain-images: all'

保留页面中所有图片 URL(markdown 形式),默认全保留

响应:返回该资源的 Markdown 正文,所有链接与图片 URL 均以 markdown 保留;内容不递归嵌套解析(不展开链接指向的页面)。

可选 body 字段:

字段

说明

page

PDF 选页

url

raw HTML 的 base url

上传解析不缓存(一次性语义,缓存键依赖文件内容,保持简单)。

缓存与超时

read 缓存(一级)

只缓存解析后的 Markdown 全文(不缓存原始字节),HTTP 直连与 MCP read 工具共用同一缓存层。

行为

缓存键

uri(含 query)+ engine;engine 归一化为 auto/browser/curl 三值

TTL

READ_CACHE_TTL(默认 600s,10 分钟),命中后滑动续期(expire_at = now + TTL;写缓存基准 = 写入完成时刻)

清理

惰性删除(访问到过期即删重抓)+ 每小时定时兜底清理(仅删过期行)

in-flight 去重

同键并发只抓一次(共享进行中 Promise),完成后移除;不同 engine 互不等待

只缓存成功响应

jina 非 200 / 超时不写缓存,避免缓存坏结果

缓存命中

瞬时返回,不占用 timeout 预算

落盘

库文件同目录 read-cache/,文件名为 sha256(键);索引存于 sqlite read_cache

超时(三层)

机制

语义

jina 内部抓取

x-timeout(≤180s,透传)

软:等网络空闲或到点,超时返回已有内容

HTTP 层整体

X-Read-Timeout header / env READ_TIMEOUT(默认 90s)

硬:整体预算(加载 + 解析 + 返回),超时 504

MCP 参数

timeout(≤600s)

硬:超时返回可读错误文本

要点:

  • MCP timeout 语义 = 「加载 web + 解析」整体预算(一次 http(s) 读取);默认解析链:timeout 参数 > env READ_TIMEOUT > 内建 90s。

  • 透传映射:整体预算 clamp 到 180 后同时作为 jina x-timeout 传入,让 jina 内部预算与整体对齐(不出现 jina 还挂着、我们先 504 的错位)。

  • HTTP 层:X-Read-Timeout header 为 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)

认证

M8ven Score