内容
# markfetch
**适用于 AI 代理和 shell 的阅读器视图。获取任何 URL,返回干净的 Markdown — 采用真正的 Chrome 请求指纹,而不是 curl。**
[](https://www.npmjs.com/package/markfetch)
[](https://github.com/vasylenko/markfetch/actions/workflows/ci.yml)
[](https://nodejs.org/)
[](https://github.com/vasylenko/markfetch/blob/main/LICENSE)
内置于 AI 编码代理中的 fetch 工具返回原始 HTML、损坏的 Markdown 或 Cloudflare 的 403 错误。`markfetch` 发送 **一致的 Chrome 头部集**,使 bot 检测系统看到一个真正的浏览器,然后将响应通过 **与浏览器相同的阅读器视图管道**(Mozilla 的 Readability → turndown)。输出的 Markdown 与人类运行“保存为 Markdown”无法区分 — 在一个 naive curl 会被阻止的网站上。
一个命令,两个界面:
- **CLI** — 传递 URL。打印到 stdout 或 `-o` 到文件。
```bash
npm i -g markfetch
markfetch https://en.wikipedia.org/wiki/Markdown
```
- **MCP stdio 服务器** — 裸调用。进入 Claude Desktop / Claude Code / Cursor / Goose / 任何 stdio-MCP 客户端。
```json
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"]
}
}
}
```
这段代码是整个 MCP 设置 — 或者跳转到 [CLI 用法](#cli-usage) 以从 shell 驱动相同的命令。
## MCP 安装命令
### Claude Code
```bash
claude mcp add --scope user markfetch -- npx -y markfetch
```
### Codex
```bash
codex mcp add markfetch -- npx -y markfetch
```
### Gemini CLI
```bash
gemini mcp add -s user markfetch npx -y markfetch
```
## 为什么选择 markfetch?
| | 真实的浏览器指纹 | 阅读器视图提取 | 结构化错误 | 零配置 |
|---|:---:|:---:|:---:|:---:|
| 内置代理提取工具 | – | – | – | ✓ |
| 常规 Playwright / Puppeteer | ✓ | – | – | – |
| `mcp-server-fetch` (Python) | – | 基本 | – | – |
| CloudFlare `/markdown` | ✓ | ✓ | – | 付费 |
| **`markfetch`** | **✓** | **✓** | **✓ (8 个代码)** | **✓** |
- **真实的浏览器请求指纹。** `User-Agent`、`Sec-CH-UA-*`、`Sec-Fetch-*`、`Accept-*` — 一致的 Chrome 头部集。一个带有客户端提示的 Chrome UA 比 curl 更强的自动化信号,因此 `markfetch` 发送完整的集合,从启动时根据 UA 派生,因此重写在内部保持一致。HTTP/1.1 via TLS:一些 CDN 指纹 undici 的 HTTP/2 连接并返回 403。
- **阅读器视图质量提取。** [linkedom](https://github.com/WebReflection/linkedom) → [@mozilla/readability](https://github.com/mozilla/readability) → [turndown](https://github.com/mixmark-io/turndown) 带有 GFM 表格、删除线和任务列表。代码块保留 `language-X` 提示。Sphinx 样式的裸 `<pre>` 块渲染为代码,而不是转义的文字。单词内的下划线保持未转义 — 不再出现 `list\_tools`。
- **一个工具,一个形状(MCP)。** `fetch_markdown(url, savePath?, raw?)` 在 `content[0].text` 中返回 Markdown。没有 `structuredContent`,没有 frontmatter,没有元数据字段。几个主要的 MCP 客户端(Claude Code CLI,VS Code/Copilot)仅将 `structuredContent` 转发给模型,当同时存在时丢弃 `content[]` — `markfetch` 故意停留在 LLM 可以实际读取的通道上。
- **`savePath` / `-o` 出口阀。** 传递绝对路径(MCP `savePath`)或 `-o <path>`(CLI),输出落在磁盘上而不是响应通道上。当客户端的内联工具结果容量将截断大响应时使用,或从 shell 管道重定向输出。文件仅是获取的输出(提取的 Markdown 或带有 `--raw` 的原始正文) — 获取错误返回 `[code]` 字符串并且从不触摸磁盘。
- **整个文档或诚实的失败。** 没有分页,没有截断。如果文档不适合 `MARKFETCH_MAX_BYTES`,则得到 `too_large` — 永远不会是半真半假。
- **Stdio-clean.** Stdout 保留用于 MCP 帧。Stderr 仅为致命错误。没有日志垃圾,没有 ANSI 转义 — 保持 stderr 可解析以供 shell 消费者使用。
- **纯 Node,没有子进程。** 没有 Playwright,没有无头 Chromium,没有 Python 跳转。单个 Node 进程 — 无论您将其作为 MCP 服务器还是从 shell 调用,都只有一个 Node 进程。
## CLI 用法
`markfetch` 作为 shell 工具使用:当带有至少一个参数调用时,它将 argv 解析为 CLI,而不是启动 MCP 服务器。裸调用(零参数)保持现有的 MCP 服务器行为 — 所有现有的 MCP 客户端配置保持不变且可正常工作。
```bash
# 将干净的 Markdown 打印到 stdout
npx -y markfetch https://example.com/article
# 保存到文件(绝对或相对路径)
npx -y markfetch https://example.com/article -o article.md
# 管道到另一个工具
npx -y markfetch https://example.com/article | pandoc -o article.pdf
# 获取 JSON / APIs / 页面源代码
npx -y markfetch --raw https://api.github.com/repos/vasylenko/markfetch
```
对于重复使用,安装一次:
```bash
npm i -g markfetch # 然后在任何地方:markfetch <url>
# 或作为项目开发依赖
npm i -D markfetch # 然后在 package.json 脚本中: "markfetch <url>"
```
标志:
| 标志 | 目的 |
|---|---|
| `-o, --output <path>` | 将输出保存到文件(绝对或相对路径)。默认为 stdout。 |
| `--raw` | 返回未处理的响应正文为 UTF-8 文本 — 跳过 Readability 和 content-type 门控。对于 JSON、XML、纯文本或页面源代码(不保留二进制)。 |
| `-V, --version` | 打印版本并退出。 |
| `-h, --help` | 打印用法并退出。 |
错误输出到 stderr,带有与 MCP 工具返回的相同的 `[code] message` 形状(参见下表),并且进程退出时状态为非零。相同的 env 变量 (`MARKFETCH_TIMEOUT_MS`、`MARKFETCH_MAX_BYTES`、`MARKFETCH_USER_AGENT`) 在两种模式下都适用。`MARKFETCH_ALLOWED_WRITE_ROOTS` 仅限 MCP — 参见 [写入沙箱](#write-sandbox)。
错误携带八个确定性代码之一:
| 代码 | 含义 |
|---|---|
| `network_error` | DNS / TCP / TLS 失败,或来自提取器的意外内部错误。 |
| `http_error` | 上游返回非 2xx 状态。 |
| `timeout` | 单个请求的预算 `MARKFETCH_TIMEOUT_MS` 超出。 |
| `unsupported_content_type` | 响应不是 `text/html` 或 `application/xhtml+xml`(不适用于 `--raw` / `raw`)。 |
| `extraction_failed` | Readability 没有返回任何文章内容(纯客户端渲染的 SPA 通常如此)。不适用于 `--raw` / `raw`。 |
| `too_large` | 响应正文或提取的 Markdown 超出 `MARKFETCH_MAX_BYTES`。 |
| `save_failed` | 给定 `savePath` 但 `writeFile` 失败(缺少父目录,权限被拒绝等)。 |
| `save_forbidden` | `savePath` 解析在允许的写入根目录之外 — 参见 [写入沙箱](#write-sandbox)。MCP 独有;CLI 没有沙箱。 |
## 不是什么
- **不是爬虫。** 没有递归,没有 `robots.txt` 解析,没有速率限制协调。一个 URL 进,一个文档出。
- **没有身份验证。** 匿名提取 — 没有 cookie 容器,没有身份验证头部,没有会话重用。登录墙后的页面返回公共响应,通常表现为 `http_error`。
- **不是 JS 渲染器。** 纯客户端渲染的 SPA 没有静态内容返回 `extraction_failed`。具有服务器渲染或 SEO 预渲染的 SPA 将提取它们提供的静态内容。
## 配置
| 变量 | 默认值 | 目的 |
|---|---|---|
| `MARKFETCH_TIMEOUT_MS` | `30000` | 单个请求的超时时间(毫秒) |
| `MARKFETCH_MAX_BYTES` | `5000000` | 响应正文和提取的 Markdown 的上限 |
| `MARKFETCH_USER_AGENT` | 固定的 Chrome 130 字符串 | 重写 UA。必须是 Chrome UA — `Sec-CH-UA-*` 客户端提示从中在启动时派生;非 Chrome 字符串快速失败 |
| `MARKFETCH_ALLOWED_WRITE_ROOTS` | `os.tmpdir()` + `process.cwd()` | 仅限 MCP。 以路径分隔符分隔的绝对路径列表,允许作为 MCP `savePath` 写入根目录。替换默认值 — 参见 [写入沙箱](#write-sandbox) |
通过 MCP 客户端配置的 `env` 块传递重写:
```json
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_TIMEOUT_MS": "60000"
}
}
}
}
```
### 写入沙箱
MCP `savePath` 写入被限制在允许的根目录集内。默认情况下,允许的集合是 `os.tmpdir()` ∪ `process.cwd()` (每个在启动时通过 `fs.realpath` 解析一次)。`savePath` 在此集合之外返回 `save_forbidden`,并且不创建文件。
用 `MARKFETCH_ALLOWED_WRITE_ROOTS` 重写默认值 — 以平台的路径分隔符(POSIX 上为 `:`,Windows 上为 `;`)分隔的绝对路径列表。设置时,重写 **替换** 默认值 — 它不合并。要保持 `os.tmpdir()` 或 `process.cwd()` 可访问,请自己列出它们;下面的示例显示 `/tmp` 为此目的。格式错误的 值(非绝对条目,或不存在的目录)在 stderr 上快速失败。
```json
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_ALLOWED_WRITE_ROOTS": "/Users/me/markfetch-out:/tmp"
}
}
}
}
```
在 Windows 上,使用反斜杠和 `;` 作为分隔符:
```json
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_ALLOWED_WRITE_ROOTS": "C:\\Users\\me\\markfetch-out;C:\\Users\\me\\AppData\\Local\\Temp"
}
}
}
}
```
注意:
- **沙箱仅限 MCP。** CLI 是无限制的 — shell 中的人类是安全边界,markfetch CLI 不运行任何沙箱检查。 不对称性存在是因为 MCP 工具由语言模型驱动,该模型可能由它刚刚获取的页面的内容驱动。
- **指向外部的符号链接被阻止。** 每个候选的 `savePath` 通过 `fs.realpath` 解析到其实际目的地,然后进行包含检查,因此在沙箱内植入的符号链接不能用于逃逸。
- **在 Windows 上,包含是大小写不敏感的** (`C:\Users\Bob` 和 `c:\users\bob` 是同一路径)。
## 开发
需要 Node.js ≥ 24。在 CI 中测试 Linux、macOS 和 Windows。
在迭代 CLI 更改时,`tsx src/index.ts <url>` 和 `tsx src/index.ts --help` 通过与编译的 `dist/index.js` 相同的 argv 区分调度器 — 在编辑之间无需重建。
要将 MCP 客户端指向本地源代码构建,请将 `npx` 替换为 `node` + 指向 `dist/index.js` 的绝对路径:
```json
{
"mcpServers": {
"markfetch": {
"command": "node",
"args": ["/absolute/path/to/markfetch/dist/index.js"]
}
}
}
```
## 责任使用
`markfetch` 是一个按需获取工具,而不是爬虫。请在您有权获取的 URL 上使用它,并尊重您查询的任何站点的服务条款。 维护者不对滥用承担任何责任 — 参见 [LICENSE](LICENSE)。
## 许可证
[MIT](LICENSE)