Groundhog
以下摘录自该仓库的 README(片段) · 在 GitHub 读完整 README →
**Web search, read and research for AI agents — through a real, stealth-patched Chrome.**
Groundhog is an MCP server that finds pages, reads them,
and researches across them, returning clean Markdown a model can trust: text no human could
see is **stripped by default before the model reads it**, every source comes back with a
**provenance receipt**, and a real browser reads pages that block plain fetchers — without
the SSRF holes of naive fetch tools.
Measured against other fetch layers
Twenty-eight pages, each carrying text a human reader cannot see, plus three controls
carrying none. A case passes when the payload does not reach the model **and** the article
text still does — a fetcher that returns nothing contains every payload and is useless.
| | contained | kept the article | reported it | false positives |
| --- | --- | --- | --- | --- |
| 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 reports 25 of the 28 rather than all of them. The other three — a hidden input
value, a meta description, an iframe srcdoc — are contained because no extractor ever
collected them, and naming text that was never on its way to the model would describe work
nothing did.
Containment happens by accident all the time — an article extractor prunes a hidden
because its heuristics dislike it, not because anything asked whether a reader could see
it. That is why the last column matters: **nothing else tested tells the caller that
anything was removed**, so an empty threat list and a page with nothing hidden look
identical.
The corpus is published at
dmytrome.github.io/groundhog, so any fetcher can
be measured against the same pages. Harness, per-case results and the limits of what this
proves are in benchmark/ — including that hosted services are a snapshot of
the date recorded in RESULTS.md.
Quick start
Add Groundhog to your MCP client — that's it. On the first fetch, Groundhog starts the
Chrome you already have, headless and in its own profile at ~/.groundhog/chrome: no
Docker, no repo checkout, and no window, so it never takes focus while you work. It stays
running for the next fetch. Chrome refuses remote control of your everyday profile (since
Chrome 136), so your own browsing, tabs and logins are never touched.
Prefer the hardened browser? GROUNDHOG_BROWSER=stealth pulls and starts the stealth-browser
container instead (Docker or Podman required). When that path has to run, any stale container
named groundhog-browser is removed first; a reachable browser is never touched.
Claude Code:
Claude Desktop / Cursor / Windsurf (claude_desktop_config.json or equivalent):
uvx fetches groundhog-mcp from PyPI on first run. The first fetch pulls the browser
image (once, a few minutes); later fetches are instant. No container runtime? The status
tool and any error say how to install one — or point CDP_URL at a hosted browser for
zero-install use.
**Prefer to manage the browser yourself?** Start it and Groundhog will just use it:
Set GROUNDHOG_AUTO_START_BROWSER=false to disable auto-start. To run the MCP server from
source: cd mcp && uv sync && uv run groundhog-mcp.
Use any CDP browser
CDP_URL takes the address a browser gives you, used exactly as given. An http:// address
is probed for /json/version; a ws:// or wss:// address — what hosted browsers hand out,
often with a key in the query — is connected to directly.
Auto-start only ever runs for a local address that isn't answering, so a browser you point
CDP_URL at is yours to manage. status reports the address without its query or
credentials.
All four tools are annotated readOnlyHint, which is what lets a client run them without a
per-call confirmation. That describes what they do to *your data*: nothing is written, and no
remote state is changed. Worth knowing, because it is the one exception: with auto-start on,
the first call may start Chrome in Groundhog's own profile — or, with GROUNDHOG_BROWSER=stealth,
pull and run the browser container and remove an **unreachable** container named
groundhog-browser first. A reachable browser is never touched, and
GROUNDHOG_AUTO_START_BROWSER=false turns the whole path off.
What makes it different
- **Hidden text is stripped before the model reads it.** Groundhog renders a real DOM, so it
can judge what a *human* would actually see and strip what they could not, reporting each