Doramagic 项目包 · 项目说明书
markfetch 项目
生成时间:2026-05-15 00:33:16 UTC
项目概述
markfetch 是一个纯 Node.js 编写的 URL 转 Markdown 工具,同时提供 MCP(Model Context Protocol)服务器模式和命令行界面(CLI)两种使用方式,专为 AI 智能体设计。该项目由 Serhii Vasylenko 开发,采用 MIT 许可证开源发布。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
项目简介
markfetch 是一个纯 Node.js 编写的 URL 转 Markdown 工具,同时提供 MCP(Model Context Protocol)服务器模式和命令行界面(CLI)两种使用方式,专为 AI 智能体设计。该项目由 Serhii Vasylenko 开发,采用 MIT 许可证开源发布。
markfetch 的核心功能是接收一个 HTTP/HTTPS URL,获取其 HTML 内容,提取主要文章内容,并将其转换为干净的 Markdown 格式输出。输出结果与人类执行"另存为 Markdown"命令获得的内容高度相似,能够在提供真实浏览器特征指纹的同时,绕过许多网站的反爬虫机制。
核心特性
多模式支持
markfetch 提供两种使用模式,可根据不同场景灵活选择:
| 模式 | 触发方式 | 输出方式 | 典型用途 |
|---|---|---|---|
| MCP 服务器模式 | 无参数启动或作为 MCP 工具调用 | content[0].text 结构化数据 | 集成到 Claude Code、Cursor、Goose 等 AI 客户端 |
| CLI 命令行模式 | markfetch <url> | 标准输出或文件 | Shell 脚本、管道操作、直接终端使用 |
技术架构特点
- 纯 Node.js 实现:无任何子进程依赖,不使用 Playwright、headless Chromium 或 Python 等外部运行时 资料来源:README.md
- 真实浏览器指纹:通过 HTTP/2 传输协议和完整的 Chrome 浏览器请求头集合,模拟真实浏览器访问行为
- 客户端提示头:自动从
MARKFETCH_USER_AGENT派生出Sec-CH-UA-*客户端提示头,确保与 Chrome UA 字符串一致 - 单文档处理:每次调用处理单个 URL,无递归爬取、无 robots.txt 解析、无速率限制编排
核心依赖组件
| 组件 | 版本 | 用途 |
|---|---|---|
@modelcontextprotocol/sdk | ^1.29.0 | MCP 协议实现,提供 stdio 通信能力 |
@mozilla/readability | ^0.5.0 | Mozilla 开源的 HTML 内容提取库,从页面中提取主要文章内容 |
turndown | ^7.0.0 | 将 HTML 转换为 Markdown 的转换器 |
turndown-plugin-gfm | ^1.0.2 | GitHub Flavored Markdown 插件,支持表格、任务列表等格式 |
linkedom | ^0.18.0 | 轻量级 DOM 解析器,用于在 Node.js 环境中解析和操作 HTML |
undici | ^8.2.0 | HTTP/2 客户端库,处理网络请求 |
zod | ^3.0.0 | TypeScript 类型验证库,用于 MCP 输入模式定义 |
commander | ^14.0.3 | CLI 参数解析库 |
系统架构
graph TD
subgraph "入口层"
A["src/index.ts<br/>参数分发器"]
end
subgraph "适配器层"
B["src/mcp.ts<br/>MCP 适配器"]
C["src/cli.ts<br/>CLI 适配器"]
end
subgraph "核心层"
D["src/core.ts<br/>fetchMarkdown 核心逻辑"]
end
subgraph "依赖库"
E["@mozilla/readability<br/>内容提取"]
F["turndown<br/>HTML→Markdown"]
G["undici<br/>HTTP 客户端"]
H["linkedom<br/>DOM 解析"]
end
subgraph "安全层"
I["src/sandbox.ts<br/>写入沙箱"]
end
A -->|"process.argv.length > 1"| C
A -->|"process.argv.length === 1"| B
B --> D
C --> D
D --> E
D --> F
D --> G
D --> H
D -->|"savePath 参数"| I目录结构
markfetch/
├── src/
│ ├── index.ts # 入口文件,argv 路由分发
│ ├── core.ts # 核心业务逻辑:获取→提取→转换
│ ├── mcp.ts # MCP 服务器适配器
│ ├── cli.ts # CLI 命令适配器
│ └── sandbox.ts # 写入路径安全检查
├── dist/ # TypeScript 编译输出目录
├── tests/ # 测试文件目录
├── package.json # 项目配置
├── README.md # 项目文档
└── CHANGELOG.md # 变更日志
工作流程
MCP 模式工作流程
sequenceDiagram
participant Client as MCP 客户端
participant Server as markfetch MCP 服务器
participant Core as 核心模块
participant Web as 目标 URL
Client->>Server: 调用 fetch_markdown(url, savePath?)
Server->>Core: fetchMarkdown({ url, savePath })
Core->>Web: HTTP/2 GET 请求 (Chrome UA)
Web-->>Core: HTML 响应
Core->>Core: Readability 提取文章内容
Core->>Core: Turndown 转换为 Markdown
alt savePath 存在
Core->>Server: 检查 savePath 安全性
Core->>Core: 写入文件
else savePath 不存在
Core->>Server: 返回 Markdown 内容
end
Server-->>Client: { content: [{ text: "..." }] }CLI 模式工作流程
graph LR
A["markfetch <url>"] --> B["解析参数"]
B --> C{"-o 参数?"}
C -->|"是"| D["解析输出路径"]
C -->|"否"| E["输出到 stdout"]
D --> F["调用 fetchMarkdown"]
E --> F
F --> G{"保存成功?"}
G -->|"是"| H["console.log 确认信息"]
G -->|"否"| I["console.error 错误信息"]
H --> J["process.exit(0)"]
I --> K["process.exit(1)"]错误处理机制
markfetch 定义了 8 种确定性错误代码,所有错误都遵循统一的 [code] message 格式返回:
| 错误代码 | 含义 | 触发场景 |
|---|---|---|
network_error | 网络错误 | DNS 解析失败、TCP 连接失败、TLS 握手失败、意外内部错误 |
http_error | HTTP 错误 | 目标服务器返回非 2xx 状态码 |
timeout | 请求超时 | 超过 MARKFETCH_TIMEOUT_MS 配置的超时时间 |
unsupported_content_type | 不支持的内容类型 | 响应不是 text/html 或 application/xhtml+xml |
extraction_failed | 提取失败 | Readability 无法提取任何文章内容(常见于纯客户端渲染的 SPA) |
too_large | 内容过大 | 响应体或提取后的 Markdown 超过 MARKFETCH_MAX_BYTES 限制 |
save_failed | 保存失败 | 指定了 savePath 但写入文件失败(目录不存在、权限不足等) |
save_forbidden | 保存禁止 | savePath 路径超出了允许的写入根目录 |
配置选项
环境变量配置
| 环境变量 | 默认值 | 说明 |
|---|---|---|
MARKFETCH_TIMEOUT_MS | 30000 | 单次请求超时时间(毫秒) |
MARKFETCH_MAX_BYTES | 5000000 | 响应体和提取后 Markdown 的字节数上限(约 5MB) |
MARKFETCH_USER_AGENT | Chrome 130 固定字符串 | 浏览器标识字符串。必须是 Chrome UA 格式,否则启动时快速失败 |
MARKFETCH_ALLOWED_WRITE_ROOTS | os.tmpdir() + process.cwd() | MCP 模式专用。以路径分隔符分隔的绝对路径列表,限定 savePath 可写入的根目录范围 |
MCP 配置示例
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_TIMEOUT_MS": "60000",
"MARKFETCH_USER_AGENT": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/130.0.0.0 Safari/537.36"
}
}
}
}
写入沙箱机制
MCP 模式下的 savePath 参数受写入沙箱限制,防止 AI 智能体将文件写入任意目录。CLI 模式不受此限制。
沙箱规则
- 默认根目录:系统临时目录 + 进程当前工作目录
- 路径检查:目标路径必须位于允许的根目录内或为其子路径
- 符号链接处理:符号链接指向允许根目录外的内容将被阻止
- 跨平台支持:POSIX 系统使用
:分隔符,Windows 使用;分隔符
自定义允许根目录
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_ALLOWED_WRITE_ROOTS": "/Users/me/markfetch-out:/tmp"
}
}
}
}
使用场景与限制
适用场景
- 获取文章、文档、博客帖子、新闻页面等静态 HTML 内容
- 自动化文档抓取和转换
- AI 智能体的网页内容获取工具
- 需要绕过基础反爬虫机制的网页访问
已知限制
| 限制类型 | 说明 |
|---|---|
| 不进行身份认证 | 仅支持匿名访问,不支持 Cookie、认证头或会话复用 |
| 非递归爬取 | 无多层级页面爬取能力,每次仅处理单个 URL |
| 不支持 SPA 渲染 | 纯客户端渲染(无静态 HTML)的 SPA 返回 extraction_failed |
| 遵循 robots.txt | 不解析也不遵守 robots.txt 规则 |
| Node.js 版本要求 | 需要 Node.js 24.0.0 或更高版本 资料来源:package.json |
快速开始
安装
npm install -g markfetch
CLI 使用
# 输出到标准输出
markfetch https://en.wikipedia.org/wiki/Markdown
# 输出到文件
markfetch https://example.com/article -o ./output/article.md
MCP 集成
#### Claude Code
claude mcp add --scope user markfetch -- npx -y markfetch
#### Codex
codex mcp add markfetch -- npx -y markfetch
#### Gemini CLI
gemini mcp add -s user markfetch npx -y markfetch
MCP 工具调用
// 工具名称
fetch_markdown
// 输入参数
{
url: "https://example.com/page", // 必填,绝对 URL
savePath: "/absolute/path/to/file.md" // 可选,保存路径
}
// 返回格式
{
content: [{ type: "text", text: "# Markdown 内容..." }]
}
版本信息
当前版本:0.6.0
项目源码托管于 GitHub:https://github.com/vasylenko/markfetch
许可证:MIT 资料来源:package.json
来源:https://github.com/vasylenko/markfetch / 项目说明书
系统架构
markfetch 是一个用于将网页转换为干净 Markdown 格式的工具,同时提供 CLI 和 MCP(Model Context Protocol)两种调用接口。项目采用适配器模式,将核心业务逻辑与接口层分离,确保核心逻辑在两种模式下完全一致。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
概述
markfetch 是一个用于将网页转换为干净 Markdown 格式的工具,同时提供 CLI 和 MCP(Model Context Protocol)两种调用接口。项目采用适配器模式,将核心业务逻辑与接口层分离,确保核心逻辑在两种模式下完全一致。
架构设计遵循以下原则:
- 纯 Node.js,无子进程:不依赖 Playwright、Chromium 或 Python
- 单通道输出:MCP 模式下仅使用
content[0].text,不使用structuredContent - 结构化错误:统一的 8 种错误码,适配器统一转换
- 惰性加载:stdout 预留给 MCP 帧,CLI 代码在 MCP 模式下永不加载
资料来源:README.md
组件架构
项目由五个核心模块组成,按职责可分为三层:
graph TD
subgraph "接口层 (Interface Layer)"
CLI[cli.ts<br/>CLI 适配器]
MCP[mcp.ts<br/>MCP 适配器]
end
subgraph "调度层 (Dispatch Layer)"
IDX[index.ts<br/>参数路由调度器]
end
subgraph "核心层 (Core Layer)"
CORE[core.ts<br/>核心业务逻辑]
SB[sandbox.ts<br/>写入沙箱]
end
CLI --> IDX
MCP --> IDX
IDX -->|lazy import| CLI
IDX -->|lazy import| MCP
CORE --> SB
MCP --> SB入口调度器 (index.ts)
index.ts 负责根据命令行参数决定启动模式:
| 条件 | 行为 |
|---|---|
process.argv.length === 2(无参数) | 启动 MCP stdio 服务器 |
| 有命令行参数 | 加载 CLI 适配器 |
// 伪代码实现
if (process.argv.length === 2) {
// 启动 MCP 服务器
import('./mcp.js').then(m => m.runMcpServer());
} else {
// 启动 CLI
import('./cli.js').then(c => c.runCli());
}
这种惰性导入机制确保 CLI 相关代码(包含 console.log 调用)在 MCP 模式下完全不可达。
资料来源:src/index.ts
CLI 适配器 (cli.ts)
CLI 适配器基于 commander 库实现,提供命令行界面:
支持的参数:
| 参数 | 说明 |
|---|---|
<url> | 必选,HTTP/HTTPS URL |
-o, --output <path> | 可选,输出文件路径 |
输出行为:
- stdout:Markdown 原始内容(无尾部换行符)
- stderr:
[code] message格式的错误信息 - exitCode:成功 0,失败 1
// CLI 核心逻辑伪代码
const savePath = options.output ? resolve(process.cwd(), options.output) : undefined;
const { markdown, bytes, savedTo } = await fetchMarkdown({ url, savePath });
if (savedTo !== undefined) {
console.log(`Saved ${bytes} bytes to ${savedTo}`);
} else {
process.stdout.write(markdown);
}
资料来源:src/cli.ts:1-62
MCP 适配器 (mcp.ts)
MCP 适配器基于 @modelcontextprotocol/sdk 实现标准化的 stdio 服务器:
注册的 Tool:
server.registerTool("fetch_markdown", {
inputSchema: {
url: z.string().url(),
savePath: z.string().refine(isAbsolute).optional()
}
})
返回格式(单通道,无 structuredContent):
{
"content": [
{ "type": "text", "text": "# Markdown content..." }
],
"isError": false
}
错误返回格式:
{
"content": [
{ "type": "text", "text": "[http_error] 404 Not Found" }
],
"isError": true
}
资料来源:src/mcp.ts
核心业务逻辑 (core.ts)
core.ts 包含完整的 URL 到 Markdown 转换管道:
graph LR
A[URL] --> B[undici HTTP 客户端]
B --> C{响应状态码?}
C -->|非 2xx| D[http_error]
C -->|2xx| E{Content-Type?}
E -->|非 HTML| F[unsupported_content_type]
E -->|HTML| G[decodeEncodedCodeTags]
G --> H[ensureBaseHref]
H --> I[rewriteForReadability]
I --> J[Readability 解析]
J -->|无内容| K[extraction_failed]
J -->|有内容| L[Turndown 转换]
L --> M{大小检查}
M -->|超限| N[too_large]
M -->|正常| O[Markdown 输出]主要函数:
| 函数 | 职责 |
|---|---|
fetchMarkdown() | 主入口,协调整个流程 |
extractArticle() | HTML 解析与内容提取 |
convertToMarkdown() | Markdown 转换与清理 |
rewriteForReadability() | 预处理器:处理脚注、折叠面板等 |
decodeEncodedCodeTags() | 解码 HTML 编码的 <code> 标签 |
ensureBaseHref() | 注入 <base href> 修复相对链接 |
关键设计决策:
- keepClasses: true:保留
<code class="language-X">以支持代码高亮提示 - Turndown escape 定制:禁用
\_和\-/\=的转义,避免噪声 - 标题去重:如果 Readability 保留了原始
<h1>,不重复添加标题 - 空标题修剪:移除连续的空标题节点
资料来源:src/core.ts
写入沙箱 (sandbox.ts)
MCP 模式的 savePath 参数受到写入沙箱限制:
默认允许路径:
os.tmpdir() ∪ process.cwd()
环境变量配置:
# POSIX
MARKFETCH_ALLOWED_WRITE_ROOTS="/custom/path:/tmp"
# Windows
MARKFETCH_ALLOWED_WRITE_ROOTS="C:\output;D:\temp"
验证算法:
graph TD
A[savePath] --> B{解析为绝对路径}
B -->|失败| C[save_failed]
B -->|成功| D{是否在允许路径内?}
D -->|是| E[允许写入]
D -->|否| F[save_forbidden]
G[符号链接] --> H[解析到真实路径后再检查]核心验证逻辑:
function isPathAllowed(path: string, roots: string[]): PathCheckResult {
const resolved = realpath(path);
for (const root of roots) {
const rel = relative(root, resolved);
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel))) {
return { ok: true, resolved };
}
}
return { ok: false, reason: `...' is outside allowed roots` };
}
注意:CLI 模式不执行沙箱检查——Shell 层面的用户是安全边界。
资料来源:src/sandbox.ts
数据流图
CLI 完整数据流
sequenceDiagram
participant User
participant CLI as cli.ts
participant Core as core.ts
participant Sandbox as sandbox.ts
participant FS as FileSystem
User->>CLI: markfetch <url> -o /path/out.md
CLI->>CLI: commander 解析参数
CLI->>Core: fetchMarkdown({ url, savePath })
Core->>Core: undici.fetch()
Core->>Core: Readability 解析
Core->>Core: Turndown 转换
Core->>Core: 大小检查
Core-->>CLI: { markdown, bytes, savedTo }
alt savePath 存在
CLI->>Core: 调用时传入 savePath
Core->>Sandbox: isPathAllowed(savePath)
Sandbox-->>Core: { ok: true, resolved }
Core->>FS: writeFile(savePath)
Core-->>CLI: { savedTo: path }
end
CLI->>User: Saved N bytes to /path/out.mdMCP 完整数据流
sequenceDiagram
participant LLM as LLM / Agent
participant MCP as MCP Client
participant Server as mcp.ts
participant Core as core.ts
participant Sandbox as sandbox.ts
LLM->>MCP: fetch_markdown({ url, savePath })
MCP->>Server: stdio JSON-RPC 请求
Server->>Core: fetchMarkdown()
Core->>Core: 处理流程...
alt savePath 在沙箱外
Core->>Sandbox: isPathAllowed()
Sandbox-->>Core: { ok: false }
Core-->>Server: throw MarkfetchError
Server-->>MCP: [save_forbidden] message
MCP-->>LLM: 错误响应
else 成功
Core-->>Server: { markdown }
Server-->>MCP: { content: [{ text }] }
MCP-->>LLM: Markdown 内容
end错误处理架构
所有错误统一通过 MarkfetchError 异常类传播,适配器负责转换为各自的格式:
graph TD
subgraph "错误源"
NE[network_error]
HE[http_error]
TO[timeout]
UC[unsupported_content_type]
EF[extraction_failed]
TL[too_large]
SF[save_failed]
SB[save_forbidden]
end
subgraph "异常传播"
ERR[MarkfetchError]
end
subgraph "适配器转换"
CLI_ERR[["console.error(`[${code}] ${msg}`)"]]
MCP_ERR[["errorResult() → isError: true"]]
end
NE --> ERR
HE --> ERR
TO --> ERR
UC --> ERR
EF --> ERR
TL --> ERR
SF --> ERR
SB --> ERR
ERR --> CLI_ERR
ERR --> MCP_ERR8 种错误码对照表:
| 错误码 | 含义 | CLI 行为 | MCP 行为 |
|---|---|---|---|
network_error | DNS/TCP/TLS 失败 | stderr 输出 | isError: true |
http_error | 非 2xx 状态 | stderr 输出 | isError: true |
timeout | 超过 MARKFETCH_TIMEOUT_MS | stderr 输出 | isError: true |
unsupported_content_type | 非 HTML 响应 | stderr 输出 | isError: true |
extraction_failed | Readability 无法提取内容 | stderr 输出 | isError: true |
too_large | 超过 MARKFETCH_MAX_BYTES | stderr 输出 | isError: true |
save_failed | 写入文件失败 | stderr 输出 | isError: true |
save_forbidden | savePath 在沙箱外 | 不适用 | isError: true |
资料来源:README.md
依赖关系
graph TD
subgraph "项目模块"
INDEX[src/index.ts]
CLI[src/cli.ts]
MCP[src/mcp.ts]
CORE[src/core.ts]
SANDBOX[src/sandbox.ts]
end
subgraph "生产依赖"
SDK[@modelcontextprotocol/sdk]
READABILITY[@mozilla/readability]
TURNDOWN[turndown]
LINKEDOM[linkedom]
UNDICI[undici]
COMMANDER[commander]
ZOD[zod]
end
INDEX --> CLI
INDEX --> MCP
CLI --> CORE
CLI --> COMMANDER
MCP --> SDK
MCP --> CORE
MCP --> ZOD
CORE --> READABILITY
CORE --> TURNDOWN
CORE --> LINKEDOM
CORE --> UNDICI
CORE --> SANDBOX关键依赖说明:
| 依赖 | 版本 | 用途 |
|---|---|---|
@modelcontextprotocol/sdk | ^0.6.x | MCP stdio 服务器实现 |
@mozilla/readability | ^0.5.x | HTML 文章内容提取 |
turndown | ^7.x | HTML 转 Markdown |
linkedom | ^0.18.x | 服务端 DOM 解析(替代 jsdom) |
undici | ^6.x | HTTP 客户端 |
commander | ^14.x | CLI 参数解析 |
zod | ^3.x | Schema 验证 |
资料来源:package.json
环境变量配置
| 变量 | 默认值 | 作用域 | 说明 |
|---|---|---|---|
MARKFETCH_TIMEOUT_MS | 30000 | 全部 | 单次请求超时(毫秒) |
MARKFETCH_MAX_BYTES | 5000000 | 全部 | 响应体和转换后 Markdown 的字节上限 |
MARKFETCH_USER_AGENT | Chrome 130 UA | 全部 | HTTP User-Agent,必须是 Chrome UA |
MARKFETCH_ALLOWED_WRITE_ROOTS | os.tmpdir() + process.cwd() | 仅 MCP | 沙箱允许的写入根目录列表 |
配置在启动时验证,无效值会快速失败并输出到 stderr。
资料来源:README.md
版本演进
| 版本 | 主要架构变更 |
|---|---|
| 0.4.0 | 引入 MCP 适配器,分离核心逻辑 |
| 0.5.0 | 引入 CLI 适配器,index.ts 惰性路由 |
| 0.5.0 | 错误处理统一为 MarkfetchError 抛出 |
| 0.6.0 | 引入写入沙箱 sandbox.ts |
资料来源:CHANGELOG.md
资料来源:[README.md](https://github.com/vasylenko/markfetch/blob/main/README.md)
安装与部署
markfetch 是一个用于将网页内容转换为 Markdown 格式的工具,支持 CLI 命令行和 MCP (Model Context Protocol) 两种使用模式。本页面详细介绍如何在不同环境中安装和部署 markfetch。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
系统要求
| 组件 | 最低版本 | 说明 |
|---|---|---|
| Node.js | ≥ 24 | 必须使用 ES Modules ("type": "module") |
| npm | - | 用于全局安装或本地依赖管理 |
| 操作系统 | 无限制 | 支持 Linux、macOS、Windows |
资料来源:package.json:7
markfetch 采用纯 Node.js 实现,不依赖 Playwright、Chromium 或 Python 等外部运行时环境。
安装方式
方式一:npm 全局安装(CLI 模式)
通过 npm 全局安装后,markfetch 命令将可在任意目录下使用:
npm i -g markfetch
安装完成后即可在命令行中使用:
markfetch https://en.wikipedia.org/wiki/Markdown
资料来源:README.md:48-54
方式二:npx 免安装运行
如不想全局安装,可直接使用 npx 临时下载并执行:
npx -y markfetch https://example.com
方式三:MCP Server 部署
markfetch 也可作为 MCP Server 部署到各种 AI 客户端。以下是常见客户端的配置方式:
#### Claude Code
claude mcp add --scope user markfetch -- npx -y markfetch
#### Codex
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"]
}
}
#### Gemini CLI
gemini mcp add -s user markfetch npx -y markfetch
#### 通用 MCP 配置(标准 JSON)
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"]
}
}
}
资料来源:README.md:56-95
方式四:本地源码运行
git clone https://github.com/vasylenko/markfetch.git
cd markfetch
npm install
npm run build
构建产物位于 dist/ 目录,入口文件为 dist/index.js。可使用以下方式调用:
# CLI 模式
node dist/index.js <url>
# MCP 模式(无参数启动)
node dist/index.js
资料来源:package.json:13
CLI 命令行使用
基本语法
markfetch <url> [options]
命令行参数
| 参数/选项 | 说明 |
|---|---|
<url> | 必填,目标网页的绝对 HTTP(S) URL |
-o, --output <path> | 可选,将 Markdown 保存到指定文件(绝对或相对路径) |
-V, --version | 打印版本号并退出 |
-h, --help | 打印帮助信息并退出 |
输出模式
CLI 模式根据是否指定输出路径有两种输出行为:
graph TD
A[执行 markfetch] --> B{是否指定 -o 参数?}
B -->|是| C[保存到文件]
B -->|否| D[输出到 stdout]
C --> E[打印确认信息: Saved X bytes to <path>]
D --> F[直接输出 Markdown 内容]- 无
-o参数:Markdown 内容直接输出到标准输出,无额外换行 - 指定
-o参数:写入文件后打印Saved {bytes} bytes to {path}
资料来源:src/cli.ts:26-33
错误处理
CLI 模式下,错误信息输出到 stderr,格式为:
[<error_code>] <error_message>
进程退出码为非零值。
资料来源:src/cli.ts:42-43
MCP Server 部署
工作模式
markfetch 支持通过 process.argv.length 自动区分 MCP 和 CLI 模式:
graph TD
A[启动 markfetch] --> B{process.argv.length}
B -->|等于 1| C[启动 MCP stdio Server]
B -->|大于 1| D[进入 CLI 模式]这种设计确保:
- 零参数启动自动进入 MCP 模式
- 现有 MCP 客户端配置无需任何修改
- CLI 代码 (
src/cli.ts) 在 MCP 模式下不会被加载
资料来源:README.md:38-40
MCP 工具接口
markfetch 注册了单一 MCP 工具 fetch_markdown:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 是 | 目标网页的绝对 HTTP(S) URL |
savePath | string | 否 | 绝对路径,用于保存 Markdown 到文件 |
返回结果在 content[0].text 中,不包含 structuredContent 字段,这是特意设计以确保与各类 MCP 客户端兼容。
资料来源:README.md:10-17
环境变量配置
markfetch 支持多个环境变量用于自定义行为。所有变量在启动时进行验证,无效值会导致快速失败并输出错误到 stderr。
| 环境变量 | 默认值 | 单位 | 说明 |
|---|---|---|---|
MARKFETCH_TIMEOUT_MS | 30000 | 毫秒 | 单次请求超时时间 |
MARKFETCH_MAX_BYTES | 5000000 | 字节 | 响应体和提取 Markdown 的最大大小(约 5MB) |
MARKFETCH_USER_AGENT | Chrome 130 | - | HTTP User-Agent,必须为 Chrome UA 字符串 |
MARKFETCH_ALLOWED_WRITE_ROOTS | os.tmpdir() + process.cwd() | - | MCP 模式允许写入的根目录列表 |
配置示例
在 MCP 配置文件(如 Claude Desktop 配置)中设置环境变量:
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_TIMEOUT_MS": "60000"
}
}
}
}
资料来源:README.md:104-115
写入沙箱安全机制
MCP 模式下,savePath 参数的写入操作受到安全沙箱限制。
工作原理
graph TD
A[MCP savePath 请求] --> B{路径合法性检查}
B --> C{绝对路径?}
C -->|否| D[拒绝: save_forbidden]
C -->|是| E{路径是否在允许根目录内?}
E -->|是| F[写入文件]
E -->|否| G[拒绝: save_forbidden]允许的写入根目录
默认允许的写入根目录为:
- 系统临时目录 (
os.tmpdir()) - 当前工作目录 (
process.cwd())
路径通过 fs.realpath 解析一次后缓存,确保符号链接被正确追踪。
自定义允许目录
使用 MARKFETCH_ALLOWED_WRITE_ROOTS 覆盖默认集合(注意:替换而非合并):
Linux/macOS (POSIX):
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_ALLOWED_WRITE_ROOTS": "/Users/me/markfetch-out:/tmp"
}
}
}
}
Windows:
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_ALLOWED_WRITE_ROOTS": "C:\\Users\\me\\markfetch-out;C:\\Users\\me\\AppData\\Local\\Temp"
}
}
}
}
安全特性
- 符号链接防护:检查时使用规范化后的路径,写入时也使用相同的规范化路径,防止通过
link/..逃逸 - Windows 大小写不敏感处理:Windows 文件系统大小写不敏感,使用
.toLowerCase()进行比较 - CLI 模式无沙箱:CLI 模式设计为由人类直接在 shell 中使用,不执行任何写入限制
资料来源:src/sandbox.ts:1-30, README.md:72-90
错误代码参考
markfetch 定义了 8 种确定性错误代码:
| 错误代码 | 含义 | 触发条件 |
|---|---|---|
network_error | 网络错误 | DNS、TCP、TLS 失败或内部错误 |
http_error | HTTP 错误 | 上游返回非 2xx 状态码 |
timeout | 超时 | 超过 MARKFETCH_TIMEOUT_MS 限制 |
unsupported_content_type | 不支持的类型 | 响应不是 text/html 或 application/xhtml+xml |
extraction_failed | 提取失败 | Readability 未找到文章内容(常见于纯客户端渲染 SPA) |
too_large | 内容过大 | 响应体或提取的 Markdown 超过 MARKFETCH_MAX_BYTES |
save_failed | 保存失败 | savePath 指定但写入文件失败(目录不存在、权限不足等) |
save_forbidden | 保存被禁止 | savePath 超出允许的写入根目录 |
资料来源:README.md:100-108
版本历史
| 版本 | 发布日期 | 关键变更 |
|---|---|---|
| 0.6.0 | - | MCP 写入沙箱安全修复 |
| 0.5.1 | - | MARKFETCH_ALLOWED_WRITE_ROOTS 环境变量支持 |
| 0.5.0 | 2026-05-12 | 新增 CLI 模式 |
| 0.4.0 | 2026-05-10 | 新增 MCP Server 支持 |
| 0.4.1 | 2026-05-11 | 修复 package.json bin 字段问题 |
资料来源:CHANGELOG.md:1-45
资料来源:[package.json:7]()
命令行界面
markfetch 提供两种运行模式:MCP(Model Context Protocol)stdio 服务器模式和命令行界面(CLI)模式。CLI 模式允许用户直接从终端获取网页内容并将其转换为 Markdown 格式输出。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
概述
markfetch 提供两种运行模式:MCP(Model Context Protocol)stdio 服务器模式和命令行界面(CLI)模式。CLI 模式允许用户直接从终端获取网页内容并将其转换为 Markdown 格式输出。
命令行界面的核心职责包括:解析用户输入的 URL 和命令行参数、调用核心提取模块处理网页、将 Markdown 结果输出到标准输出或指定文件、以及以结构化格式报告错误信息。
CLI 适配器位于 src/cli.ts,采用懒加载机制——只有当进程检测到命令行参数时才加载该模块,确保 MCP 模式下不会引入任何控制台输出代码。
工作流程
graph TD
A[启动 markfetch] --> B{命令行参数数量}
B -->|0 个参数| C[启动 MCP stdio 服务器]
B -->|1+ 个参数| D[加载 CLI 适配器]
D --> E[解析 URL 和选项]
E --> F[调用 core.fetchMarkdown]
F --> G{savePath 存在?}
G -->|是| H[写入文件并输出确认信息]
G -->|否| I[输出 Markdown 到 stdout]
F -->|异常| J[输出错误到 stderr 并退出]
H --> K[process.exitCode = 0]
I --> KCLI 适配器遵循统一错误处理约定:所有核心模块抛出的 MarkfetchError 异常由适配器捕获并转换为带错误代码的标准错误输出。
安装与调用
全局安装
npm i -g markfetch
安装后,markfetch 命令全局可用。
基本用法
markfetch <url>
将 URL 对应的网页内容提取为 Markdown 并输出到标准输出。
输出到文件
markfetch <url> -o <path>
markfetch <url> --output <path>
路径可以是绝对路径或相对于当前工作目录的相对路径。
命令行选项
| 选项 | 说明 |
|---|---|
<url> | 必选参数,目标网页的绝对 HTTP/HTTPS URL |
-o, --output <path> | 可选,将 Markdown 保存到指定文件路径 |
-V, --version | 打印版本号并退出 |
-h, --help | 打印帮助信息并退出 |
资料来源:src/cli.ts:1-10
输出机制
CLI 适配器对输出的处理遵循"标准输出保留给 Markdown 内容"的原则:
- 无 savePath:原始 Markdown 内容直接写入 stdout,不添加额外换行符,与 MCP 的
content[0].text格式保持一致 - 有 savePath:写入文件后输出确认信息
Saved <bytes> bytes to <path>到 stdout
if (savedTo !== undefined) {
// 确认信息——CLI 唯一添加的 stdout 换行
console.log(`Saved ${bytes} bytes to ${savedTo}`);
} else {
// 原始 Markdown 内容——无额外换行
process.stdout.write(markdown);
}
资料来源:src/cli.ts:37-47
错误处理
CLI 采用与 MCP 适配器一致的错误格式,通过 classifyError 函数将异常分类并输出:
[<error_code>] <error_message>
错误信息输出到 stderr,并设置 process.exitCode = 1 确保管道脚本能够检测到失败状态。
} catch (err) {
const { code, message } = classifyError(err);
console.error(`[${code}] ${message}`);
process.exitCode = 1;
}
资料来源:src/cli.ts:48-53
错误代码对照表
| 错误代码 | 含义 |
|---|---|
network_error | DNS、TCP、TLS 连接失败或内部网络错误 |
http_error | 目标服务器返回非 2xx 状态码 |
timeout | 请求超时(默认 30 秒,可通过 MARKFETCH_TIMEOUT_MS 配置) |
unsupported_content_type | 响应不是 HTML 内容 |
extraction_failed | Readability 无法提取文章内容(常见于纯客户端渲染的 SPA) |
too_large | 内容超过 MARKFETCH_MAX_BYTES 限制(默认 5MB) |
save_failed | 文件写入失败(目录不存在、权限不足等) |
save_forbidden | savePath 超出允许的写入根目录 |
资料来源:README.md:1-100
路径解析规则
CLI 在调用核心模块之前统一处理路径解析:
const savePath = options.output
? resolve(process.cwd(), options.output)
: undefined;
规则说明:
- 绝对路径保持不变
- 相对路径相对于
process.cwd()解析 - 不执行波浪号(
~)展开——由 shell 在 argv 到达进程前处理 - 核心模块接收的始终是绝对路径,确保行为一致性
资料来源:src/cli.ts:20-27
与 MCP 适配器的对比
graph TD
subgraph CLI 模式
A1[命令行参数] --> B1[cli.ts 适配器]
B1 --> C1[console.log 控制输出]
B1 --> C2[console.error 控制错误]
end
subgraph MCP 模式
A2[stdio 帧] --> B2[mcp.ts 适配器]
B2 --> C3[MCP JSON-RPC 响应]
B2 --> C4[structuredContent 格式]
end
subgraph 共享核心
D[core.fetchMarkdown]
E[MarkfetchError]
F[extractArticle]
end
B1 --> D
B2 --> D
C1 -.不使用.-> C3
C2 -.不使用.-> C4关键差异:
| 特性 | CLI 模式 | MCP 模式 |
|---|---|---|
| 交互协议 | 终端 stdin/stdout | stdio JSON-RPC |
| 输出格式 | 原始 Markdown | MCP content 数组 |
| 沙箱写入 | 无限制 | 受 MARKFETCH_ALLOWED_WRITE_ROOTS 限制 |
| 控制台输出 | 可用 | 不可用 |
CLI 模式没有沙箱限制——shell 用户是安全边界,因此允许写入任意路径。MCP 模式由于由语言模型驱动,引入写入沙箱以防止路径遍历攻击。
资料来源:src/mcp.ts:1-50 资料来源:src/sandbox.ts:1-50
分发器机制
src/index.ts 负责根据进程参数数量决定加载哪个适配器:
// 懒加载分发逻辑
// process.argv.length <= 1: 仅 node 命令,无参数
// process.argv.length == 2: 仅命令名(如 'node markfetch'),启动 MCP
// process.argv.length > 2: 有额外参数,启动 CLI
这种设计确保:
- 零参数启动时始终进入 MCP 模式,保持现有配置的兼容性
- 任何命令行参数触发 CLI 模式
- 适配器模块懒加载,MCP 模式下永不引入 CLI 代码路径
资料来源:src/index.ts
环境变量配置
CLI 与 MCP 共享以下环境变量:
| 变量名 | 默认值 | 说明 |
|---|---|---|
MARKFETCH_TIMEOUT_MS | 30000 | 单次请求超时(毫秒) |
MARKFETCH_MAX_BYTES | 5000000 | 响应体和提取 Markdown 的字节上限 |
MARKFETCH_USER_AGENT | Chrome 130 UA 字符串 | HTTP User-Agent 头 |
注意:MARKFETCH_ALLOWED_WRITE_ROOTS 仅在 MCP 模式下生效,CLI 模式不受此限制。
资料来源:README.md:100-150
典型使用场景
管道处理
markfetch https://example.com/article | grep -A5 "## Introduction"
保存长文档
markfetch https://docs.example.com/guide -o /tmp/guide.md
批量脚本集成
#!/bin/bash
for url in "${urls[@]}"; do
markfetch "$url" -o "/output/$(basename "$url").md" || echo "Failed: $url" >&2
done
版本历史
- 0.5.0 (2026-05-12):新增 CLI 模式,采用 commander.js 进行参数解析
- 0.6.0:当前版本,CLI 与 MCP 双模式稳定运行
资料来源:CHANGELOG.md
资料来源:[src/cli.ts:1-10](https://github.com/vasylenko/markfetch/blob/main/src/cli.ts#L1-L10)
MCP 服务器
markfetch 的 MCP 服务器是基于 Model Context Protocol (MCP) 的标准输入输出(stdio)服务器实现,提供 fetchmarkdown 工具供 AI 代理使用。该服务器通过单一工具接口将网页内容转换为干净的 Markdown 格式,支持直接返回内容或写入文件系统。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
概述
markfetch 的 MCP 服务器是基于 Model Context Protocol (MCP) 的标准输入输出(stdio)服务器实现,提供 fetch_markdown 工具供 AI 代理使用。该服务器通过单一工具接口将网页内容转换为干净的 Markdown 格式,支持直接返回内容或写入文件系统。
graph TD
A[MCP 客户端] -->|stdio| B[markfetch MCP 服务器]
B --> C[src/index.ts 分发器]
C -->|无参数| D[MCP 模式]
C -->|有参数| E[CLI 模式]
D --> F[src/mcp.ts]
E --> G[src/cli.ts]
F --> H[src/core.ts]
G --> H
H --> I[fetchMarkdown 核心逻辑]架构设计
双模式入口
markfetch 采用单一二进制文件支持两种运行模式,通过 process.argv.length 在运行时自动判断: 资料来源:src/index.ts:1-15
| 模式 | 触发条件 | 入口文件 | 输出通道 |
|---|---|---|---|
| MCP stdio 服务器 | 无命令行参数 | src/mcp.ts | stdout 用于 MCP 协议帧 |
| CLI 工具 | 存在命令行参数 | src/cli.ts | stdout 用于 markdown 输出 |
关键设计原则:stdout 保留给 MCP 协议帧使用,stderr 仅用于致命错误输出,确保 stdio 通道的纯净性。 资料来源:src/cli.ts:45-48
MCP 服务器初始化
服务器使用 @modelcontextprotocol/sdk 包提供的 McpServer 类进行初始化:
const server = new McpServer({ name: "markfetch", version: "0.6.0" });
版本号与 package.json 中的 version 字段保持一致,确保 MCP 客户端能获取准确的版本信息。 资料来源:src/mcp.ts:12
工具定义
fetch_markdown 工具
fetch_markdown 是 markfetch 提供的唯一 MCP 工具,具有以下特性:
| 特性 | 说明 |
|---|---|
| 返回通道 | content[0].text 单一通道,无 structuredContent |
| 适用场景 | 文章、文档、博客帖子、新闻、参考页面 |
| 限制 | 匿名获取,无认证支持 |
#### 输入参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 是 | 完整的 HTTP/HTTPS URL,服务器自动跟随重定向 |
savePath | string | 否 | 绝对路径,将 markdown 写入文件而非返回 |
url 参数使用 Zod schema 进行验证:
url: z
.string()
.url()
.describe("Absolute http(s) URL of the page to fetch...")
savePath 必须为绝对路径:
savePath: z
.string()
.refine(isAbsolute, "savePath must be an absolute filesystem path")
.optional()
资料来源:src/mcp.ts:14-38
#### 返回格式
成功响应:
{
"content": [
{
"type": "text",
"text": "# Markdown Content Here..."
}
]
}
错误响应:
{
"content": [
{
"type": "text",
"text": "[error_code] Error message"
}
],
"isError": true
}
错误处理
错误代码体系
MCP 服务器返回 8 种确定性错误代码:
| 错误代码 | 含义 | 触发条件 |
|---|---|---|
network_error | 网络故障 | DNS/TCP/TLS 失败或内部错误 |
http_error | HTTP 错误 | 上游返回非 2xx 状态码 |
timeout | 请求超时 | 超过 MARKFETCH_TIMEOUT_MS 配置的时间 |
unsupported_content_type | 不支持的类型 | 响应不是 text/html 或 application/xhtml+xml |
extraction_failed | 提取失败 | Readability 无法提取文章内容(常见于纯客户端渲染的 SPA) |
too_large | 内容过大 | 响应体或提取的 markdown 超过 MARKFETCH_MAX_BYTES |
save_failed | 保存失败 | 写入文件失败(目录不存在、权限问题等) |
save_forbidden | 保存禁止 | savePath 超出允许的写入根目录 |
资料来源:src/mcp.ts:4-9
错误结果生成
function errorResult(code: ErrorCode, message: string) {
return {
content: [{ type: "text" as const, text: `[${code}] ${message}` }],
isError: true,
};
}
所有错误统一通过 errorResult() 函数格式化,确保错误消息格式的一致性:[错误代码] 错误描述。 资料来源:src/mcp.ts:4-9
配置选项
MCP 服务器支持以下环境变量配置:
| 环境变量 | 默认值 | 说明 |
|---|---|---|
MARKFETCH_TIMEOUT_MS | 30000 | 单次请求超时时间(毫秒) |
MARKFETCH_MAX_BYTES | 5000000 | 响应体和提取 markdown 的最大字节数 |
MARKFETCH_USER_AGENT | Chrome 130 固定字符串 | HTTP User-Agent,必须为 Chrome UA |
MARKFETCH_ALLOWED_WRITE_ROOTS | os.tmpdir() + process.cwd() | MCP 模式允许的写入根目录列表 |
写入沙箱
MCP 模式的 savePath 写入被限制在允许的根目录集合内。默认情况下包含系统临时目录和进程工作目录。 资料来源:src/sandbox.ts:1-10
graph LR
A[savePath] --> B{检查是否在允许根目录内}
B -->|是| C[允许写入]
B -->|否| D[返回 save_forbidden 错误]
E[默认允许根目录] --> B
F[MARKFETCH_ALLOWED_WRITE_ROOTS] --> E#### 自定义允许根目录
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_ALLOWED_WRITE_ROOTS": "/Users/me/markfetch-out:/tmp"
}
}
}
}
设置此变量会完全替换默认根目录,而非合并。如需保留默认值,必须显式包含。 资料来源:README.md:Configuration
#### 路径解析逻辑
sandbox.ts 中的路径验证逻辑:
- 解析
savePath为绝对路径 - 遍历所有允许根目录
- 检查目标路径是否在任一允许根目录内
- Windows 平台使用不区分大小写的比较
// Win32 case-fold: filesystem is case-insensitive
const fold = process.platform === "win32"
? (s: string) => s.toLowerCase()
: (s: string) => s;
资料来源:src/sandbox.ts:32-36
MCP 客户端集成
Claude Code
claude mcp add --scope user markfetch -- npx -y markfetch
Codex
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"]
}
}
}
Gemini CLI
gemini mcp add -s user markfetch npx -y markfetch
本地路径配置
对于需要使用本地构建版本的场景:
{
"mcpServers": {
"markfetch": {
"command": "node",
"args": ["/absolute/path/to/markfetch/dist/index.js"]
}
}
}
资料来源:README.md:MCP install commands
与 CLI 模式的区别
| 特性 | MCP 服务器 | CLI 工具 |
|---|---|---|
| 调用方式 | stdio 协议 | 命令行参数 |
| 输出通道 | content[0].text | stdout 直接输出 |
| 写入沙箱 | 启用 | 禁用(无限制) |
| 错误输出 | isError: true | stderr + 退出码 |
| 相对路径处理 | 不支持(需绝对路径) | 支持(相对 cwd 解析) |
CLI 模式中相对路径会被解析为绝对路径后再传给核心逻辑:
const savePath = options.output
? resolve(process.cwd(), options.output)
: undefined;
资料来源:src/cli.ts:13-20
版本历史
| 版本 | 发布日期 | MCP 相关变更 |
|---|---|---|
| 0.6.0 | 当前版本 | 稳定版 MCP 实现 |
| 0.5.0 | 2026-05-12 | 引入 CLI 模式,源文件重构为独立模块 |
| 0.4.0 | 2026-05-10 | 初始 MCP 工具 fetch_markdown |
0.4.0 版本引入了原始的 MCP 实现,0.5.0 版本将源码重构为 src/mcp.ts(MCP 适配器)、src/cli.ts(CLI 适配器)和 src/core.ts(核心管道 + 错误处理)的分离结构,同时保持了 MCP 消费者的公共 API 完全兼容。 资料来源:CHANGELOG.md:版本历史
安全考量
MCP 模式的安全设计
- 写入沙箱隔离:MCP 工具由语言模型驱动,可能被页面内容引导,因此
savePath限制在可配置的允许目录内 - CLI 模式无沙箱:命令行由人类直接使用,作为安全边界,不施加写入限制
- Symlink 防护:符号链接指向允许根目录外的路径会被阻止 资料来源:README.md:Write sandbox
User-Agent 要求
MARKFETCH_USER_AGENT 必须为 Chrome 用户代理字符串。非 Chrome 字符串会在启动时快速失败,防止因客户端提示不匹配导致的问题。 资料来源:README.md:Configuration
资料来源:[src/mcp.ts:14-38]()
HTTP 指纹与请求模拟
markfetch 在发起 HTTP 请求时模拟真实浏览器的指纹特征,以绕过网站的反爬虫机制。其核心设计理念是使每个请求在 HTTP 协议层面与真实 Chrome 浏览器发出的请求无法区分,从而在不支持 JavaScript 渲染的环境中也能获取到完整的页面内容。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
概述
markfetch 在发起 HTTP 请求时模拟真实浏览器的指纹特征,以绕过网站的反爬虫机制。其核心设计理念是使每个请求在 HTTP 协议层面与真实 Chrome 浏览器发出的请求无法区分,从而在不支持 JavaScript 渲染的环境中也能获取到完整的页面内容。
markfetch 使用 HTTP/2 传输层 结合 一致的 Chrome 请求头集合,并通过 MARKFETCH_USER_AGENT 环境变量动态生成 Sec-CH-UA-* 客户端提示头,确保请求指纹的真实性与可配置性。
资料来源:README.md:47
核心机制
HTTP/2 传输
markfetch 默认使用 HTTP/2 协议进行网络请求。相比 HTTP/1.1,HTTP/2 的多路复用、头部压缩和服务器推送特性使得请求模式更接近现代浏览器的实际行为。
Chrome 请求头集
项目预置了一套完整的 Chrome 请求头集合,覆盖了以下关键头字段:
| 头字段类型 | 说明 |
|---|---|
User-Agent | Chrome 130 版本标识 |
Sec-CH-UA-* | 客户端提示头,基于 User-Agent 动态派生 |
Accept | 内容协商头 |
Accept-Language | 语言偏好 |
Accept-Encoding | 压缩算法支持 |
这套头字段组合确保了请求在 TLS 握手后的 HTTP 层与真实 Chrome 浏览器保持一致。
配置项
环境变量配置
| 变量名 | 默认值 | 用途 |
|---|---|---|
MARKFETCH_TIMEOUT_MS | 30000 | 单次请求超时时间(毫秒) |
MARKFETCH_MAX_BYTES | 5000000 | 响应体与提取后 markdown 的字节数上限 |
MARKFETCH_USER_AGENT | Chrome 130 固定版本字符串 | 覆盖默认 UA,必须为 Chrome UA 格式 |
MARKFETCH_USER_AGENT 的值在进程启动时派生 Sec-CH-UA-* 客户端提示。如果传入非 Chrome 格式的字符串,程序会在启动时快速失败并在 stderr 输出错误。
资料来源:README.md:88-93
配置示例
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_USER_AGENT": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"MARKFETCH_TIMEOUT_MS": "60000"
}
}
}
}
请求流程
graph TD
A[入口: fetchMarkdown] --> B[验证环境变量]
B --> C{配置有效?}
C -->|无效| D[启动时快速失败]
C -->|有效| E[构造 HTTP/2 请求]
E --> F[附加 Chrome 请求头集]
F --> G[基于 UA 派生 Sec-CH-UA-*]
G --> H[发送请求到目标 URL]
H --> I{响应状态}
I -->|2xx| J[进入内容提取流程]
I -->|非 2xx| K[抛出 http_error]
J --> L[解码 HTML 实体]
L --> M[注入 base href]
M --> N[解析 DOM 树]
N --> O[Readability 提取]
O --> P[Turndown 转 Markdown]基础 URL 注入机制
由于 linkedom 解析器在文档没有 <base href> 时会将相对路径保留为 /wiki/... 形式,markfetch 在 extractArticle 函数中通过 ensureBaseHref 注入重定向后的规范 URL,确保后续的相对链接能够被正确解析。
function ensureBaseHref(html: string, url: string): string {
const safeUrl = url.replaceAll("&", "&").replaceAll('"', """);
const stripped = html.replaceAll(/<base\s[^>]*>/gi, "");
if (/<head\b[^>]*>/i.test(stripped)) {
return stripped.replace(
/<head\b([^>]*)>/i,
`<head$1><base href="${safeUrl}">`,
);
}
if (/<html\b[^>]*>/i.test(stripped)) {
return stripped.replace(
/<html\b([^>]*)/i,
`<html$1><head><base href="${safeUrl}"></head>`,
);
}
return stripped;
}
资料来源:src/core.ts:18-33
该函数会:
- 对 URL 中的特殊字符进行 HTML 实体转义
- 移除页面中已有的
<base>标签(上游 URL 更具权威性) - 将规范 URL 注入到
<head>或<html>标签中
请求适配层
MCP 适配器
MCP 适配器 (src/mcp.ts) 接收已验证的输入(URL 语法由适配器的 schema 校验,savePath 为绝对路径),并将错误统一映射为 MarkfetchError。
const server = new McpServer({ name: "markfetch", version: "0.6.0" });
server.registerTool(
"fetch_markdown",
{
description:
"Fetch a single public HTTP/S URL and return its main article content as clean markdown...",
inputSchema: {
url: z.string().url().describe("Absolute http(s) URL..."),
savePath: z.string().refine(isAbsolute, "savePath must be an absolute filesystem path").optional()
}
}
);
资料来源:src/mcp.ts:22-38
CLI 适配器
CLI 适配器 (src/cli.ts) 将相对输出路径解析为绝对路径后再传递给核心模块:
const savePath = options.output
? resolve(process.cwd(), options.output)
: undefined;
CLI 模式下的错误通过 stderr 输出,格式为 [code] message,并设置 process.exitCode = 1。
资料来源:src/cli.ts:28-32
错误码体系
markfetch 定义了 8 种确定性错误码,用于标识请求和处理过程中的各类失败场景:
| 错误码 | 含义 | 触发条件 |
|---|---|---|
network_error | DNS/TCP/TLS 故障或内部错误 | 网络层异常 |
http_error | 上游返回非 2xx 状态 | HTTP 响应状态码 ≥ 400 |
timeout | 超过 MARKFETCH_TIMEOUT_MS | 请求超时 |
unsupported_content_type | 响应不是 HTML 类型 | Content-Type 非 text/html 或 application/xhtml+xml |
extraction_failed | Readability 未提取到内容 | 纯客户端渲染的 SPA |
too_large | 超过 MARKFETCH_MAX_BYTES | 响应体或 markdown 超限 |
save_failed | 文件写入失败 | 目录不存在或权限不足 |
save_forbidden | 路径超出允许的写入根目录 | MCP 模式下路径未通过沙箱校验 |
资料来源:README.md:74-82
与其他方案的对比
| 方案 | 真实浏览器指纹 | Reader-View 提取 | 结构化错误 | 零配置 |
|---|---|---|---|---|
| 内置 Agent fetch 工具 | ✗ | ✗ | ✗ | ✓ |
| 通用 Playwright/Puppeteer | ✓ | ✗ | ✗ | ✗ |
| mcp-server-fetch (Python) | ✗ | 基础 | ✗ | ✗ |
| CloudFlare /markdown | ✗ | ✓ | ✗ | ✗ |
| markfetch | ✓ | ✓ | ✓ | ✓ |
markfetch 的独特优势在于同时实现了真实浏览器指纹和 Reader-View 风格的内容提取,而无需运行无头浏览器。
资料来源:README.md:55-62
设计原则
单进程架构
markfetch 采用纯 Node.js 实现,不依赖 Playwright、headless Chromium 或 Python 子进程。这使得:
- 启动开销极低
- 内存占用可控
- 适合在 MCP stdio 服务器等受限环境中运行
Stdio 清洁原则
- stdout 保留给 MCP 协议帧
- stderr 仅用于致命错误
- 无日志输出、无 ANSI 转义码
MCP 与 CLI 的行为一致性
两种调用方式使用相同的核心模块,唯一的差异在于:
- MCP 模式启用写入沙箱 (
save_forbidden错误码) - CLI 模式完全不受限(人类用户是安全边界)
资料来源:README.md:49-54
版本历史
| 版本 | 变更内容 |
|---|---|
| 0.4.0 | 引入 HTTP/2 传输和 Chrome 请求头集,MARKFETCH_USER_AGENT 环境变量 |
| 0.5.0 | 新增 CLI 模式,保持 MCP 和 CLI 行为一致 |
| 0.6.0 | 完善错误码体系和环境变量验证 |
资料来源:CHANGELOG.md:1-15
资料来源:[README.md:47]()
内容提取管道
内容提取管道(Content Extraction Pipeline)是 markfetch 项目的核心模块,负责将任意 HTTP/HTTPS URL 的 HTML 内容转换为干净的 Markdown 格式。该管道是纯 Node.js 实现,不依赖 Playwright 或无头浏览器,通过 HTTP/2 传输和完整的 Chrome 请求头集实现真实浏览器指纹模拟。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
概述
内容提取管道(Content Extraction Pipeline)是 markfetch 项目的核心模块,负责将任意 HTTP/HTTPS URL 的 HTML 内容转换为干净的 Markdown 格式。该管道是纯 Node.js 实现,不依赖 Playwright 或无头浏览器,通过 HTTP/2 传输和完整的 Chrome 请求头集实现真实浏览器指纹模拟。
资料来源:README.md:1-15
架构总览
markfetch 的内容提取管道遵循经典的"获取-解析-转换"三阶段架构。整个管道从 URL 输入开始,依次经过网络请求层、内容解析层和 Markdown 转换层,最终输出结构化的 Markdown 文档。
graph TD
A[URL 输入] --> B[HTTP/2 网络请求]
B --> C{响应状态码}
C -->|2xx| D[HTML 内容]
C -->|非2xx| E[http_error]
D --> F[内容类型检测]
F -->|text/html| G[解码编码标签]
F -->|非HTML| I[unsupported_content_type]
G --> H[注入 Base Href]
H --> J[Readability 解析]
J --> K{提取结果}
K -->|有内容| L[转换为 Markdown]
K -->|无内容| M[extraction_failed]
L --> N[大小检查]
N -->|未超限| O[返回结果]
N -->|超出限制| P[too_large]
O --> Q{savePath}
Q -->|指定路径| R[沙盒写入]
Q -->|标准输出| S[stdout 输出]资料来源:src/core.ts:1-100
核心组件
核心模块结构
markfetch 的核心提取逻辑集中在 src/core.ts 中,包含以下关键函数:
| 函数名 | 职责 | 返回值 | |
|---|---|---|---|
decodeEncodedCodeTags() | 解码 HTML 实体编码的代码标签 | string | |
ensureBaseHref() | 注入 <base> 标签确保相对路径正确解析 | string | |
rewriteForReadability() | 重写 DOM 结构以优化提取效果 | void | |
extractArticle() | 使用 Readability 提取文章内容 | `{title, content} \ | null` |
convertToMarkdown() | 将 HTML 转换为 Markdown | string |
资料来源:src/core.ts:1-150
依赖库
| 库名 | 版本 | 用途 |
|---|---|---|
@mozilla/readability | 最新稳定版 | 从 HTML 中提取主要文章内容 |
turndown | 最新稳定版 | 将 HTML DOM 转换为 Markdown |
linkedom | 最新稳定版 | 轻量级 DOM 解析器 |
undici | 最新稳定版 | HTTP/2 客户端 |
资料来源:package.json:1-30
管道各阶段详解
阶段一:HTML 实体解码
function decodeEncodedCodeTags(html: string): string {
return html.replaceAll(
/<(\/?(?:code|pre)(?:\s[^&]*?)?\/?)>/g,
(_, tag) => `<${tag}>`,
);
}
此函数专门处理 HTML 实体编码的 <code> 和 <pre> 标签。由于代码块在转换过程中需要保留原始格式,markfetch 需要将这些被实体编码的标签转换回标准 HTML 标签形式。
正则表达式说明:
<- 匹配左尖括号的实体编码形式\/?- 可选的闭合斜杠(?:code|pre)- 匹配code或pre标签名(?:\s[^&]*?)?- 可选的属性部分>- 匹配右尖括号的实体编码形式
资料来源:src/core.ts:10-18
阶段二:Base Href 注入
function ensureBaseHref(html: string, url: string): string {
const safeUrl = url.replaceAll("&", "&").replaceAll('"', """);
const stripped = html.replaceAll(/<base\s[^>]*>/gi, "");
if (/<head\b[^>]*>/i.test(stripped)) {
return stripped.replace(
/<head\b([^>]*)>/i,
`<head$1><base href="${safeUrl}">`,
);
}
if (/<html\b[^>]*>/i.test(stripped)) {
return stripped.replace(
/<html\b([^>]*)>/i,
`<html$1><head><base href="${safeUrl}"></head>`,
);
}
return stripped;
}
此函数的目的是确保相对链接和图片路径能够正确解析。由于 linkedom 解析器不会自动填充 baseURI,导致相对路径(如 /wiki/...)无法正确转换为绝对 URL。管道通过以下步骤解决此问题:
- 移除页面原有的
<base>标签(避免冲突) - 在
<head>标签内注入新的<base href>指向最终重定向后的 URL - 如果没有
<head>标签,则在<html>标签后创建新的<head>
资料来源:src/core.ts:25-50
阶段三:DOM 重写优化
function rewriteForReadability(document: Document): void {
// 处理脚注aside元素
const footnoteAsides = document.querySelectorAll(
'aside.footnote-brackets, ' +
'aside[role="doc-endnotes"], aside[role="doc-footnote"], aside[role="doc-footnotes"]',
);
// 处理details/summary折叠元素
for (const el of Array.from(document.querySelectorAll("details"))) {
const parent = el.parentNode;
if (!parent) continue;
while (el.firstChild) parent.insertBefore(el.firstChild, el);
el.remove();
}
// 处理 MediaWiki 样式标题容器
for (const el of Array.from(document.querySelectorAll("div.mw-heading"))) {
const heading = el.querySelector("h1, h2, h3, h4, h5, h6");
if (!heading) continue;
el.parentNode?.replaceChild(heading, el);
}
}
此函数对 DOM 结构进行预处理,使 Readability 能够更准确地提取主要内容:
| 处理类型 | 选择器 | 处理方式 |
|---|---|---|
| 脚注区域 | aside.footnote-* | 替换为 <section> 以保留内容 |
| 折叠元素 | details | 展开内容并移除容器 |
| 标题容器 | div.mw-heading | 提换为纯标题元素 |
资料来源:src/core.ts:75-115
阶段四:Readability 内容提取
function extractArticle(
html: string,
url: string,
): { title: string; content: string } | null {
const decoded = decodeEncodedCodeTags(html);
const withBase = ensureBaseHref(decoded, url);
const { document } = parseHTML(withBase);
rewriteForReadability(document);
const article = new Readability(document, {
keepClasses: true,
}).parse();
if (!article?.content?.trim()) return null;
return { title: (article.title ?? "").trim(), content: article.content };
}
Readability 是 Mozilla 开发的专门用于从网页中提取主要文章内容的库。markfetch 的配置使用 keepClasses: true 选项,目的是保留 <code> 元素的 class="language-X" 属性,使 turndown 能够在代码块中输出语言提示标记。
资料来源:src/core.ts:118-145
阶段五:Markdown 转换
function convertToMarkdown(article: {
title: string;
content: string;
}): string {
const body = TURNDOWN.turndown(article.content);
// 如果 Readability 保留了页面的 <h1>,不重复添加标题
const contentLeadsWithH1 = /^\s*<h1[\s>]/i.test(article.content);
let result = article.title && !contentLeadsWithH1
? `# ${article.title}\n\n${body}`
: body;
// 移除空标题
result = pruneEmptyHeadings(result);
// 处理代码块语言提示
result = patchCodeFenceLanguageHints(result);
return result;
}
Turndown 配置了自定义的转义规则,以解决以下问题:
- 行内下划线保护:Markdown 中行内代码外的下划线通常需要转义,但 markfetch 保留了未转义的形式以提高可读性
- 标题下划线处理:CommonMark 的 setext 标题使用
=或-字符,但后面紧跟字母数字字符的情况不是标题,应保留原样
资料来源:src/core.ts:50-75
错误处理机制
错误代码表
| 错误代码 | 含义 | 触发条件 |
|---|---|---|
network_error | 网络层故障 | DNS 解析、TCP 连接、TLS 握手失败 |
http_error | HTTP 协议错误 | 服务器返回非 2xx 状态码 |
timeout | 请求超时 | 超过 MARKFETCH_TIMEOUT_MS 配置的时间 |
unsupported_content_type | 不支持的类型 | 响应不是 text/html 或 application/xhtml+xml |
extraction_failed | 提取失败 | Readability 返回空内容(典型于纯客户端渲染 SPA) |
too_large | 内容过大 | 响应体或提取后的 Markdown 超过 MARKFETCH_MAX_BYTES |
save_failed | 保存失败 | 指定了 savePath 但写入文件失败 |
save_forbidden | 写入被禁止 | savePath 超出允许的写入根目录 |
资料来源:README.md:60-80
错误传播流程
graph LR
A[核心模块抛出] --> B[MarkfetchError]
B --> C[MCP 适配器]
B --> D[CLI 适配器]
C --> E[errorResult 格式化]
D --> F[classifyError 处理]
E --> G[返回给 LLM]
F --> H[stderr 输出]
H --> I[非零退出码]资料来源:src/mcp.ts:1-50 和 src/cli.ts:1-50
配置参数
环境变量配置
| 变量名 | 默认值 | 用途 |
|---|---|---|
MARKFETCH_TIMEOUT_MS | 30000 | 单次请求超时时间(毫秒) |
MARKFETCH_MAX_BYTES | 5000000 | 响应体和提取 Markdown 的最大字节数 |
MARKFETCH_USER_AGENT | Chrome 130 UA 字符串 | HTTP User-Agent 头 |
MARKFETCH_ALLOWED_WRITE_ROOTS | os.tmpdir() + process.cwd() | MCP 模式下允许写入的根目录列表 |
所有配置变量在启动时进行验证,无效值会立即失败并输出到 stderr,避免产生难以调试的运行时错误。
资料来源:README.md:55-65
适配器模式
markfetch 采用适配器模式同时支持 CLI 和 MCP 两种调用方式。入口文件 src/index.ts 根据 process.argv.length 延迟加载对应的适配器:
// 伪代码示例
if (process.argv.length > 1) {
// CLI 模式
import('./cli.js');
} else {
// MCP 模式
import('./mcp.js');
}
这种设计确保了"stdout 保留给 MCP 帧"这一不变量是结构性的——CLI 代码在 MCP 模式下永远不会加载,因此不存在通过 console.log 污染输出的可能性。
资料来源:CHANGELOG.md:40-60
安全沙盒
MCP 模式下的文件写入操作受到沙盒限制,防止恶意提示词诱导 LLM 将文件写入任意位置:
graph TD
A[savePath 参数] --> B{绝对路径检查}
B -->|否| C[返回 save_forbidden]
B -->|是| D[符号链接解析]
D --> E{路径限制检查}
E -->|在允许范围内| F[写入文件]
E -->|超出允许范围| G[返回 save_forbidden]沙盒默认允许写入 os.tmpdir() 和 process.cwd() 目录。可以通过 MARKFETCH_ALLOWED_WRITE_ROOTS 环境变量覆盖(注意:覆盖是替换而非合并)。
资料来源:src/sandbox.ts:1-80
性能特性
内存效率
markfetch 采用流式处理策略:
- 响应体直接流入解析器,不经过完整的内存缓冲
- DOM 操作在 linkedom 轻量级解析器中完成,相比原生 DOM 节省内存
- Markdown 转换采用增量处理,避免一次性加载整个文档树
大小限制
MARKFETCH_MAX_BYTES 限制应用于两个阶段:
- 原始响应体大小
- 转换后的 Markdown 大小
如果任一阶段超出限制,管道返回 too_large 错误而非返回不完整的内容。
资料来源:README.md:50-55
版本历史
| 版本 | 变更内容 |
|---|---|
| 0.4.0 | 初始 MCP 工具实现,引入 Readability + Turndown 管道 |
| 0.4.1 | 修复 bin 入口路径问题,改进文档 |
| 0.5.0 | 新增 CLI 模式,引入适配器架构 |
| 0.6.0 | 新增沙盒写入限制,8 种确定性错误码体系完成 |
资料来源:CHANGELOG.md:1-100
资料来源:[README.md:1-15]()
写操作沙箱
写操作沙箱(Write Sandbox)是 markfetch 项目中为 MCP(Model Context Protocol)模式设计的文件系统写入安全机制。其核心功能是限制 MCP 工具的 savePath 参数只能写入预定义的允许目录范围内,防止语言模型通过路径遍历(如 ../../etc/passwd)将文件写入敏感位置。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
概述
写操作沙箱(Write Sandbox)是 markfetch 项目中为 MCP(Model Context Protocol)模式设计的文件系统写入安全机制。其核心功能是限制 MCP 工具的 savePath 参数只能写入预定义的允许目录范围内,防止语言模型通过路径遍历(如 ../../etc/passwd)将文件写入敏感位置。
该沙箱仅在 MCP 模式下生效,CLI 模式不执行任何沙箱检查,因为在命令行环境中人类用户本身就是安全边界。
资料来源:README.md:沙箱设计说明
架构设计
组件关系
graph TD
A[MCP 客户端调用 fetch_markdown] --> B{是否提供 savePath?}
B -->|否| Z[直接返回 markdown]
B -->|是| C[沙箱校验模块]
C --> D[路径解析]
C --> E[符号链接展开]
C --> F[边界检查]
D --> G{是否在允许范围内?}
G -->|是| H[写入文件]
G -->|否| I[返回 save_forbidden 错误]
J[环境变量配置] --> C
K[默认根目录] --> C核心模块
| 模块 | 文件位置 | 职责 |
|---|---|---|
| 沙箱校验核心 | src/sandbox.ts | 实现路径合法性检查逻辑 |
| MCP 适配器 | src/mcp.ts | 在工具调用前调用沙箱校验 |
| 环境配置解析 | src/mcp.ts | 读取并验证 MARKFETCH_ALLOWED_WRITE_ROOTS |
资料来源:src/sandbox.ts:1-50, src/mcp.ts:构建允许根目录
工作流程
写入校验流程
sequenceDiagram
participant MCP as MCP 客户端
participant Server as markfetch Server
participant Sandbox as 沙箱模块
participant FS as 文件系统
MCP->>Server: fetch_markdown(url, savePath="/tmp/out.md")
Server->>Sandbox: validatePath("/tmp/out.md")
Sandbox->>Sandbox: realpath("/tmp/out.md")
Sandbox->>Sandbox: 获取 allowedRoots
Sandbox->>Sandbox: 检查相对路径
alt 路径在允许范围内
Sandbox-->>Server: { ok: true, resolved: "/private/tmp/out.md" }
Server->>FS: writeFile(resolved)
FS-->>Server: 写入成功
Server-->>MCP: { content: [...], isError: false }
else 路径超出范围
Sandbox-->>Server: { ok: false, reason: "..." }
Server-->>MCP: { content: "[save_forbidden] ...", isError: true }
end默认允许根目录
启动时,markfetch 会自动解析并记录两个默认写根目录:
| 默认根目录 | 获取方式 | 说明 |
|---|---|---|
| 系统临时目录 | os.tmpdir() | 通常为 /tmp(POSIX)或 C:\Users\xxx\AppData\Local\Temp(Windows) |
| 当前工作目录 | process.cwd() | markfetch 进程启动时的目录 |
两个路径都会通过 fs.realpath 展开符号链接,确保路径规范唯一。
资料来源:src/mcp.ts:构建 allowedRoots, README.md:默认值说明
路径校验算法
核心检查逻辑
沙箱的路径校验算法位于 src/sandbox.ts,主要步骤如下:
graph TD
A[接收 savePath] --> B[realpath 解析符号链接]
B --> C{Win32 平台?}
C -->|是| D[转换为小写比较]
C -->|否| E[保持原大小写]
D --> F[遍历 allowedRoots]
E --> F
F --> G{计算相对路径}
G --> H{rel === ''?}
H -->|是| J[允许 - 目标即为根目录]
G --> K{rel 不以 '..' 开头且非绝对路径?}
K -->|是| J
K -->|否| L[拒绝 - 路径超出边界]
L --> M[返回 ok: false]
J --> N[返回 ok: true]Windows 大小写处理
Windows 文件系统大小写不敏感,但 fs.realpath 不会自动规范化大小写。为防止 /Users/Me/../me/ 这类绕过检查,沙箱在 Windows 平台会将路径和小写的根目录都转换为小写进行比较。
const fold = process.platform === "win32"
? (s: string) => s.toLowerCase()
: (s: string) => s;
资料来源:src/sandbox.ts:Win32 case-fold 处理
配置选项
环境变量
| 环境变量 | 默认值 | 说明 |
|---|---|---|
MARKFETCH_ALLOWED_WRITE_ROOTS | os.tmpdir() + ':' + process.cwd() | 冒号(POSIX)或分号(Windows)分隔的绝对路径列表 |
配置规则
- 替换而非合并:设置该变量会完全替换默认值,不会追加
- 必须为绝对路径:每个路径必须是绝对路径,相对路径会导致启动失败
- 目录必须存在:路径指向的目录必须在启动时存在,否则失败
- 平台分隔符:
- POSIX(Linux/macOS):使用冒号
:分隔 - Windows:使用分号
;分隔
配置示例
Linux/macOS 配置:
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_ALLOWED_WRITE_ROOTS": "/Users/me/markfetch-out:/tmp"
}
}
}
}
Windows 配置:
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_ALLOWED_WRITE_ROOTS": "C:\\Users\\me\\markfetch-out;C:\\Users\\me\\AppData\\Local\\Temp"
}
}
}
}
资料来源:README.md:环境变量配置说明
符号链接处理
安全修复历史
早期版本存在符号链接逃逸漏洞:检查时对 <sandbox>/link/../out.md 进行路径规范化后看似合法,但写入时路径从左到右解析符号链接,导致实际写入位置超出沙箱。
修复方案:将 realpath 解析后的规范路径直接传递给 writeFile,确保检查路径和写入路径完全一致。
| 版本 | 问题 | 影响 |
|---|---|---|
| 修复前 | 检查规范化路径,但写入原始路径 | link/.. 可逃逸到沙箱外 |
| 修复后 | 检查路径即为写入路径 | 无逃逸可能 |
资料来源:CHANGELOG.md:符号链接逃逸修复说明
错误处理
错误码
| 错误码 | 触发条件 | 返回内容示例 |
|---|---|---|
save_forbidden | savePath 解析后不在允许的写根目录内 | [save_forbidden] '/etc/passwd' is outside the allowed write roots: ['/tmp', '/project'] |
返回数据结构
// 允许的路径
{
ok: true,
resolved: "/private/tmp/out.md" // realpath 展开后的规范路径
}
// 拒绝的路径
{
ok: false,
reason: "'/etc/passwd' is outside the allowed write roots: ['/tmp', '/project']"
}
注意:确认消息中仍会回显原始 savePath,以确保在 tmpdir 本身是符号链接的主机(如 macOS /var → /private/var)上消息的稳定性。
资料来源:src/sandbox.ts:返回值结构, src/mcp.ts:错误转换
MCP 工具 schema 约束
MCP 适配器在 src/mcp.ts 中使用 Zod 对 savePath 参数进行预校验:
savePath: z
.string()
.refine(isAbsolute, "savePath must be an absolute filesystem path")
.optional()
这确保了:
savePath必须是字符串- 字符串必须是绝对路径(
isAbsolute校验) - 参数可选(未提供时直接返回 markdown)
如果路径不是绝对路径,校验会直接失败,不会进入沙箱检查阶段。
资料来源:src/mcp.ts:savePath schema 定义
CLI 模式差异
| 特性 | MCP 模式 | CLI 模式 |
|---|---|---|
| 沙箱检查 | ✅ 启用 | ❌ 禁用 |
| 路径要求 | 必须为绝对路径 | 可为相对路径(相对 cwd) |
| 符号链接检查 | ✅ 启用 | ❌ 禁用 |
| 错误码 | save_forbidden | 直接拒绝写入 |
CLI 模式的无限制设计基于以下假设:命令行环境中的人类用户是天然的安全边界,可以自行判断路径的适当性。
资料来源:README.md:CLI 模式说明, README.md:沙箱设计说明
安全考量
设计原则
- 最小权限原则:默认只允许写入临时目录和当前工作目录
- 显式优于隐式:环境变量会替换而非合并默认配置
- fail-fast:配置错误在启动时立即失败,而非运行时
- 路径规范化:所有路径经过
realpath展开,防止符号链接攻击
潜在风险
- 并发写入冲突:多个请求写入相同文件名时,后写覆盖前写(无锁保护)
- 临时目录清理:系统清理
/tmp可能导致未保存的文件丢失 - 符号链接竞态:在路径检查和写入之间存在微小时间窗口,攻击难度极高但理论上可能
资料来源:README.md:安全说明, src/sandbox.ts:安全设计
资料来源:[README.md:沙箱设计说明]()
配置与环境变量
markfetch 通过环境变量提供运行时配置能力,支持自定义超时、响应大小限制、用户代理以及写入沙箱策略。这些配置项在服务启动时进行验证,确保在处理请求前就能捕获无效配置。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
继续阅读本节完整说明和来源证据。
环境变量总览
markfetch 提供以下四个环境变量用于运行时配置:
| 变量名 | 默认值 | 说明 |
|---|---|---|
MARKFETCH_TIMEOUT_MS | 30000 | 单次请求超时时间(毫秒) |
MARKFETCH_MAX_BYTES | 5000000 | 响应体和提取后 Markdown 的最大字节数 |
MARKFETCH_USER_AGENT | Chrome 130 固定字符串 | HTTP 请求的用户代理 |
MARKFETCH_ALLOWED_WRITE_ROOTS | os.tmpdir() + process.cwd() | MCP 模式下允许写入的根目录列表 |
所有环境变量在服务启动时进行验证,无效值会快速失败并将错误输出到 stderr,避免在请求处理时才产生混淆的错误信息。资料来源:README.md
超时配置
MARKFETCH_TIMEOUT_MS 控制单个 HTTP 请求的最大等待时间。当请求超过设定时间未完成时,返回 timeout 错误码。资料来源:README.md
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_TIMEOUT_MS": "60000"
}
}
}
}
增加超时值适用于网络环境较差或目标服务器响应缓慢的场景。
响应大小限制
MARKFETCH_MAX_BYTES 设置响应体和最终提取的 Markdown 内容的大小上限。当任一阶段超过此限制时,返回 too_large 错误码。资料来源:README.md
MARKFETCH_MAX_BYTES=5000000
默认值约 5MB,适用于绝大多数网页场景。超大型文档建议使用 savePath 参数将结果直接写入磁盘。
用户代理配置
MARKFETCH_USER_AGENT 用于自定义 HTTP 请求头中的 User-Agent 字段。默认值是固定的 Chrome 130 字符串,用于模拟真实浏览器指纹。资料来源:README.md
{
"env": {
"MARKFETCH_ALLOWED_WRITE_ROOTS": "/Users/me/markfetch-out:/tmp"
}
}
markfetch 会从 MARKFETCH_USER_AGENT 推导出 Sec-CH-UA-* 客户端提示头。非 Chrome 浏览器的 UA 字符串会启动时快速失败。
写入沙箱(Write Sandbox)
写入沙箱是 MCP 模式下特有的安全机制,用于限制 savePath 参数可以写入的目录范围。CLI 模式下不实施沙箱限制,因为命令行用户本身就是安全边界。资料来源:README.md
默认允许的写入根目录
默认情况下,允许写入的根目录为:
- 系统临时目录(
os.tmpdir()) - 当前工作目录(
process.cwd())
两个路径在服务启动时通过 fs.realpath 解析一次,后续路径验证使用解析后的绝对路径。
自定义允许根目录
通过 MARKFETCH_ALLOWED_WRITE_ROOTS 可以覆盖默认的允许根目录列表:
| 平台 | 路径分隔符 | 示例 |
|---|---|---|
| POSIX | : | /Users/me/markfetch-out:/tmp |
| Windows | ; | C:\Users\me\markfetch-out;C:\Users\me\AppData\Local\Temp |
配置此变量会完全替换默认值,不会合并。因此如果需要保留临时目录访问权限,必须显式列出。资料来源:README.md
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_ALLOWED_WRITE_ROOTS": "/Users/me/markfetch-out:/tmp"
}
}
}
}
沙箱验证逻辑
沙箱验证在 src/sandbox.ts 中实现,核心逻辑如下:资料来源:src/sandbox.ts
// 将 savePath 重新附加到可能的根目录
const reattached = isAbsolute(savePath)
? savePath
: join(resolvedAncestor, ...trailing);
// Windows 平台大小写不敏感,使用小写比较
const fold = process.platform === "win32"
? (s: string) => s.toLowerCase()
: (s: string) => s;
// 检查重附加后的路径是否在允许的根目录范围内
for (const root of roots) {
const rel = relative(fold(root), foldedTarget);
if (rel === "") return { ok: true, resolved: reattached };
if (!rel.startsWith("..") && !isAbsolute(rel)) {
return { ok: true, resolved: reattached };
}
}
验证失败处理
当 savePath 解析后不在允许的根目录范围内时:
- MCP 模式返回
save_forbidden错误码 - CLI 模式不受沙箱限制,无此检查
return {
ok: false,
reason: `'${reattached}' is outside the allowed write roots: [${roots.map((r) => `'${r}'`).join(", ")}]`
};
符号链接处理
沙箱机制会阻止指向允许根目录外的符号链接。每个路径都通过 fs.realpath 解析后再验证。资料来源:README.md
配置验证
沙箱配置在服务启动时进行验证:
- 路径必须是绝对路径
- 目录必须存在
- 格式错误会在启动时快速失败并输出到 stderr
配置传递方式
MCP 模式
MCP 客户端通过配置文件的 env 块传递环境变量:资料来源:src/mcp.ts
{
"mcpServers": {
"markfetch": {
"command": "npx",
"args": ["-y", "markfetch"],
"env": {
"MARKFETCH_TIMEOUT_MS": "60000",
"MARKFETCH_MAX_BYTES": "10000000",
"MARKFETCH_USER_AGENT": "Mozilla/5.0 ...",
"MARKFETCH_ALLOWED_WRITE_ROOTS": "/custom/path"
}
}
}
}
CLI 模式
CLI 模式下可直接通过 shell 环境变量设置:
export MARKFETCH_TIMEOUT_MS=60000
markfetch https://example.com
或在一行内设置:
MARKFETCH_TIMEOUT_MS=60000 markfetch https://example.com
架构流程图
graph TD
A[服务启动] --> B{环境变量验证}
B -->|有效| C[启动 MCP/CLI 服务]
B -->|无效| D[输出错误到 stderr<br/>进程退出]
C --> E[接收请求]
E --> F{MCP 模式?}
F -->|是| G[检查 savePath 沙箱]
F -->|否| H[直接处理请求]
G -->|在允许范围内| H
G -->|超出范围| I[返回 save_forbidden]
H --> J[执行 fetch_markdown]
J --> K{超时?}
J --> L{响应大小?}
J --> M{内容类型?}
K -->|是| N[返回 timeout]
L -->|是| O[返回 too_large]
M -->|否| P[返回 unsupported_content_type]
J --> Q[提取文章内容]
Q --> R{提取成功?}
R -->|否| S[返回 extraction_failed]
R -->|是| T[转换为 Markdown]
T --> U{保存到文件?}
U -->|是| V[写入 savePath]
U -->|否| W[返回 content[0].text]
V --> X[返回确认信息]错误码与配置关系
| 错误码 | 相关配置 | 说明 |
|---|---|---|
timeout | MARKFETCH_TIMEOUT_MS | 请求超时 |
too_large | MARKFETCH_MAX_BYTES | 响应或内容超出限制 |
save_failed | 路径写入权限 | 写入文件失败 |
save_forbidden | MARKFETCH_ALLOWED_WRITE_ROOTS | 路径超出沙箱范围 |
资料来源:README.md
最佳实践建议
- 生产环境:建议设置合理的
MARKFETCH_TIMEOUT_MS(如 30000-60000ms)和MARKFETCH_MAX_BYTES(如 5-10MB)
- MCP 部署:显式设置
MARKFETCH_ALLOWED_WRITE_ROOTS,明确允许的输出目录
- 安全考虑:不要在 MCP 模式下完全移除沙箱限制,保持最小权限原则
- Windows 平台:使用
;作为分隔符,并确保路径格式正确(反斜杠或正斜杠均可)
- 调试场景:可以临时增大超时和大小限制来排查问题
资料来源:[README.md](https://github.com/vasylenko/markfetch/blob/main/README.md)
错误处理机制
markfetch 采用统一的错误处理架构,确保在 CLI 和 MCP 两种运行模式下都能提供一致的、机器可读的诊断信息。所有错误均通过预定义的错误代码标识,支持确定性错误处理,便于调用方进行逻辑分支和日志记录。
继续阅读本节完整说明和来源证据。
概述
markfetch 采用统一的错误处理架构,确保在 CLI 和 MCP 两种运行模式下都能提供一致的、机器可读的诊断信息。所有错误均通过预定义的错误代码标识,支持确定性错误处理,便于调用方进行逻辑分支和日志记录。
核心设计原则:
- 确定性错误代码:8 种标准化错误代码覆盖所有故障场景
- 统一错误类型:
MarkfetchError类在 core 层统一抛出 - 适配器隔离:CLI 和 MCP 适配器各自负责错误格式转换
- 启动时验证:环境变量在进程启动时即进行校验,失败快速报错
资料来源:README.md
资料来源:[README.md](https://github.com/vasylenko/markfetch/blob/main/README.md)
失败模式与踩坑日记
保留 Doramagic 在发现、验证和编译中沉淀的项目专属风险,不把社区讨论只当作装饰信息。
可能增加新用户试用和生产接入成本。
假设不成立时,用户拿不到承诺的能力。
新项目、停更项目和活跃项目会被混在一起,推荐信任度下降。
下游已经要求复核,不能在页面中弱化。
Pitfall Log / 踩坑日志
项目:vasylenko/markfetch
摘要:发现 7 个潜在踩坑项,其中 0 个为 high/blocking;最高优先级:安装坑 - 来源证据:v0.4.1。
1. 安装坑 · 来源证据:v0.4.1
- 严重度:medium
- 证据强度:source_linked
- 发现:GitHub 社区证据显示该项目存在一个安装相关的待验证问题:v0.4.1
- 对用户的影响:可能增加新用户试用和生产接入成本。
- 建议检查:来源显示可能已有修复、规避或版本变化,说明书中必须标注适用版本。
- 防护动作:不得脱离来源链接放大为确定性结论;需要标注适用版本和复核状态。
- 证据:community_evidence:github | cevd_749b65614f7b40e0b524f4e932cd4aca | https://github.com/vasylenko/markfetch/releases/tag/v0.4.1 | 来源讨论提到 node 相关条件,需在安装/试用前复核。
2. 能力坑 · 能力判断依赖假设
- 严重度:medium
- 证据强度:source_linked
- 发现:README/documentation is current enough for a first validation pass.
- 对用户的影响:假设不成立时,用户拿不到承诺的能力。
- 建议检查:将假设转成下游验证清单。
- 防护动作:假设必须转成验证项;没有验证结果前不能写成事实。
- 证据:capability.assumptions | github_repo:1234238440 | https://github.com/vasylenko/markfetch | README/documentation is current enough for a first validation pass.
3. 维护坑 · 维护活跃度未知
- 严重度:medium
- 证据强度:source_linked
- 发现:未记录 last_activity_observed。
- 对用户的影响:新项目、停更项目和活跃项目会被混在一起,推荐信任度下降。
- 建议检查:补 GitHub 最近 commit、release、issue/PR 响应信号。
- 防护动作:维护活跃度未知时,推荐强度不能标为高信任。
- 证据:evidence.maintainer_signals | github_repo:1234238440 | https://github.com/vasylenko/markfetch | last_activity_observed missing
4. 安全/权限坑 · 下游验证发现风险项
- 严重度:medium
- 证据强度:source_linked
- 发现:no_demo
- 对用户的影响:下游已经要求复核,不能在页面中弱化。
- 建议检查:进入安全/权限治理复核队列。
- 防护动作:下游风险存在时必须保持 review/recommendation 降级。
- 证据:downstream_validation.risk_items | github_repo:1234238440 | https://github.com/vasylenko/markfetch | no_demo; severity=medium
5. 安全/权限坑 · 存在评分风险
- 严重度:medium
- 证据强度:source_linked
- 发现:no_demo
- 对用户的影响:风险会影响是否适合普通用户安装。
- 建议检查:把风险写入边界卡,并确认是否需要人工复核。
- 防护动作:评分风险必须进入边界卡,不能只作为内部分数。
- 证据:risks.scoring_risks | github_repo:1234238440 | https://github.com/vasylenko/markfetch | no_demo; severity=medium
6. 维护坑 · issue/PR 响应质量未知
- 严重度:low
- 证据强度:source_linked
- 发现:issue_or_pr_quality=unknown。
- 对用户的影响:用户无法判断遇到问题后是否有人维护。
- 建议检查:抽样最近 issue/PR,判断是否长期无人处理。
- 防护动作:issue/PR 响应未知时,必须提示维护风险。
- 证据:evidence.maintainer_signals | github_repo:1234238440 | https://github.com/vasylenko/markfetch | issue_or_pr_quality=unknown
7. 维护坑 · 发布节奏不明确
- 严重度:low
- 证据强度:source_linked
- 发现:release_recency=unknown。
- 对用户的影响:安装命令和文档可能落后于代码,用户踩坑概率升高。
- 建议检查:确认最近 release/tag 和 README 安装命令是否一致。
- 防护动作:发布节奏未知或过期时,安装说明必须标注可能漂移。
- 证据:evidence.maintainer_signals | github_repo:1234238440 | https://github.com/vasylenko/markfetch | release_recency=unknown
来源:Doramagic 发现、验证与编译记录