关于 Groundhog
Groundhog 是供 Agent 通过 Chrome 搜索、阅读和研究网页的 MCP 服务。它会移除并报告网页中的隐藏文本,提供来源凭据,并将搜索与页面段落排序结合起来。
主要功能
- 01搜索网页
- 02通过 Chrome 阅读页面
- 03移除并报告隐藏文本
- 04提供来源溯源凭据
- 05为研究段落排序
适用场景
- 01跨网页来源研究问题
- 02阅读普通抓取器无法访问的页面
- 03检查页面中被移除的隐藏文本
- 04追溯提取内容的来源
MCP 相关
向 MCP 客户端提供 search、read_url 和 research 工具。它通过 Chrome 浏览器访问页面,并返回提取内容与来源信息。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
4收录的工具列表 · 更新于 2 天前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| url | string | 是 | 绝对 http(s) URL。拒绝私有地址和环回地址。 |
| query | any | 否 | 设置后,`matches` 会携带与其最相关的段落,每段都包含标题和用于引用的偏移量。 |
| format | enum | 否 | 'markdown' 提取文章;'text' 返回页面渲染后的文本。markdown | text |
| max_tokens | any | 否 | 内容的 Token 预算。省略时使用服务器的 GROUNDHOG_MAX_TOKENS(默认值为 20000)。必须为正数。 |
| include_hidden | boolean | 否 | 保留人类读者不可见的文本。无论如何,这些文本都会在 `threats` 中报告;此设置仅控制它们是否保留在内容中。 |
详细介绍
Groundhog
通过真实且经过隐身补丁处理的 Chrome,为 AI Agent 提供网页搜索、阅读和研究功能。 Groundhog 是一个 MCP 服务,能返回大模型可信赖的纯净 Markdown:人类看不到的文本会在大模型读取前被剔除并报告;每个来源都附带来源凭证;真实浏览器还能读取会阻止普通抓取器访问的网页。
agent / crawler ──MCP──▶ Groundhog (search, read_url, research) ──CDP──▶ stealth Chrome ──▶ the web
与其他抓取层的实测对比
28 个页面包含人类看不到的文本,另有 3 个对照页面不含此类文本。当载荷未传到大模型且文章文本仍然保留时,该案例即为通过。
| 已拦截 | 保留文章 | 已报告 | 误报 | |
|---|---|---|---|---|
requests + trafilatura | 25/28 | 28/28 | 0/28 | 0/3 |
| Scrapling (HTTP) | 16/28 | 28/28 | 0/28 | 0/3 |
| Jina Reader | 15/28 | 28/28 | 0/28 | 0/3 |
| Firecrawl | 7/28 | 28/28 | 0/28 | 0/3 |
| Groundhog | 28/28 | 28/28 | 25/28 | 0/3 |
其他提取器通常会意外丢弃隐藏文本,但都不会告知调用方删除了内容。Groundhog 未报告的 3 种情况(隐藏的输入值、元描述、iframe srcdoc)不会到达任何提取器。语料库公开于
dmytrome.github.io/groundhog;测试框架及其限制见 benchmark/。
快速开始
Claude Code:
claude mcp add groundhog -- uvx groundhog-mcp
Claude Desktop、Cursor、Windsurf:
{
"mcpServers": {
"groundhog": {
"command": "uvx",
"args": ["groundhog-mcp"]
}
}
}
首次抓取时,Groundhog 会在独立配置目录 ~/.groundhog/chrome 中,以无头模式启动你现有的 Chrome。不会触碰你自己的浏览活动、标签页或登录状态。如果未安装 Chrome,status 和错误信息会说明需要安装什么。
GROUNDHOG_BROWSER=stealth 会改为运行经过强化的浏览器容器(Docker 或 Podman;首次拉取需要几分钟)。如需自行运行:
docker run -d --rm --name groundhog-browser --shm-size 512m \
-p 127.0.0.1:9222:9222 -- ghcr.io/dmytrome/groundhog:latest
CDP_URL 会按原样将 Groundhog 指向任意浏览器:对 http:// 地址会探测 /json/version,对 ws:// 或 wss:// 地址(托管浏览器提供的地址)则会直接连接。只有本地地址无响应时才会自动启动;GROUNDHOG_AUTO_START_BROWSER=false 可将其关闭。从源码运行:cd mcp && uv sync && uv run groundhog-mcp。
这四个工具都标注为 readOnlyHint:不会写入任何内容,也不会更改远程状态。唯一的副作用是自动启动,它可能会启动 Chrome;使用 stealth 时,也可能会在移除名为 groundhog-browser 且无法访问的容器后运行该容器。
与众不同之处
- 隐藏文本会被剔除并报告。 渲染后的 DOM 展示人类可见的内容;其余内容都会被移除,并逐项列入
threats。这是一种有力的启发式方法,并非确凿证明:参见隐藏文本检测的局限。 - 每个来源都附带凭证:包括提取内容的 SHA-256、规范 URL、语言、字数,以及网页声明的作者和日期。
- 默认安全。 SSRF 防护会检查每个文档请求,包括重定向和框架,并按域名进行速率限制。参见安全性。
- 不暴露自动化特征。 Groundhog 通过原始 CDP 驱动 Chrome,且从不启用 Puppeteer、Playwright 和 Selenium 中会被反机器人机制检测到的
Runtime域。 - 真实的指纹。 使用真实 Chrome,意味着使用真实的 TLS、HTTP/2、WebGL 和 canvas。在无头模式下,
HeadlessChrome标记会被替换,客户端提示会从浏览器复制;共享工作线程和服务工作线程不在覆盖范围内。隐身镜像会在 Xvfb 下以有头模式运行。 - 无需大模型,也无需 API 密钥。
research会返回提取结果,由你的 Agent 负责综合分析。
工具
read_url(url, format="markdown", max_tokens=None, query=None, include_hidden=False)
| Key | 含义 |
|---|---|
markdown | 提取的内容,优先使用文章内容;如果没有,则回退为代码围栏内的渲染文本,因此其中的内容都不会被解析为 Markdown;PDF 会按每页返回一个 ## Page N 分节,并将文本放在代码围栏中。format 可以是 markdown 或 text |
title | 页面标题 |
url | 你请求的 URL |
final_url | 重定向后的 URL,会由 SSRF 防护机制重新检查。如果不可用,则报告请求的 URL,并附带 final_url_suppressed 威胁 |
fetched_at | UTC ISO-8601 时间戳 |
status | ok;如果返回的不是你请求的页面,则为 challenge / blocked / rate_limited / not_found / server_error / unsupported_content。blocked 包括 401、403 和其他 4xx 错误页面。too_large 表示页面包含超过 5 MiB 的 HTML 或超过 2 MiB 的文本,或者 PDF 超过 10 MiB、100 页或 20 秒的解析时限,此时不会返回任何内容。加密或格式错误的 PDF 会标记为 unsupported_content。empty 表示渲染后的页面没有读者能看到的文本(封闭 Shadow DOM、iframe 和 alt 属性中的文本不计),无论 markdown 中有什么内容。使用 include_hidden 时,样式隐藏的文本也会计为可见;不可见字符始终不计。unknown 表示没有观察到可用的状态。状态根据文本来源文档判断,并在客户端重定向之后进行 |
http_status | 顶层 HTTP 状态,或 null |
truncated | 内容是否因 token 预算用尽而被截断 |
threats | 被剥离的内容;未发现内容时为空 |
matches | 使用 query 时:按排名排列的段落,包含 id、heading、offset、end、score 和 clamped(为适应 max_tokens 而截短)。offset 和 end 是页面完整 Markdown 中的索引,该范围对应返回的文本;长代码块的一部分会在前后补上围栏行。代码不会在片段中间截断:如果匹配的代码块或片段超过 max_tokens,则会被省略;如果没有内容适合返回,markdown 会为空并设置 truncated,因此请调高 max_tokens |
set_aside | 使用 query 时:因相关性低于阈值而被舍弃的评分段落 |
provenance | 内容哈希、规范 URL、语言、字数、作者和日期 |
默认会剥离:display:none、visibility:hidden、content-visibility:hidden、
opacity ≤ 0.05、font-size < 4px、零尺寸以及渲染为空的 display:contents 盒子、.sr-only 样式的亚像素盒子、clip: rect(...)、会将内容完全裁掉的 clip-path(circle(0)、inset(100%)、单行多边形)、任一轴上使用 transform: scale(0)、透明度不大于 0.05 的 filter、裁切溢出内容的零高度或零宽度盒子、透明或与背景同色的文本、位于页面之外的内容、折叠的 <details>、<template> 内容,以及零宽度字符、双向控制字符和 Unicode Tag 字符。如果盒子中唯一的文本是已打开的弹出框或模态对话框,则会保留,因为该对话框仍显示在屏幕上;盒子中的其他文本照常剥离。HTML 注释也会报告。
在 PDF 中,使用渲染模式 3 或 7 绘制、不会显示任何内容的文本会被剥离,并报告为 hidden_pdf。这包括扫描页面的 OCR 层,因此,如果扫描件的唯一文本就是该层,则会标记为 empty,除非你传入 include_hidden。
include_hidden=True 会保留文本,但仍会报告该文本。alt、aria-label、aria-description 和 title 中的文本不会出现在 markdown 或渲染文本中;长度达到 125 个字符或以上的值会报告为 hidden_attribute,并附上截短的摘录,因为已知长文本可能被用作载体。
请将 threats 视为不可信内容:其中的摘录来自页面,由页面作者编写,经过清理且长度受限。
type | 内容 |
|---|---|
hidden_css | 隐藏原因、80 个字符的摘录和 DOM位置 |
hidden_attribute | 同上,适用于 alt、aria-label、aria-description 或 title 属性中的文本 |
hidden_template | 同上,适用于 <template> 中的文本 |
hidden_pdf | 同上,适用于 PDF 以不可见方式绘制的文本,页面作为其位置 |
zero_width / bidi / tag | 在提供的文本中检测到的原因中的码点和计数 |
report_truncated | 达到上限后丢弃的条目数 |
final_url_suppressed | 页面最终 URL 不可用,因此未返回 |
detection_degraded | 检测在页面自身的 JavaScript 环境中运行,页面可以篡改该环境 |
strip_incomplete | 文本是根据标记而非布局重建的,通常是因为存在开放的影子根 |
每个页面最多返回 50 条发现(research 中每个来源最多 10 条),两类发现分别设定上限,以免诱饵内容淹没真实载荷。通知会在达到上限后追加。
query 会将从头部截断改为对清理后的内容进行 BM25 段落排序,并在预算范围内保留得分达到最高得分一半的段落。
search(query, limit=10)
返回命中项(title、url、snippet、engine、score、published)以及提供结果的backend。不会抓取任何内容。设置 SEARXNG_URL 后,会使用你的 SearXNG 实例,该实例需要配置 formats: [html, json];否则会通过浏览器呈现搜索页面。命中项文本会经过清理并限制长度;若命中项的 URL 在清理后会发生变化、不是 http(s)、包含凭据或长度超过 2048 个字符,则会被丢弃。无法提供结果的后端会引发错误,而不会返回空网页。
research(query, max_sources=5, max_tokens=None, include_domains=None, exclude_domains=None, published_after=None, aim=None, follow=None)
搜索并读取排名靠前的来源,然后一次性对所有来源的段落进行排序。
passages:id、text、source_url、heading、score、start、end、clamped、fence。text与markdown[start:end]完全一致;clamped表示为满足预算而截短了文本。代码块不会被截短,只会被排除。若段落从长代码块中截取而来,fence会包含代码块的起始行:当text不以该行开头时,请用它重新打开代码块;当text不以闭合围栏结尾时,请闭合代码块。只要页面未发生变化,id就保持稳定。sources:url、title、status(ok/blocked/timeout/error)、page_status(与read_url中相同)、threats、threat_total、threat_counts(按类型统计的所有发现,包括检测器丢弃为hidden_unclassified的页面片段)、provenance、filtered、depth和discovered_via(跟进该来源所依据的候选项,并包含你的why)。来源获取失败不会导致整个调用失败。挑战页面、错误页面、empty页面或任何markdown为空的页面都会被视为not_content,且不会贡献段落。- 除非设置了
include_domains,否则每个可注册域名仅取一个页面;设置后,每个站点的页面数不得超过GROUNDHOG_MAX_CONCURRENT_PAGES;max_sources最高可设为 10。
include_domains 只读取这些域名及其子域名,exclude_domains 不会读取这些域名,而 published_after(YYYY-MM-DD)会排除日期更早的页面。最多可包含 8 个域名、排除 50 个域名。若页面重定向至被排除的域名或跳出所包含的域名范围、日期过早,或其 URL 或文本与已使用页面重复,则会在 filtered 中列出,且不会贡献段落。发送给搜索引擎的查询最多会注明 8 个域名;更多排除域名会在搜索后进行过滤。aim(最多 200 个字符)说明证据必须证明什么,并会原样回传,不会改变搜索或排序结果。
receipt 中包含 executed_query、去重后的命中项和 hits_by_domain、未抓取的命中项(hits_excluded、hits_outside_domains、hits_before_date、hits_same_domain、超过来源上限而未读取的 hits_unread)、各来源只计为 failed、not_content、filtered 或 usable 其中之一的计数、outside_domains、unknown_dates,以及被 omitted、set_aside 或 clamped 的段落。在得出网页上没有答案的结论前,请先阅读这些内容。
candidates 是返回段落中的链接,最多 12 个,每页不超过 4 个,每个域名不超过 3 个。每个候选项都包含 token、url、domain、label(页面写出的链接文本)、parent_source_url、parent_passage_id、depth 和 flags(parent_strip_incomplete、parent_detection_degraded、parent_threats=N、likely_unsupported_format、same_domain_pages=N)。不会提供指向当前页面、此次调用中已读取页面、域名筛选范围之外的页面,或来自链接深度为 3 的页面的链接。若要读取其中一些链接,请使用相同的 query 和筛选条件再次调用,并传入 follow=[{"token": ..., "why": ...}](最多 8 个,每个 why 最长 160 个字符):不同调用之间不会保留任何信息。跟进操作不会执行搜索,followed 会说明每个条目的状态:read、not_content、failed、filtered、duplicate、超过 same_domain_limit(截止时间前每个站点最多可以开始读取的页面数)、invalid_token 或 expired。令牌由服务端签名,并会在 30 分钟后或服务重启时过期。
响应的 JSON 文本块不会超过 40,000 个字符;MCP 结果还会在 structuredContent 中再次返回同一对象。若内容将超出上限,会依次优先保留卡片、威胁摘录、段落和元数据,并在收据中分别计数(candidates_omitted、threat_excerpts_omitted、passages_omitted、sources_omitted、metadata_compacted);被省略来源的安全计数会保留在 omitted_safety 中,notes 也会说明这一点。对某个来源使用 read_url 会返回其页面;缩小查询范围或减少来源数量可以腾出更多空间。
status()
返回 browser_reachable、cdp_url(仅包含 scheme、host 和 port)以及浏览器无法连接时的 hint。
配置
MCP 服务:
| 环境变量 | 默认值 | 用途 |
|---|---|---|
CDP_URL | http://127.0.0.1:9222 | CDP 端点,可为本地或远程。未经过身份验证,请妥善保密 |
GROUNDHOG_BROWSER | chrome | 自动启动时启动的浏览器:chrome 或 stealth。设置了 GROUNDHOG_COMPOSE_FILE 或 GROUNDHOG_BROWSER_IMAGE 时,默认为 stealth |
GROUNDHOG_CHROME_PATH | (found) | Chrome 可执行文件 |
GROUNDHOG_CHROME_PROFILE | ~/.groundhog/chrome | 配置文件目录;在此处登录后,后续会复用该登录状态 |
GROUNDHOG_BLOCK_PRIVATE_IPS | true | SSRF 防护 |
GROUNDHOG_MIN_DELAY_MS | 5000 | 对同一域名的请求间隔 |
GROUNDHOG_MAX_TOKENS | 20000 | Token 限额 |
GROUNDHOG_MAX_CONCURRENT_PAGES | 4 | 并发标签页数 |
SEARXNG_URL | (unset) | 用于 search 的 SearXNG 实例 |
GROUNDHOG_SEARCH_BACKEND | auto | auto、searxng 或 serp |
GROUNDHOG_AUTO_START_BROWSER | true | 本地 CDP_URL 无响应时启动浏览器 |
GROUNDHOG_BROWSER_IMAGE | ghcr.io/dmytrome/groundhog:latest | stealth 使用的镜像 |
GROUNDHOG_COMPOSE_FILE | (none) | 改为使用 docker compose -f <file> up -d 启动 |
浏览器容器:
| 环境变量 | 默认值 | 用途 |
|---|---|---|
USER_AGENT | 根据已安装的 Chrome 生成 | 启动时设置 UA,每个作用域中的值都不相同 |
PROXY | (none) | 上游代理 http://user:pass@host:port;时区和区域设置与出口 IP 一致 |
TZ | UTC | 备用时区 |
WINDOW_SIZE | 1920,1080 | Chrome 窗口大小 |
XVFB_WHD | 1920x1080x24 | 虚拟显示器 |
隐身 Chrome 容器
在 Xvfb 下运行带界面的 Chrome,并提供 CDP 端点,可供任何 CDP 客户端使用。它报告为 Chrome 而非 HeadlessChrome,将 navigator.webdriver 设为 false,并在启动时设置 UA。
使用 PROXY 时,时区和区域设置会与出口 IP 匹配,凭据通过本地 tinyproxy 传递,WebRTC 会固定使用该代理。WebGL 使用检测到的 GPU,否则使用 Mesa llvmpipe。
在全新容器(Chrome 151,无代理)上通过原始 CDP 测试:
| 检测器 | 结果 |
|---|---|
| deviceandbrowserinfo | 非机器人(isBot: false,零项标记) |
| browserscan | 正常 |
| bot.sannysoft.com | 31 / 31 项检查通过 |
iphey 会将任何数据中心 IP 标记出来,因此会跟踪但不计分。实时结果表见 RESULTS.md。Puppeteer、Playwright 和 Selenium 仍会被标记,因为它们会启用 Runtime;examples/ 中有对应的客户端示例,更多工具见 OTHER_TOOLS.md。
安全
CDP 端点未经过身份验证:任何能访问该端口的人都可以控制浏览器。请将其限制在
localhost 或专用网络中。隐身镜像使用 --no-sandbox 运行,因此请确保容器隔离。请通过
SECURITY.md 报告漏洞。
SSRF 防护会解析每个主机,并拒绝环回、私有、链路本地(包括
169.254.169.254)、保留、多播、未指定、CGNAT 和 IPv4 映射的 IPv6 地址、
非 http(s) 协议,以及包含凭据的 URL。它会检查 Chrome 即将发送的每个文档请求,包括重定向和跨站框架,并再次检查
final_url。
子资源由 Chrome 的本地网络访问规则处理,而 Groundhog 启动的 Chrome 已关闭预加载功能。
防护的限制:
- 只检查文档。较旧的 Chrome,或在配置文件中获准访问本地网络的网站,其子资源不受此防护覆盖。
- 隐身容器和你提供的浏览器中,预加载功能仍处于开启状态。
- Groundhog 和 Chrome 会分别解析 DNS,因此仍存在短 TTL 的 DNS 重绑定窗口。
- 抓取操作共用一个配置文件,因此一个页面设置的 Cookie 会保留到后续抓取中,且页面 JavaScript 可以从该配置文件自行发出请求。
GROUNDHOG_BLOCK_PRIVATE_IPS=false 会关闭防护;仅当确实需要访问内部地址时才使用。
隐藏文本检测的限制
threats 列表为空并不意味着万无一失。检测在页面无法篡改的隔离环境中运行,且不会修改实时页面。但:
- 阈值可以设置得恰好略低于检测值(
opacity: 0.06、transform: scale(0.01)),并且只有在能够证明某种样式完全不绘制内容时,才会将其视为隐藏,因此以下情况可以通过检测:text-indent、inset(50%)(Chrome 会对其报告的值进行取整)、以像素为单位或通过url()/path()指定的clip-path、3D 旋转、mask-image、被其他元素覆盖绘制的文本或堆叠在其他元素后方的文本,以及放置在裁剪溢出内容的框外的定位子元素。 - 在 PDF 中,只检测渲染模式 3 和 7。白色或微小文本、页面外的文本、隐藏图层以及图像下方的文本都能通过检测,并且每个 PDF 结果都会在
detection_degraded通知中说明这一点。如果不可见文本使用的字体无法测量字形宽度(竖排文本、非Identity-H的 CMap),则会返回unsupported_content,而不会返回含有该文本的结果;除非传入include_hidden。解析在子进程中运行,20 秒后会终止。在 Linux 上,内存使用也限制为 1 GiB;在其他系统上,则仅受时间上限和解码流 256 MiB 的上限约束。 - 不可见字符集采用拒绝列表。
- 无法读取封闭的 Shadow Root,因此会完全省略其中的内容。
- 当页面在层叠优先级上胜过隐藏样式表,或隐藏了自己的
<body>时,文本会根据标记重建,换行符为推测值,并会通过strip_incomplete说明这一情况。
拦截检测的限制
- 如果供应商响应头(
cf-mitigated、x-vercel-mitigated、x-amzn-waf-action、x-dd-b、x-datadome-cid)或质询资源 (Cloudflare、DataDome、PerimeterX、Imperva)表明存在质询,则会确定为质询。其他供应商则根据近乎空白页面中的英文措辞进行判断。 - 软拦截(返回 200,但悄悄扣留数据)会被视为内容。
unknown不等于ok,但也不会被视为拦截。- 会有意忽略供应商标识(
server: cloudflare、cf-ray)。
关于“隐身”的说明
尽力而为。它可以绕过常见的开源检测器,但无法绕过依据 IP 信誉和行为进行拦截的商业反机器人系统。请将其用于合法且经授权的自动化操作。
隐私政策
Groundhog 是可自托管的软件,不会与项目运行的任何服务通信。它不会收集任何信息:没有遥测数据、分析数据、崩溃报告或账户。
页面由你设备上的浏览器获取,并返回给 MCP 客户端。服务不保留数据库,也不写入日志。浏览器配置文件会持久保存 Cookie 和存储数据:本地 Chrome 使用 ~/.groundhog/chrome(删除该文件夹即可清除),stealth 使用容器(运行 docker rm -f groundhog-browser)。
你的流量会到达你获取内容的网站、你的搜索后端(你的 SearXNG;如果未配置,则使用公共搜索引擎),以及仅当设置了 PROXY 时用于查询代理出口 IP 的 IP 地理位置服务;安装时还会访问 PyPI 和 GHCR。
如有疑问,请参阅 SECURITY.md 或访问 https://github.com/dmytrome/groundhog/issues。
许可证
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。