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.0MCP 协议实现,提供 stdio 通信能力
@mozilla/readability^0.5.0Mozilla 开源的 HTML 内容提取库,从页面中提取主要文章内容
turndown^7.0.0将 HTML 转换为 Markdown 的转换器
turndown-plugin-gfm^1.0.2GitHub Flavored Markdown 插件,支持表格、任务列表等格式
linkedom^0.18.0轻量级 DOM 解析器,用于在 Node.js 环境中解析和操作 HTML
undici^8.2.0HTTP/2 客户端库,处理网络请求
zod^3.0.0TypeScript 类型验证库,用于 MCP 输入模式定义
commander^14.0.3CLI 参数解析库

系统架构

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_errorHTTP 错误目标服务器返回非 2xx 状态码
timeout请求超时超过 MARKFETCH_TIMEOUT_MS 配置的超时时间
unsupported_content_type不支持的内容类型响应不是 text/htmlapplication/xhtml+xml
extraction_failed提取失败Readability 无法提取任何文章内容(常见于纯客户端渲染的 SPA)
too_large内容过大响应体或提取后的 Markdown 超过 MARKFETCH_MAX_BYTES 限制
save_failed保存失败指定了 savePath 但写入文件失败(目录不存在、权限不足等)
save_forbidden保存禁止savePath 路径超出了允许的写入根目录

配置选项

环境变量配置

环境变量默认值说明
MARKFETCH_TIMEOUT_MS30000单次请求超时时间(毫秒)
MARKFETCH_MAX_BYTES5000000响应体和提取后 Markdown 的字节数上限(约 5MB)
MARKFETCH_USER_AGENTChrome 130 固定字符串浏览器标识字符串。必须是 Chrome UA 格式,否则启动时快速失败
MARKFETCH_ALLOWED_WRITE_ROOTSos.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 模式不受此限制。

沙箱规则

  1. 默认根目录:系统临时目录 + 进程当前工作目录
  2. 路径检查:目标路径必须位于允许的根目录内或为其子路径
  3. 符号链接处理:符号链接指向允许根目录外的内容将被阻止
  4. 跨平台支持: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)两种调用接口。项目采用适配器模式,将核心业务逻辑与接口层分离,确保核心逻辑在两种模式下完全一致。

章节 相关页面

继续阅读本节完整说明和来源证据。

章节 入口调度器 (index.ts)

继续阅读本节完整说明和来源证据。

章节 CLI 适配器 (cli.ts)

继续阅读本节完整说明和来源证据。

章节 MCP 适配器 (mcp.ts)

继续阅读本节完整说明和来源证据。

概述

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> 修复相对链接

关键设计决策:

  1. keepClasses: true:保留 <code class="language-X"> 以支持代码高亮提示
  2. Turndown escape 定制:禁用 \_\-/\= 的转义,避免噪声
  3. 标题去重:如果 Readability 保留了原始 <h1>,不重复添加标题
  4. 空标题修剪:移除连续的空标题节点

资料来源: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.md

MCP 完整数据流

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_ERR

8 种错误码对照表:

错误码含义CLI 行为MCP 行为
network_errorDNS/TCP/TLS 失败stderr 输出isError: true
http_error非 2xx 状态stderr 输出isError: true
timeout超过 MARKFETCH_TIMEOUT_MSstderr 输出isError: true
unsupported_content_type非 HTML 响应stderr 输出isError: true
extraction_failedReadability 无法提取内容stderr 输出isError: true
too_large超过 MARKFETCH_MAX_BYTESstderr 输出isError: true
save_failed写入文件失败stderr 输出isError: true
save_forbiddensavePath 在沙箱外不适用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.xMCP stdio 服务器实现
@mozilla/readability^0.5.xHTML 文章内容提取
turndown^7.xHTML 转 Markdown
linkedom^0.18.x服务端 DOM 解析(替代 jsdom)
undici^6.xHTTP 客户端
commander^14.xCLI 参数解析
zod^3.xSchema 验证

资料来源:package.json

环境变量配置

变量默认值作用域说明
MARKFETCH_TIMEOUT_MS30000全部单次请求超时(毫秒)
MARKFETCH_MAX_BYTES5000000全部响应体和转换后 Markdown 的字节上限
MARKFETCH_USER_AGENTChrome 130 UA全部HTTP User-Agent,必须是 Chrome UA
MARKFETCH_ALLOWED_WRITE_ROOTSos.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。

章节 相关页面

继续阅读本节完整说明和来源证据。

章节 方式一:npm 全局安装(CLI 模式)

继续阅读本节完整说明和来源证据。

章节 方式二:npx 免安装运行

继续阅读本节完整说明和来源证据。

章节 方式三:MCP Server 部署

继续阅读本节完整说明和来源证据。

系统要求

组件最低版本说明
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

参数类型必填说明
urlstring目标网页的绝对 HTTP(S) URL
savePathstring绝对路径,用于保存 Markdown 到文件

返回结果在 content[0].text 中,不包含 structuredContent 字段,这是特意设计以确保与各类 MCP 客户端兼容。

资料来源:README.md:10-17

环境变量配置

markfetch 支持多个环境变量用于自定义行为。所有变量在启动时进行验证,无效值会导致快速失败并输出错误到 stderr。

环境变量默认值单位说明
MARKFETCH_TIMEOUT_MS30000毫秒单次请求超时时间
MARKFETCH_MAX_BYTES5000000字节响应体和提取 Markdown 的最大大小(约 5MB)
MARKFETCH_USER_AGENTChrome 130-HTTP User-Agent,必须为 Chrome UA 字符串
MARKFETCH_ALLOWED_WRITE_ROOTSos.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"
      }
    }
  }
}

安全特性

  1. 符号链接防护:检查时使用规范化后的路径,写入时也使用相同的规范化路径,防止通过 link/.. 逃逸
  2. Windows 大小写不敏感处理:Windows 文件系统大小写不敏感,使用 .toLowerCase() 进行比较
  3. CLI 模式无沙箱:CLI 模式设计为由人类直接在 shell 中使用,不执行任何写入限制

资料来源:src/sandbox.ts:1-30, README.md:72-90

错误代码参考

markfetch 定义了 8 种确定性错误代码:

错误代码含义触发条件
network_error网络错误DNS、TCP、TLS 失败或内部错误
http_errorHTTP 错误上游返回非 2xx 状态码
timeout超时超过 MARKFETCH_TIMEOUT_MS 限制
unsupported_content_type不支持的类型响应不是 text/htmlapplication/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.02026-05-12新增 CLI 模式
0.4.02026-05-10新增 MCP Server 支持
0.4.12026-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 --> K

CLI 适配器遵循统一错误处理约定:所有核心模块抛出的 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_errorDNS、TCP、TLS 连接失败或内部网络错误
http_error目标服务器返回非 2xx 状态码
timeout请求超时(默认 30 秒,可通过 MARKFETCH_TIMEOUT_MS 配置)
unsupported_content_type响应不是 HTML 内容
extraction_failedReadability 无法提取文章内容(常见于纯客户端渲染的 SPA)
too_large内容超过 MARKFETCH_MAX_BYTES 限制(默认 5MB)
save_failed文件写入失败(目录不存在、权限不足等)
save_forbiddensavePath 超出允许的写入根目录

资料来源: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/stdoutstdio JSON-RPC
输出格式原始 MarkdownMCP 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_MS30000单次请求超时(毫秒)
MARKFETCH_MAX_BYTES5000000响应体和提取 Markdown 的字节上限
MARKFETCH_USER_AGENTChrome 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 格式,支持直接返回内容或写入文件系统。

章节 相关页面

继续阅读本节完整说明和来源证据。

章节 双模式入口

继续阅读本节完整说明和来源证据。

章节 MCP 服务器初始化

继续阅读本节完整说明和来源证据。

章节 fetchmarkdown 工具

继续阅读本节完整说明和来源证据。

概述

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.tsstdout 用于 MCP 协议帧
CLI 工具存在命令行参数src/cli.tsstdout 用于 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
适用场景文章、文档、博客帖子、新闻、参考页面
限制匿名获取,无认证支持

#### 输入参数

参数类型必填说明
urlstring完整的 HTTP/HTTPS URL,服务器自动跟随重定向
savePathstring绝对路径,将 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_errorHTTP 错误上游返回非 2xx 状态码
timeout请求超时超过 MARKFETCH_TIMEOUT_MS 配置的时间
unsupported_content_type不支持的类型响应不是 text/htmlapplication/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_MS30000单次请求超时时间(毫秒)
MARKFETCH_MAX_BYTES5000000响应体和提取 markdown 的最大字节数
MARKFETCH_USER_AGENTChrome 130 固定字符串HTTP User-Agent,必须为 Chrome UA
MARKFETCH_ALLOWED_WRITE_ROOTSos.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 中的路径验证逻辑:

  1. 解析 savePath 为绝对路径
  2. 遍历所有允许根目录
  3. 检查目标路径是否在任一允许根目录内
  4. 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].textstdout 直接输出
写入沙箱启用禁用(无限制)
错误输出isError: truestderr + 退出码
相对路径处理不支持(需绝对路径)支持(相对 cwd 解析)

CLI 模式中相对路径会被解析为绝对路径后再传给核心逻辑:

const savePath = options.output
  ? resolve(process.cwd(), options.output)
  : undefined;

资料来源:src/cli.ts:13-20

版本历史

版本发布日期MCP 相关变更
0.6.0当前版本稳定版 MCP 实现
0.5.02026-05-12引入 CLI 模式,源文件重构为独立模块
0.4.02026-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 模式的安全设计

  1. 写入沙箱隔离:MCP 工具由语言模型驱动,可能被页面内容引导,因此 savePath 限制在可配置的允许目录内
  2. CLI 模式无沙箱:命令行由人类直接使用,作为安全边界,不施加写入限制
  3. 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 渲染的环境中也能获取到完整的页面内容。

章节 相关页面

继续阅读本节完整说明和来源证据。

章节 HTTP/2 传输

继续阅读本节完整说明和来源证据。

章节 Chrome 请求头集

继续阅读本节完整说明和来源证据。

章节 环境变量配置

继续阅读本节完整说明和来源证据。

概述

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-AgentChrome 130 版本标识
Sec-CH-UA-*客户端提示头,基于 User-Agent 动态派生
Accept内容协商头
Accept-Language语言偏好
Accept-Encoding压缩算法支持

这套头字段组合确保了请求在 TLS 握手后的 HTTP 层与真实 Chrome 浏览器保持一致。

配置项

环境变量配置

变量名默认值用途
MARKFETCH_TIMEOUT_MS30000单次请求超时时间(毫秒)
MARKFETCH_MAX_BYTES5000000响应体与提取后 markdown 的字节数上限
MARKFETCH_USER_AGENTChrome 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("&", "&amp;").replaceAll('"', "&quot;");
  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

该函数会:

  1. 对 URL 中的特殊字符进行 HTML 实体转义
  2. 移除页面中已有的 <base> 标签(上游 URL 更具权威性)
  3. 将规范 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_errorDNS/TCP/TLS 故障或内部错误网络层异常
http_error上游返回非 2xx 状态HTTP 响应状态码 ≥ 400
timeout超过 MARKFETCH_TIMEOUT_MS请求超时
unsupported_content_type响应不是 HTML 类型Content-Type 非 text/htmlapplication/xhtml+xml
extraction_failedReadability 未提取到内容纯客户端渲染的 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 请求头集实现真实浏览器指纹模拟。

章节 相关页面

继续阅读本节完整说明和来源证据。

章节 核心模块结构

继续阅读本节完整说明和来源证据。

章节 依赖库

继续阅读本节完整说明和来源证据。

章节 阶段一:HTML 实体解码

继续阅读本节完整说明和来源证据。

概述

内容提取管道(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 转换为 Markdownstring

资料来源: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(
    /&lt;(\/?(?:code|pre)(?:\s[^&]*?)?\/?)&gt;/g,
    (_, tag) => `<${tag}>`,
  );
}

此函数专门处理 HTML 实体编码的 <code><pre> 标签。由于代码块在转换过程中需要保留原始格式,markfetch 需要将这些被实体编码的标签转换回标准 HTML 标签形式。

正则表达式说明:

  • &lt; - 匹配左尖括号的实体编码形式
  • \/? - 可选的闭合斜杠
  • (?:code|pre) - 匹配 codepre 标签名
  • (?:\s[^&]*?)? - 可选的属性部分
  • &gt; - 匹配右尖括号的实体编码形式

资料来源:src/core.ts:10-18

阶段二:Base Href 注入

function ensureBaseHref(html: string, url: string): string {
  const safeUrl = url.replaceAll("&", "&amp;").replaceAll('"', "&quot;");
  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。管道通过以下步骤解决此问题:

  1. 移除页面原有的 <base> 标签(避免冲突)
  2. <head> 标签内注入新的 <base href> 指向最终重定向后的 URL
  3. 如果没有 <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 配置了自定义的转义规则,以解决以下问题:

  1. 行内下划线保护:Markdown 中行内代码外的下划线通常需要转义,但 markfetch 保留了未转义的形式以提高可读性
  2. 标题下划线处理:CommonMark 的 setext 标题使用 =- 字符,但后面紧跟字母数字字符的情况不是标题,应保留原样

资料来源:src/core.ts:50-75

错误处理机制

错误代码表

错误代码含义触发条件
network_error网络层故障DNS 解析、TCP 连接、TLS 握手失败
http_errorHTTP 协议错误服务器返回非 2xx 状态码
timeout请求超时超过 MARKFETCH_TIMEOUT_MS 配置的时间
unsupported_content_type不支持的类型响应不是 text/htmlapplication/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-50src/cli.ts:1-50

配置参数

环境变量配置

变量名默认值用途
MARKFETCH_TIMEOUT_MS30000单次请求超时时间(毫秒)
MARKFETCH_MAX_BYTES5000000响应体和提取 Markdown 的最大字节数
MARKFETCH_USER_AGENTChrome 130 UA 字符串HTTP User-Agent 头
MARKFETCH_ALLOWED_WRITE_ROOTSos.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 限制应用于两个阶段:

  1. 原始响应体大小
  2. 转换后的 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_ROOTSos.tmpdir() + ':' + process.cwd()冒号(POSIX)或分号(Windows)分隔的绝对路径列表

配置规则

  1. 替换而非合并:设置该变量会完全替换默认值,不会追加
  2. 必须为绝对路径:每个路径必须是绝对路径,相对路径会导致启动失败
  3. 目录必须存在:路径指向的目录必须在启动时存在,否则失败
  4. 平台分隔符
  • 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_forbiddensavePath 解析后不在允许的写根目录内[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()

这确保了:

  1. savePath 必须是字符串
  2. 字符串必须是绝对路径(isAbsolute 校验)
  3. 参数可选(未提供时直接返回 markdown)

如果路径不是绝对路径,校验会直接失败,不会进入沙箱检查阶段。

资料来源:src/mcp.ts:savePath schema 定义

CLI 模式差异

特性MCP 模式CLI 模式
沙箱检查✅ 启用❌ 禁用
路径要求必须为绝对路径可为相对路径(相对 cwd)
符号链接检查✅ 启用❌ 禁用
错误码save_forbidden直接拒绝写入

CLI 模式的无限制设计基于以下假设:命令行环境中的人类用户是天然的安全边界,可以自行判断路径的适当性。

资料来源:README.md:CLI 模式说明, README.md:沙箱设计说明

安全考量

设计原则

  1. 最小权限原则:默认只允许写入临时目录和当前工作目录
  2. 显式优于隐式:环境变量会替换而非合并默认配置
  3. fail-fast:配置错误在启动时立即失败,而非运行时
  4. 路径规范化:所有路径经过 realpath 展开,防止符号链接攻击

潜在风险

  • 并发写入冲突:多个请求写入相同文件名时,后写覆盖前写(无锁保护)
  • 临时目录清理:系统清理 /tmp 可能导致未保存的文件丢失
  • 符号链接竞态:在路径检查和写入之间存在微小时间窗口,攻击难度极高但理论上可能

资料来源:README.md:安全说明, src/sandbox.ts:安全设计

资料来源:[README.md:沙箱设计说明]()

配置与环境变量

markfetch 通过环境变量提供运行时配置能力,支持自定义超时、响应大小限制、用户代理以及写入沙箱策略。这些配置项在服务启动时进行验证,确保在处理请求前就能捕获无效配置。

章节 相关页面

继续阅读本节完整说明和来源证据。

章节 默认允许的写入根目录

继续阅读本节完整说明和来源证据。

章节 自定义允许根目录

继续阅读本节完整说明和来源证据。

章节 沙箱验证逻辑

继续阅读本节完整说明和来源证据。

环境变量总览

markfetch 提供以下四个环境变量用于运行时配置:

变量名默认值说明
MARKFETCH_TIMEOUT_MS30000单次请求超时时间(毫秒)
MARKFETCH_MAX_BYTES5000000响应体和提取后 Markdown 的最大字节数
MARKFETCH_USER_AGENTChrome 130 固定字符串HTTP 请求的用户代理
MARKFETCH_ALLOWED_WRITE_ROOTSos.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[返回确认信息]

错误码与配置关系

错误码相关配置说明
timeoutMARKFETCH_TIMEOUT_MS请求超时
too_largeMARKFETCH_MAX_BYTES响应或内容超出限制
save_failed路径写入权限写入文件失败
save_forbiddenMARKFETCH_ALLOWED_WRITE_ROOTS路径超出沙箱范围

资料来源:README.md

最佳实践建议

  1. 生产环境:建议设置合理的 MARKFETCH_TIMEOUT_MS(如 30000-60000ms)和 MARKFETCH_MAX_BYTES(如 5-10MB)
  1. MCP 部署:显式设置 MARKFETCH_ALLOWED_WRITE_ROOTS,明确允许的输出目录
  1. 安全考虑:不要在 MCP 模式下完全移除沙箱限制,保持最小权限原则
  1. Windows 平台:使用 ; 作为分隔符,并确保路径格式正确(反斜杠或正斜杠均可)
  1. 调试场景:可以临时增大超时和大小限制来排查问题

资料来源:[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 在发现、验证和编译中沉淀的项目专属风险,不把社区讨论只当作装饰信息。

medium 来源证据:v0.4.1

可能增加新用户试用和生产接入成本。

medium 能力判断依赖假设

假设不成立时,用户拿不到承诺的能力。

medium 维护活跃度未知

新项目、停更项目和活跃项目会被混在一起,推荐信任度下降。

medium 下游验证发现风险项

下游已经要求复核,不能在页面中弱化。

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 发现、验证与编译记录