Groundhog
Groundhog is an MCP server that provides web search, page reading, and multi-source research through a real stealth-patched Chrome browser, delivering clean Markdown/text with provenance and security features.
read_url(url, ...): Fetch any web page via the stealth browser. Returns clean content with hidden/injected text stripped by default (reported inthreats). Provenance includes SHA-256 content hash, canonical URL, language, word count, author, and dates. Supports relevance-ranked passage selection with a query (BM25), redirect-safe final URL tracking, SSRF guard, token budget truncation, and optional inclusion of hidden text (include_hidden=true).search(query, limit=10): Perform a web search using a self-hosted SearXNG instance or stealth-rendered SERP; returns ranked hits with title, URL, snippet, engine, score, and publication date. Snippets are sanitized to remove invisible characters. Results are links only—no pages are fetched.research(query, max_sources=5, max_tokens=None): One-call research: searches, reads top sources through the stealth browser, and returns the most relevant passages across all sources with source URLs, headings, scores, and per-source threats/provenance. Fails gracefully per source (ok, blocked, timeout, error) without aborting. Limits to one page per registrable domain; max 10 sources.status(): Diagnose the server by checking if the stealth browser (CDP endpoint) is reachable, reporting the CDP URL (scheme, host, port only—no credentials) and a remediation hint if unreachable.
Security & Privacy: SSRF guard blocks private IPs and credentials; rigorous stripping of zero-width characters, bidi marks, hidden CSS, and Unicode tag blocks to prevent prompt injection. Per-domain rate limiting. Self-hosted, no external model or API key required; pages never leave your infrastructure. The stealth browser runs headfully under Xvfb with anti-detection (custom user agent, no Runtime domain).
Allows using SearXNG as a search backend for the search tool, providing ranked search results from a private SearXNG instance.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Groundhogfetch the markdown from https://example.com"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Groundhog
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.
agent / crawler ──MCP──▶ Groundhog (search, read_url, research) ──CDP──▶ stealth Chrome ──▶ the webQuick start
Add Groundhog to your MCP client — that's it. On the first fetch, Groundhog pulls and
starts the stealth-browser container for you (Docker or Podman required); no repo checkout,
no manual steps. When the default (non-compose) auto-start path has to run, any stale
container named groundhog-browser is removed first; a reachable browser is never touched.
Claude Code:
claude mcp add groundhog -- uvx groundhog-mcpClaude Desktop / Cursor / Windsurf (claude_desktop_config.json or equivalent):
{
"mcpServers": {
"groundhog": {
"command": "uvx",
"args": ["groundhog-mcp"]
}
}
}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:
docker run -d --rm --name groundhog-browser --shm-size 512m \
-p 127.0.0.1:9222:9222 -- ghcr.io/dmytrome/groundhog:latest
# or, from a repo checkout: docker compose up --build -d
curl -s http://localhost:9222/json/version # CDP is liveSet GROUNDHOG_AUTO_START_BROWSER=false to disable auto-start. To run the MCP server from
source: cd mcp && uv sync && uv run groundhog-mcp.
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 pull and run the browser container, and remove an unreachable container
named groundhog-browser first. A reachable one is never touched, and
GROUNDHOG_AUTO_START_BROWSER=false turns the whole path off.
Related MCP server: firecrawl-mcp-server
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 occurrence in
threats. A strong heuristic, not a proof — see the limits of hidden-text detection. The eleven signals, thethreatscaveat and theinclude_hiddenexception are documented underread_url.Every source carries a receipt. SHA-256 hash of the extracted content, canonical URL, language, word count, and author/date when the page declares them — so a downstream claim traces back to exactly what was read.
read_urlreturns the fetch time alongside it asfetched_at.Safe by default. The SSRF guard resolves each host before navigating and refuses to return content from a URL that redirects into a private address. Read-only, with per-domain rate limiting. This matters most in
research, where a third party chooses the URLs. See Security for the full blocklist and the guard's limits.No automation tell. Puppeteer/Playwright/Selenium enable the CDP
Runtimedomain, which anti-bots detect (isAutomatedWithCDP). Groundhog drives the browser over raw CDP and never enablesRuntime/Console, so that signal is absent — a clean session that full automation libraries can't produce overconnect_over_cdp.A real fingerprint. It's real Chrome, run headful under Xvfb (no
HeadlessChrometoken) — authentic TLS/HTTP2 fingerprint, real WebGL/canvas — not a Python HTTP client, so fingerprint-driven blocks go away and cheap proxies work where they otherwise wouldn't.No model, no API key.
researchreturns extracts, not summaries; your agent does the synthesis. Self-hosted and MIT — the pages you fetch never leave your infrastructure.
Tools
read_url(url, format="markdown", max_tokens=None, query=None, include_hidden=False)
Fetches a page and returns clean content plus provenance.
Key | Meaning |
| Extracted content (article-first, falls back to full text); |
| Page title |
| The URL you asked for |
| The URL after redirects (re-checked against the SSRF guard). Never rewritten: if the page's own final URL is unusable, the requested URL is reported and a |
| UTC ISO-8601 timestamp |
| What actually came back: |
| The top-level response's HTTP status code, or |
| Whether the content was cut to fit the token budget |
| Signals detected: hidden-CSS nodes and invisible-character classes; empty when none found |
| When |
| Content hash, canonical URL, language, word count, and author/date metadata when present |
Because Groundhog renders a real DOM, it can evaluate computed styles. Text invisible to
humans is stripped by default and each occurrence reported in threats with its signal
type and a short excerpt: display:none/visibility:hidden, content-visibility: hidden
(the subtree is skipped from layout while the element keeps an ordinary box, so no other
signal sees it), opacity ≤ 0.05, font-size < 4 px, zero-size elements, an element that generates no box
of its own (display: contents) whose contents render nothing, the sub-pixel box
used by .sr-only/.visually-hidden accessibility utility classes (a pattern attackers now
mimic), the legacy clip: rect(...) hiding technique, fully transparent text color, text
color matching the background color (near-1:1 contrast), and elements positioned entirely
outside the rendered page (e.g. left: -9999px). Non-trivial HTML comments are reported too — they never reach the
extracted content either way, but a page embedding instructions this way is worth knowing
about. A second, character-level class is stripped and reported alongside these: zero-width
characters, bidi marks and RTL overrides, and the Unicode Tag block — an invisible ASCII
mirror that is the canonical prompt-injection smuggling channel. Pass include_hidden=True
to keep the stripped text in the output; threats is still populated so you know it was
there.
Treat threats as untrusted. Entries come in six shapes (the character classes share one):
| Carries |
| The hiding |
| A codepoint and count in |
| How many entries were dropped when the cap was hit. Its own type, so it cannot be miscounted as a finding |
| The page's own final URL was unusable (over-long, or carrying invisible characters) and was not returned; |
| The collector had to run in the page's own JavaScript world, where the page can replace the DOM APIs it uses. A short list proves nothing on that page |
| The rendered text was rebuilt from markup rather than read from layout. Either a flagged node could not be removed outright — it won the cascade against the hiding stylesheet (an inline |
The value of stripping is that the payload is out of the content being reasoned over, not
that it is invisible to the model. At most 50 findings per page are returned (10 per
source in research, since the fan-out multiplies the report); beyond that a
report_truncated entry is appended stating how many were dropped, rather than truncating
silently. The two classes are capped independently, so a page cannot bury the findings that
carry its injection excerpt by flooding the report with decoys of the other kind. Notices are
appended after the cap — so they can never themselves be dropped, and a capped list is up to
50 findings plus at most two notices.
Pass query to replace blunt head-truncation with relevance-ranked passage selection:
content is chunked on markdown structure, ranked by lexical (BM25) relevance, and the top
passages within the token budget are returned; matches gives each passage's heading,
character offset, and score for downstream citation. Ranking runs on the sanitized content,
so hidden-text injection payloads cannot influence which passages surface — with the one
exception of include_hidden=True, which leaves the hidden text in the document and ranks it
along with everything else.
search(query, limit=10)
Finds pages for a query and returns ranked hits — title, url, snippet, engine,
score, published — plus the backend that answered. Hits are links only: nothing is
fetched until you pass a URL to read_url.
Two backends, chosen automatically. Set SEARXNG_URL to use your own
SearXNG instance (best results; needs formats: [html, json]
in its settings.yml, since JSON is off by default upstream). With no instance configured,
Groundhog renders a search page through the stealth browser instead — no extra
infrastructure, at the cost of depending on that page's layout. Force one with
GROUNDHOG_SEARCH_BACKEND=searxng|serp.
Every text field of a hit is attacker-influenceable — a poisoned page controls how it describes
itself — so each passes through the same invisible-character stripping as page content, and
each is length-capped. The URL is treated differently: it is what a model cites, so it is
never rewritten. A hit is dropped outright if cleaning would change its URL at all, if that
URL is not http/https, if it carries credentials, or if it exceeds 2048 characters. Both matter on the DuckDuckGo path, which percent-decodes
the redirect wrapper and can therefore turn %E2%80%8B back into a real zero-width
character inside the link. A backend that is unreachable, has JSON disabled, or whose every upstream engine
is rate-limited raises an actionable error rather than reporting an empty web.
research(query, max_sources=5, max_tokens=None)
One call for "find out about X": searches, reads the top sources through the stealth
browser, and returns the passages most relevant to query — ranked across all sources
in a single pass, so a passage from source 4 competes fairly with one from source 1.
Returns passages (each with text, source_url, heading, score) and sources (each
with url, title, status, page_status, threats, provenance). status is the fetch
outcome (ok / blocked / timeout / error); page_status is what a page that loaded
actually was — the same classification read_url reports, so a source that returned a
bot-challenge or a non-HTML body is visible rather than passing as ok — and is null when
the fetch never produced a page. A source whose page_status says its body is an interstitial
or an error page contributes no passages: it would otherwise compete for your token budget
against real content. It still appears in sources, saying why it contributed nothing. At most
one page per registrable domain, for source diversity. Passages are extracts, not summaries — nothing is generated,
and no model or API key is involved. When a passage isn't enough, read_url its
source_url for the whole page.
A source that fails doesn't fail the call: it appears in sources with a status of
blocked (SSRF guard), timeout, or error, so a partial answer is still usable and you
can see what was missed. Because search results are chosen by a third party — and
SEO-poisoned results are a documented in-the-wild attack — every fetched URL goes through
the same SSRF guard and hidden-text stripping as read_url, and each source reports what
was stripped from it. A source that failed carries provenance: null — only sources that
were actually read are hashed. threats is per-source here and capped at 10 entries per
source, lower than read_url's 50, because the fan-out multiplies it. max_sources is
capped at 10.
It's slower than an API-backed research tool: a real browser renders every source. That's the trade for reading pages that block plain fetchers, and for being able to tell you what was hidden in them.
status()
Reports whether Groundhog can reach the stealth browser. Returns browser_reachable,
cdp_url and a hint with remediation steps when it isn't reachable. The endpoint is
reported as scheme, host and port only — a hosted browser often carries a credential in
its URL, and this value reaches the model.
Configuration
MCP server (mcp/):
Env var | Default | Purpose |
|
| CDP endpoint of the stealth browser. May be remote (a DNS name or IP); auto-start is skipped for non-local values. The endpoint is unauthenticated — keep it on a private network or a tunnel. |
|
| Enforce the SSRF guard (resolve + block private ranges) |
|
| Minimum delay between requests to the same domain |
|
| Token budget before truncation |
|
| Cap on concurrent open tabs |
| (unset) | Your SearXNG instance for |
|
|
|
|
| Auto-pull-and-run the browser container when it isn't reachable (needs Docker/Podman); |
|
| Image used for auto-start |
| (none) | Use |
Dependencies: py3langid (which pulls in numpy) is used for language detection in the
provenance result. It is installed in the MCP server package only — not in the browser
container.
Browser container:
Env var | Default | Purpose |
| derived from installed Chrome | UA set at launch, so it is clean in every scope including workers |
| (none) | Upstream proxy ( |
|
| Fallback timezone; auto-derived from the exit IP when |
|
| Initial Chrome window size |
|
| Virtual display geometry |
Under the hood: the stealth Chrome container
A minimal Docker container running headful Chrome under Xvfb with a remote CDP endpoint. Any CDP-speaking client (Puppeteer, Playwright, Selenium, chromedp, raw DevTools) can drive it — Groundhog is one such client.
Headful under Xvfb, not
--headless=new— the browser reportsChrome, notHeadlessChrome, avoids headless-specific tells, and engages the real GPU path.--disable-blink-features=AutomationControlled—navigator.webdriverreadsfalse.UA set at launch from the installed Chrome version (
USER_AGENT), so it is clean in every scope — main frame, network, and Web/Service Worker globals.Proxy geo-coherence. When
PROXYis set, the entrypoint geolocates the exit IP and aligns the browser timezone and locale to it — a timezone or locale that disagrees with the IP is itself a block signal. The country→locale table is CLDR likely-subtags. Chrome can't authenticate to a proxy over--proxy-server, so credentials are relayed through a local tinyproxy; WebRTC is pinned to the proxy path so the real IP can't leak.GPU-aware WebGL. The entrypoint auto-detects a GPU (NVIDIA via the Container Toolkit, or Intel/AMD via
/dev/dri) and uses hardware acceleration; without one it runs Mesallvmpipe, a coherent software renderer that VMs and servers legitimately emit. See thegpus/deviceshints indocker-compose.yml.
Verified results
Measured against a freshly built container (Chrome 149, headful under Xvfb, no proxy), driven over raw CDP:
Detector | Result |
not a bot ( | |
Normal | |
31 / 31 checks pass |
iphey is tracked informationally, not pass/fail: its one recurring
flag is Location ("looks like you're trying to hide your location"), which fires on any
datacenter/hosting exit IP regardless of browser fingerprint or TZ correctness — it
passes on a residential IP and fails in CI (a cloud runner) and behind most proxies alike.
See RESULTS.md for the full live table (regenerated by
tests/antibot.py and the Conformance workflow).
These reflect the raw-CDP client. Full automation libraries (Puppeteer/Playwright/Selenium)
enable the CDP Runtime domain and are flagged as automated even against this container —
see examples/ for which need patched (rebrowser) variants.
Examples
Client | Path |
Puppeteer (Node) | |
Playwright (Node) | |
Playwright (Python) | |
Selenium (Python) | |
chromedp (Go) | |
Raw CDP (Python) |
See examples/OTHER_TOOLS.md for crawl4ai, Scrapy +
Playwright, go-rod, Crawlee, and nodriver pointers.
Security
The CDP endpoint is unauthenticated — anyone who can reach the port has full control
of the browser. Bind it to localhost or a trusted private network; never expose it to the
public internet. --no-sandbox is used because Chrome's sandbox does not work in an
unprivileged container; keep the container isolated. To report a vulnerability, see
SECURITY.md.
Limits of hidden-text detection
Worth knowing before treating an empty threats list as a clean bill of health. Nothing is
removed from the live page — the markup is stripped inside a separate inert document, which
is imported rather than cloned (cloneNode is itself [CEReactions]), and the rendered text
comes from the live page with the flagged nodes hidden by an adopted stylesheet. So a page
gets no synchronous hook to react to the strip. What that does not cover:
The style signals are thresholds, and the character set is a denylist. Those are the real limits — see below. The detector itself runs in an isolated world (
Page.createIsolatedWorld), so a page cannot suppress it by replacing the DOM APIs it uses; if the browser ever declines to provide one, the result carries adetection_degradedthreat rather than quietly weaker detection.Thresholds can be sat just inside.
opacity: 0.06,font-size: 4px, a contrast ratio just above 1.15 — all pass, as do hiding techniques the eleven signals don't model (clip-path,text-indent,transform: scale(0)).Invisible-character coverage is a set, not a rule. Zero-width, bidi and the Unicode Tag block are stripped and reported; codepoints outside that set are not.
When the text is rebuilt, line breaks are guessed from tag names. In the two cases above the rendered text is taken from the stripped markup, which has no layout — so an element the page styled
display:inlinestill gets a break, and a block-level tag outside the list gets none. Word boundaries are preserved; exact line structure is not.Closed shadow roots are not read. Open ones are: their content is scanned for hidden text and composed into the output as the flat tree a reader sees, slots included. A closed root is unreachable from the isolated world, so it cannot be scanned — and what cannot be scanned is not composed in. Its content stays out of the result entirely rather than arriving unexamined.
A page can win the cascade against the hiding sheet, or hide its own
<body>. An inline!importantbeats an author stylesheet, andinnerTextreturns raw text when nothing renders at all. In either case the rendered text is abandoned for the stripped markup, which is a weaker guarantee than reading real layout — reported asstrip_incompleterather than left to look like a clean strip.
Limits of block detection
status tells you a fetch returned a challenge or an error page rather than the content
you asked for. Worth knowing what it does and does not cover.
Mitigation markers are a list, and the list is not exhaustive. A challenge is called with certainty when the response carries a header that exists only to announce it (
cf-mitigated,x-vercel-mitigated,x-amzn-waf-action,x-dd-b,x-datadome-cid) or when the page requests an asset only a challenge loads (Cloudflare's orchestrator, DataDome, PerimeterX, Imperva). A vendor absent from that list, or one that changes its endpoint, falls through to the weaker signals below.The wording fallback is English, and only fires on an empty page. A challenge with no recognised marker is caught only if it renders almost no text and matches a known phrase. A localized interstitial from an unlisted vendor is the gap — it is why the markers exist, and why they are preferred over any amount of phrase tuning.
A soft block is undetectable here. A page that returns 200 with a plausible body but the data quietly withheld looks exactly like content. Nothing in this classification sees it; only comparing against what the page should contain would.
unknownis notok. It means no response, or no usable status, was observed for the document that was read. It is reported rather than assumed fine, and it is not treated as a block — a source carrying it still contributes passages inresearch.Vendor presence is not a block.
server: cloudflare,cf-rayand__cf_bmare on a large share of the web on every page it serves normally, so they are deliberately not used; nor iscf_clearance, which is issued when a challenge is passed.
What the SSRF guard blocks. Each host is resolved and rejected if it lands in loopback,
RFC-1918 private, link-local (incl. 169.254.169.254), reserved, multicast, unspecified,
CGNAT 100.64.0.0/10, or IPv4-mapped IPv6 ranges. Only http and https are allowed, and
credentials in URLs are rejected. The check runs again immediately before navigation, and
once more against final_url after redirects.
Limits of the SSRF guard. It is a strong default, not a sandbox. Know these before pointing it at untrusted URLs:
The guard resolves and checks the host before navigation and re-checks
final_urlafter the page loads. A redirect into a private address is therefore still requested by Chrome — its content is never returned, but a blind SSRF or a state-changing internalGEThas already landed. Intermediate hops in a longer redirect chain are not individually checked.Sub-resource requests the page itself issues (
img,script,iframe,fetch) are not intercepted; only the top-level navigation is checked.Groundhog resolves DNS in its own process while Chrome resolves independently at navigate time, so a short-TTL rebinding window remains open. Closing these properly needs request-level interception (CDP
Fetch).Fetches share the browser's default profile — targets are created without a separate browser context — so cookies and storage set by one page persist into later fetches. "Read-only" describes Groundhog's own API, not the JavaScript on a fetched page, which can issue requests of its own from that shared profile.
Set GROUNDHOG_BLOCK_PRIVATE_IPS=false only on a network where reaching internal addresses
is intended.
A note on "stealth"
Best-effort, not a guarantee. It defeats common open-source detectors and lets cheap proxies work on many mid-tier targets, but it does not beat sophisticated commercial anti-bot systems that gate on IP reputation, TLS/HTTP2 fingerprints, and behavioral analysis. Use it for legitimate, authorized automation and testing.
Privacy Policy
Groundhog is self-hosted software, not a service. It runs on your machine, and the project operates no servers that it talks to.
What is collected: nothing. There is no telemetry, no analytics, no crash reporting and no licence check. The maintainers receive no data about you, the URLs you fetch, or the content you read. There is no account to create.
How data is used and stored. Pages are fetched by a browser running on your own machine
and returned to the MCP client that asked for them. The server keeps no database, writes no
logs to disk, and persists nothing between calls — with one exception worth knowing: fetches
share the browser container's profile, so cookies and storage set by one fetched page remain
in that container and are visible to later fetches. Removing the container discards them
(docker rm -f groundhog-browser), and the container is removed automatically if you started
it with --rm.
Third parties your traffic reaches. Only those you direct it to, plus two you should know about:
The sites you fetch, which see the request as an ordinary browser visit from your IP — or from your proxy's exit IP if
PROXYis set.The search backend. With
SEARXNG_URLset, your queries go to the SearXNG instance you chose. Without it,searchrenders a public search engine's results page through the browser, so that engine sees the query.An IP-geolocation lookup, only when
PROXYis set. The container asks an external service for the proxy exit IP's country so it can align the browser's timezone and locale. That request carries the exit IP and nothing else. It does not happen without a proxy.PyPI and GHCR at install time, to download the package and browser image.
Retention. Nothing is retained by the project. On your machine, the browser container holds cookies and cache for its lifetime; deleting the container deletes them.
Contact. Questions and security reports: SECURITY.md, or open an issue at
https://github.com/dmytrome/groundhog/issues.
License
Available Tools
4 toolsread_urlRead a URLARead-only
Fetch one web page through the stealth browser and return clean, grounded content with provenance.
Hidden text injected for models but invisible to humans is stripped by default
and reported in threats. Use this to ground answers in live web content,
including sites that block plain fetchers.
Reads a URL you already have: use search to find URLs, or research to
search and read in one call. Fetches are rate limited per domain (5s apart by
default), so several pages from one site are not instant.
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | Absolute http(s) URL. Private and loopback addresses are refused. | |
| query | No | When set, `matches` carries the passages most relevant to it, each with its heading and offset for citation. | |
| format | No | 'markdown' extracts the article; 'text' returns the page's rendered text. | markdown |
| max_tokens | No | Token budget for the content. Omit to use the server's GROUNDHOG_MAX_TOKENS (20000 by default). Must be positive. | |
| include_hidden | No | Keep text that is invisible to a human reader. It is reported in `threats` either way; this only controls whether it stays in the content. |
Output Schema
| Name | Required | Description |
|---|---|---|
| url | Yes | |
| title | Yes | |
| status | Yes | |
| matches | Yes | |
| threats | Yes | |
| markdown | Yes | |
| final_url | Yes | |
| truncated | Yes | |
| fetched_at | Yes | |
| provenance | Yes | |
| http_status | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, so the safety profile is covered. The description adds meaningful behavioral context beyond annotations: hidden-text stripping with threat reporting, stealth-browser fetching for blocked sites, and per-domain rate limiting. It does not contradict annotations. The only minor gap is no mention of failure modes or what 'grounded content with provenance' structurally looks like, though the output schema likely covers return shape.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight paragraphs, each earning its place: first defines the action and output, second explains the hidden-text safety behavior and value proposition, third gives usage guidance, alternatives, and rate-limit caveat. It's appropriately sized for a tool with 5 parameters and meaningful behavioral nuance, though it leans slightly long relative to its sibling-differentiation needs.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, return-value details are handled structurally. The description covers the tool's distinctive behaviors (stealth fetching, hidden-text stripping, provenance), usage guidance, rate limits, and alternatives. For a tool of moderate complexity with rich schema and annotations, the description is complete enough. It could add slightly more on error/failure behavior, but it is not a material gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all five parameters are documented in the schema. The description resists repeating schema detail and instead adds cross-parameter context (e.g., 'reported in `threats`' ties include_hidden and hidden-text stripping together). The `query` and `format` behaviors become clearer through the description's grounding framing, but the schema already does the heavy lifting. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb+resource ('Fetch one web page through the stealth browser') and clearly states what it returns ('clean, grounded content with provenance'). It explicitly differentiates from siblings: 'use `search` to find URLs, or `research` to search and read in one call', naming the alternatives directly. The reference to 'sites that block plain fetchers' adds a clear use-case distinction.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly states when to use this tool vs alternatives: 'Reads a URL you already have: use search to find URLs, or research to search and read in one call.' It also discloses the rate-limiting behavior ('5s apart by default'), setting expectations about latency. This is explicit when/alternative guidance with concrete operational context.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
researchResearch the webARead-only
Search the web and return ranked passages drawn from several sources.
One call does what search + repeated read_url would: finds pages, reads
them through the stealth browser, and returns the passages most relevant to
query — each attributed to its source, with that source's provenance
receipt and any stripped injection payloads. A source that fails is reported
in sources rather than failing the whole call.
Prefer read_url when you already have the URL, and search when you only
want links. This reads max_sources pages, rate limited per domain, so it is
the slowest of the three and the one to avoid for a single known page.
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | The question to research. Passages are ranked against it. | |
| max_tokens | No | Token budget for the returned passages. Omit to use the server's GROUNDHOG_MAX_TOKENS (20000 by default). Must be positive. | |
| max_sources | No | How many pages to read. Values outside 1-10 are clamped rather than rejected. Each source is a full page fetch, so this is the main cost and latency control. |
Output Schema
| Name | Required | Description |
|---|---|---|
| query | Yes | |
| backend | Yes | |
| sources | Yes | |
| passages | Yes | |
| truncated | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, and the description adds meaningful behavioral context: that it reads through a stealth browser, returns provenance receipts, strips injection payloads, and reports failed sources in `sources` rather than failing the whole call. This adds real behavioral value beyond the annotations, though return format specifics could be further detailed beyond what the output schema shows.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three paragraphs, each earning its place: what it does, how failures are handled, and when to use alternatives. No wasted words, well front-loaded with the purpose sentence.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a complex tool with an output schema and good annotations, the description covers usage differentiation, failure semantics, cost/latency control, and behavioral traits. An output schema exists so return values needn't be spelled out. Highly complete for its complexity.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema documents all three parameters. The description adds value by explaining max_sources is the main cost/latency control and that values outside 1-10 are clamped, and that max_tokens has a server default (GROUNDHOG_MAX_TOKENS). It supplements but doesn't fully compensate the structured schema since the schema is already rich.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (research/search) plus resource (the web), and clearly differentiates from siblings by noting this combines `search` + repeated `read_url` into one call. The distinction from read_url and search is explicit.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit when-to-use guidance: prefer `read_url` when you already have the URL, `search` when you only want links. Also warns this is the slowest of the three due to rate limiting per domain, and should be avoided for a single known page. This is model-tier guidance naming alternatives and exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
searchWeb searchARead-only
Search the web and return ranked hits (title, url, snippet, engine).
Not for reading: it returns links, never page content. Use read_url for one
known URL, or research when you want the answer rather than the links.
Use this to find pages, then pass the URLs you want to read_url for safe,
grounded content. Results come from a self-hosted SearXNG instance when
SEARXNG_URL is set, otherwise from a search page rendered through the
stealth browser. Hits are links only — nothing is fetched until you ask.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | How many hits to return. Values outside 1-25 are clamped rather than rejected. | |
| query | Yes | What to search for. Must not be empty. |
Output Schema
| Name | Required | Description |
|---|---|---|
| hits | Yes | |
| query | Yes | |
| backend | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations declare readOnlyHint=true and openWorldHint=true, which aligns with the description (read-only, returns links, nothing fetched). The description adds valuable context beyond annotations: explains the dual backend (SearXNG when SEARXNG_URL set, otherwise stealth-browser rendered page) and clarifies no content is fetched until requested. Confirms the lazy-fetch behavior without contradiction.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Well-structured with a clear first line stating the core purpose, followed by a 'Not for' exclusion line, a usage-recommendation paragraph, and a backend-detail sentence. Every sentence earns its place; nothing is redundant or padded. Approximately 90 words for a tool that does meaningful work.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the output schema exists, return values are covered structurally. The description covers purpose, exclusions, alternative tools, workflow recommendation, and backend behavior. For a 2-param read-only search tool, this is fully complete—the agent has everything needed to select and invoke correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and both parameters (query, limit) are described in the schema itself. The description mentions the tool returns ranked hits but adds nothing about parameter syntax or semantics beyond schema. Baseline 3 is appropriate given full schema coverage; description adds minimal extra parameter value but the schema fully documents them.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Clear verb+resource ('Search the web') with explicit output format (ranked hits: title, url, snippet, engine). Strongly distinguishes from siblings by explicitly stating what it is NOT for (returning page content vs links).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Excellent when/when-not guidance: explicitly says use read_url for one known URL, research when you want the answer rather than links. Also gives a concrete workflow pattern: find pages, then pass URLs to read_url. This is exactly the differentiation agents need among the three sibling search-ish tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
statusBrowser statusARead-only
Check whether Groundhog can reach the stealth browser. Call this to
diagnose setup before fetching: if browser_reachable is false, follow
hint to start the browser, then retry.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| hint | Yes | |
| cdp_url | Yes | |
| browser_reachable | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description names the specific output field `browser_reachable` and the `hint` field, giving the agent concrete knowledge of what to expect. Annotations (readOnlyHint=true) already signal this is a safe read operation. The description adds value by explaining what the output means behaviorally (telling the agent what to do when unreachable), going beyond the annotation safety profile.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the core purpose first, followed by actionable diagnostics. No wasted words. Slight deduction could be argued for not being maximally compact, but the conditional instruction earns its place since it directly guides agent behavior. Efficient and purposeful.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
This is a simple, parameterless diagnostic tool with an output schema present, so the description doesn't need to explain return values extensively. The description covers the purpose, when to use it, and the conditional action to take based on the result. For a tool of this complexity, this is complete — nothing meaningful is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool has 0 parameters, so there is no parameter meaning to convey — the schema itself documents this fully (100% coverage by definition since there are no params). The task is parameter-free, so a baseline of 4-5 applies, and the description fully compensates by explaining what the tool checks and what its output communicates.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: checking whether Groundhog can reach the stealth browser. It uses a specific verb ('check') with a clear resource ('reach the stealth browser') and includes a diagnostic framing, distinguishing it from sibling tools (read_url, research, search) which are substantive data-gathering operations rather than setup diagnostics.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit when-to-use guidance: 'Call this to diagnose setup before fetching.' It also provides conditional follow-up instructions: if `browser_reachable` is false, follow `hint` to start the browser, then retry. This is directly actionable and tells the agent exactly the workflow to follow.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
2 tool updates
v0.10.0- Changed
read_url3 fields changed- added
Output schema / properties / http_statusAdded value: +{ + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Http Status" +} - added
Output schema / properties / statusAdded value: +{ + "enum": [ + "ok", + "challenge", + "blocked", + "rate_limited", + "not_found", + "server_error", + "unsupported_content", + "unknown" + ], + "title": "Status", + "type": "string" +} - changed
Output schema / requiredPrevious value: -[ - "markdown", - "title", - "url", - "final_url", - "fetched_at", - "truncated", - "threats", - "matches", - "provenance" -]New value: +[ + "markdown", + "title", + "url", + "final_url", + "fetched_at", + "status", + "http_status", + "truncated", + "threats", + "matches", + "provenance" +]
- Changed
research2 fields changed- added
Output schema / $defs / Source / properties / page_statusAdded value: +{ + "anyOf": [ + { + "enum": [ + "ok", + "challenge", + "blocked", + "rate_limited", + "not_found", + "server_error", + "unsupported_content", + "unknown" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Page Status" +} - changed
Output schema / $defs / Source / requiredPrevious value: -[ - "url", - "title", - "status", - "threats", - "provenance", - "error" -]New value: +[ + "url", + "title", + "status", + "page_status", + "threats", + "provenance", + "error" +]
3 tool updates
v1.0.0- Changed
read_url5 fields changed- added
Input schema / properties / format / descriptionAdded value: +"'markdown' extracts the article; 'text' returns the page's rendered text." - added
Input schema / properties / include_hidden / descriptionAdded value: +"Keep text that is invisible to a human reader. It is reported in `threats` either way; this only controls whether it stays in the content." - added
Input schema / properties / max_tokens / descriptionAdded value: +"Token budget for the content. Omit to use the server's GROUNDHOG_MAX_TOKENS (20000 by default). Must be positive." - added
Input schema / properties / query / descriptionAdded value: +"When set, `matches` carries the passages most relevant to it, each with its heading and offset for citation." - added
Input schema / properties / url / descriptionAdded value: +"Absolute http(s) URL. Private and loopback addresses are refused."
- Changed
research3 fields changed- added
Input schema / properties / max_sources / descriptionAdded value: +"How many pages to read. Values outside 1-10 are clamped rather than rejected. Each source is a full page fetch, so this is the main cost and latency control." - added
Input schema / properties / max_tokens / descriptionAdded value: +"Token budget for the returned passages. Omit to use the server's GROUNDHOG_MAX_TOKENS (20000 by default). Must be positive." - added
Input schema / properties / query / descriptionAdded value: +"The question to research. Passages are ranked against it."
- Changed
search2 fields changed- added
Input schema / properties / limit / descriptionAdded value: +"How many hits to return. Values outside 1-25 are clamped rather than rejected." - added
Input schema / properties / query / descriptionAdded value: +"What to search for. Must not be empty."
4 tool updates
- First observed
read_url - First observed
research - First observed
search - First observed
status
TDQS
The four tools have mostly distinct purposes: read_url reads a known URL, search returns links only, research does search+read combined, and status is a diagnostic check. Statatus is clearly separate. The main potential confusion is between read_url and research, but the descriptions explicitly clarify when to use each (read_url for known URLs, search for links only, research for combined search+read), which mitigates overlap well.
Tool names use consistent, short lowercase verbs (read_url, research, search, status) with an underscore pattern for read_url. The naming is predictable and readable. Minor deviation: research is a compound concept rather than a verb_noun pair, but this is a minor stylistic point since all names are lowercase, consistent verbs.
At 4 tools, this is a lean set that covers the core web-research workflow: status (diagnose), search (find URLs), read_url (read one page), research (combined find+read). Each tool serves a clear purpose with minimal redundancy. Slightly thin but appropriately scoped for a focused browser/search server.
The server covers the core browse/search lifecycle well: diagnose, search, read, and combined research. However, there are gaps such as no way to extract or transform content beyond returning passages, no pagination or follow-up operations, and no direct way to get raw content or handle specific output formats. The core workflow (search-then-read) is complete, but deeper research needs are not addressed.
Maintenance
Related MCP Connectors
MCP server (stdio): fetch web pages as clean readable markdown via the AgentForge API
Read any web page as clean Markdown for AI agents: fetch, search, metadata, links. SSRF-safe.
Hosted MCP server: convert PDFs to clean, LLM-ready Markdown with tables, formulas and OCR.
Docs: https://docs.keenable.ai/mcp-server Keenable is a free, remote MCP server that gives agents access to the web index. Search the web with ranked results and date/site filters, then fetch any indexed page as clean markdown. Works out of the box with no account or API key.
Related MCP Servers
- AlicenseAqualityAmaintenanceThis server enables LLMs to retrieve and process content from web pages, converting HTML to markdown for easier consumption.190,042MIT
- AlicenseAqualityCmaintenanceA Model Context Protocol (MCP) server implementation that integrates with FireCrawl for advanced web scraping capabilities.2640,1397,395MIT

Playwright MCP Serverofficial
AlicenseBqualityAmaintenanceA Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.2245,881,52736,824Apache 2.0
Jina AI Remote MCP Serverofficial
AlicenseAqualityCmaintenanceEnables web content extraction, screenshot capture, web search, arXiv paper search, and image search through Jina AI's APIs. Provides tools for reading URLs as markdown, searching the web for current information, and finding academic papers or images.19840Apache 2.0
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/dmytrome/groundhog'
If you have feedback or need assistance with the MCP directory API, please join our Discord server