Groundhog
Groundhog is a read-only MCP server that searches, reads, and researches the live web through a stealth-patched Chrome, returning clean Markdown with provenance receipts and hidden-text stripping.
search(query, limit=10)— find pages and get ranked links only (title,url,snippet,engine,score,published) via self-hosted SearXNG or a browser-rendered results page; nothing is fetched.read_url(url, format, max_tokens, query, include_hidden)— fetch one page and return clean Markdown or text, pagetitle/url/final_url,fetched_at,status(ok/challenge/blocked/rate_limited/not_found/server_error/unsupported_content/unknown),http_status,truncated,threats,matches,set_aside, andprovenance.Strips hidden text by default — text invisible to humans (hidden CSS, invisible characters, hidden attributes/templates, etc.) is removed before the model reads it and reported in
threats;include_hidden=Truekeeps it.Provenance receipts — every read source carries a SHA-256 content hash, canonical URL, language, word count, and author/date metadata when the page declares them.
Relevance-ranked passages — pass
querytoread_urlto get the passages that answer it (with heading, offset, score) instead of blunt head truncation.research(query, max_sources=5, max_tokens)— one call that searches and reads up to 10 sources, returningpassagesranked across all sources plussourceswith per-source status,page_status,threats, andprovenance; failing sources are reported rather than failing the call.status()— check whether the stealth browser is reachable, withcdp_urland a remediationhint.Safe by default — SSRF guard blocking private/loopback ranges (incl. redirects and frame navigations), read-only annotations, per-domain rate limiting, and no telemetry or API key required.
Allows using SearXNG as a search backend for the search tool, providing ranked search results from a private SearXNG instance.
Click on "Deploy 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 returns clean Markdown a model can trust: text no human could see is stripped before the model reads it and reported, every source comes with a provenance receipt, and a real browser reads pages that block plain fetchers.
agent / crawler ──MCP──▶ Groundhog (search, read_url, research) ──CDP──▶ stealth Chrome ──▶ the webMeasured against other fetch layers
Twenty-eight pages carry text a human can't see, and three controls carry none. A case passes when the payload doesn't reach the model and the article text still does.
contained | kept the article | reported it | false positives | |
| 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 |
Other extractors often drop hidden text by accident, but none of them tells the caller
anything was removed. The three cases Groundhog doesn't report (a hidden input value, a meta
description, an iframe srcdoc) never reach any extractor. The corpus is public at
dmytrome.github.io/groundhog; the harness and its
limits are in benchmark/.
Related MCP server: firecrawl-mcp-server
Quick start
Claude Code:
claude mcp add groundhog -- uvx groundhog-mcpClaude Desktop, Cursor, Windsurf:
{
"mcpServers": {
"groundhog": {
"command": "uvx",
"args": ["groundhog-mcp"]
}
}
}On the first fetch Groundhog starts the Chrome you already have, headless, in its own
profile at ~/.groundhog/chrome. Your own browsing, tabs and logins are never touched. If
Chrome is missing, status and the error say what to install.
GROUNDHOG_BROWSER=stealth runs the hardened browser container instead (Docker or Podman;
the first pull takes a few minutes). To run it yourself:
docker run -d --rm --name groundhog-browser --shm-size 512m \
-p 127.0.0.1:9222:9222 -- ghcr.io/dmytrome/groundhog:latestCDP_URL points Groundhog at any browser and is used as given: http:// is probed for
/json/version, ws:// or wss:// (what hosted browsers hand out) is connected to directly.
Auto-start only runs for a local address that isn't answering, and
GROUNDHOG_AUTO_START_BROWSER=false turns it off. From source: cd mcp && uv sync && uv run groundhog-mcp.
All four tools are annotated readOnlyHint: they write nothing and change no remote state.
The one side effect is auto-start, which may launch Chrome or, with stealth, run the
container after removing an unreachable one named groundhog-browser.
What makes it different
Hidden text is stripped and reported. A rendered DOM shows what a human would see; everything else goes, and each removal is listed in
threats. It's a strong heuristic, not a proof: see the limits.Every source carries a receipt: a SHA-256 of the extracted content, canonical URL, language, word count, and author and date when the page declares them.
Safe by default. An SSRF guard checks every document request, redirects and frames included, with per-domain rate limiting. See Security.
No automation tell. Groundhog drives Chrome over raw CDP and never enables the
Runtimedomain that anti-bots detect in Puppeteer, Playwright and Selenium.A real fingerprint. Real Chrome means real TLS, HTTP/2, WebGL and canvas. In headless mode the
HeadlessChrometoken is replaced and client hints are copied from the browser; shared and service workers aren't covered. The stealth image runs headful under Xvfb.No model, no API key.
researchreturns extracts and your agent does the synthesis.
Tools
read_url(url, format="markdown", max_tokens=None, query=None, include_hidden=False)
Key | Meaning |
| Extracted content, article first, falling back to full text; |
| Page title |
| The URL you asked for |
| The URL after redirects, re-checked by the SSRF guard. If unusable, the requested URL is reported with a |
| UTC ISO-8601 timestamp |
|
|
| The top-level HTTP status, or |
| Whether the token budget cut the content |
| What was stripped; empty when nothing was found |
| With |
| With |
| Content hash, canonical URL, language, word count, author and date |
Stripped by default: display:none, visibility:hidden, content-visibility:hidden,
opacity ≤ 0.05, font-size < 4px, zero-size and display:contents boxes that render
nothing, .sr-only-style sub-pixel boxes, clip: rect(...), transparent or
background-coloured text, off-page positioning, collapsed <details>, <template> content,
long alt / aria-label / aria-description / title text, and zero-width, bidi and
Unicode Tag characters. HTML comments are reported too. include_hidden=True keeps the text
but still reports it.
Treat threats as untrusted: its excerpts are page-authored text, cleaned and length-capped.
| Carries |
| The hiding |
| The same, for text in an |
| The same, for text inside a |
| A codepoint and count in |
| How many entries the cap dropped |
| The page's final URL was unusable and not returned |
| Detection ran in the page's own JavaScript world, which the page can tamper with |
| The text was rebuilt from markup rather than layout, usually because of open shadow roots |
At most 50 findings per page are returned (10 per source in research), with the two classes
capped separately so decoys can't bury a real payload. Notices are appended after the cap.
query swaps head-truncation for BM25 passage ranking on the sanitized content, keeping
passages within half the best score up to the budget.
search(query, limit=10)
Returns hits (title, url, snippet, engine, score, published) and the backend
that answered. Nothing is fetched. With SEARXNG_URL set it uses your
SearXNG instance, which needs formats: [html, json]; otherwise
it renders a search page through the browser. Hit text is cleaned and capped, and a hit whose
URL would change under cleaning, isn't http(s), carries credentials or exceeds 2048
characters is dropped. A backend that can't answer raises an error instead of returning an
empty web.
research(query, max_sources=5, max_tokens=None, include_domains=None, exclude_domains=None, published_after=None, aim=None)
Searches, reads the top sources and ranks passages from all of them in one pass.
passages:id,text,source_url,heading,score,start,end,clamped,fence.textis exactlymarkdown[start:end]unlessclamped. A passage cut from a long code block carries the block's opening line infence: reopen with it whentextdoesn't start with it, and close the block whentextdoesn't end with a closing fence.idstays stable while the page doesn't change.sources:url,title,status(ok/blocked/timeout/error),page_status(as inread_url),threats,provenance,filtered. A failed source doesn't fail the call. A challenge, an error page or a page with no text (whosepage_statusstaysok) counts asnot_contentand contributes no passages.One page per registrable domain unless
include_domainsis set, and then no more pages per site thanGROUNDHOG_MAX_CONCURRENT_PAGES;max_sourcesgoes up to 10.
include_domains reads only those domains and their subdomains, exclude_domains never
reads them, and published_after (YYYY-MM-DD) drops pages dated earlier. Up to 8 domains
can be included and 50 excluded. A page that redirects into an excluded domain or out of the
included ones, is dated too early, or repeats the URL or text of a page already used is listed
with filtered and contributes no passages. The query sent to the engine names at most eight
domains; further excluded ones are filtered after the search. aim (up to 200 characters)
says what the evidence must show and is echoed back without changing the search or the
ranking.
receipt holds the executed_query, the distinct hits and hits_by_domain, the hits not
fetched (hits_excluded, hits_outside_domains, hits_before_date, hits_same_domain,
hits_unread past the source limit), each source counted
once as failed, not_content, filtered or usable, outside_domains, unknown_dates,
and the passages omitted, set_aside or clamped. Read it before concluding the web has
no answer.
status()
Returns browser_reachable, cdp_url (scheme, host and port only) and a hint when the
browser can't be reached.
Configuration
MCP server:
Env var | Default | Purpose |
|
| CDP endpoint, local or remote. Unauthenticated, so keep it private |
|
| What auto-start launches: |
| (found) | Chrome executable |
|
| Profile directory; a login made here is reused |
|
| The SSRF guard |
|
| Delay between requests to one domain |
|
| Token budget |
|
| Concurrent tabs |
| (unset) | SearXNG instance for |
|
|
|
|
| Start the browser when a local |
|
| Image for |
| (none) | Start with |
Browser container:
Env var | Default | Purpose |
| derived from installed Chrome | UA set at launch, clean in every scope |
| (none) | Upstream proxy |
|
| Fallback timezone |
|
| Chrome window size |
|
| Virtual display |
The stealth Chrome container
Headful Chrome under Xvfb with a CDP endpoint, usable by any CDP client. It reports Chrome
rather than HeadlessChrome, sets navigator.webdriver to false, and sets the UA at launch.
With PROXY, timezone and locale match the exit IP, credentials go through a local tinyproxy,
and WebRTC is pinned to the proxy. WebGL uses a detected GPU, else Mesa llvmpipe.
Against a fresh container (Chrome 151, no proxy), over raw CDP:
Detector | Result |
not a bot ( | |
Normal | |
31 / 31 checks pass |
iphey flags any datacenter IP, so it's tracked but not scored. The live
table is in RESULTS.md. Puppeteer, Playwright and Selenium are still flagged
because they enable Runtime; examples/ has clients for each, and
OTHER_TOOLS.md covers more.
Security
The CDP endpoint is unauthenticated: anyone who reaches the port controls the browser. Keep it
on localhost or a private network. The stealth image runs --no-sandbox, so keep the
container isolated. Report vulnerabilities via SECURITY.md.
The SSRF guard resolves each host and refuses loopback, private, link-local (including
169.254.169.254), reserved, multicast, unspecified, CGNAT and IPv4-mapped IPv6 addresses,
non-http(s) schemes, and URLs with credentials. It checks every document request Chrome is
about to send, redirects and cross-site frames included, and final_url once more.
Sub-resources are left to Chrome's Local Network Access rule, and the Chrome that Groundhog
launches has preloading turned off.
Limits of the guard:
Only documents are checked. An older Chrome, or a site granted local-network access in the profile, isn't covered for sub-resources.
Preloading stays on in the stealth container and in a browser you supply.
Groundhog and Chrome resolve DNS separately, so a short-TTL rebinding window remains.
Fetches share one profile, so cookies set by one page persist into later fetches, and page JavaScript can make its own requests from it.
GROUNDHOG_BLOCK_PRIVATE_IPS=false turns the guard off; use it only where reaching internal
addresses is intended.
Limits of hidden-text detection
An empty threats list isn't a clean bill of health. Detection runs in an isolated world the
page can't tamper with, and the live page is never modified. But:
Thresholds can be sat just inside (
opacity: 0.06), and unmodelled techniques such asclip-path,text-indentortransform: scale(0)pass.The invisible-character set is a denylist.
Closed shadow roots can't be read, so their content is left out entirely.
When the page wins the cascade against the hiding stylesheet or hides its own
<body>, text is rebuilt from markup, line breaks are guessed, andstrip_incompletesays so.
Limits of block detection
A challenge is called with certainty from vendor headers (
cf-mitigated,x-vercel-mitigated,x-amzn-waf-action,x-dd-b,x-datadome-cid) or challenge assets (Cloudflare, DataDome, PerimeterX, Imperva). Other vendors fall back to English wording on a near-empty page.A soft block, a 200 with the data quietly withheld, looks like content.
unknownisn'tok, but it isn't treated as a block either.Vendor presence (
server: cloudflare,cf-ray) is deliberately ignored.
A note on "stealth"
Best effort. It beats common open-source detectors, not commercial anti-bot systems that gate on IP reputation and behaviour. Use it for legitimate, authorized automation.
Privacy Policy
Groundhog is self-hosted software and talks to no servers the project runs. It collects nothing: no telemetry, analytics, crash reports or accounts.
Pages are fetched by a browser on your machine and returned to your MCP client. The server
keeps no database and writes no logs. The browser profile does persist cookies and storage:
~/.groundhog/chrome for local Chrome (delete the folder to discard it), or the container for
stealth (docker rm -f groundhog-browser).
Your traffic reaches the sites you fetch, your search backend (your SearXNG, or a public search
engine without one), an IP-geolocation service for the proxy's exit IP only when PROXY is
set, and PyPI and GHCR at install time.
Questions: SECURITY.md or https://github.com/dmytrome/groundhog/issues.
License
Available Tools
4 toolsread_urlRead a URLRead-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, the content is built from the passages most relevant to it, and `matches` lists each one's heading, offset and score for citation; it carries no text. If nothing matches, `matches` is empty and the whole page is returned. On a long page the relevant passages can still be most of it, so set `max_tokens` to bound the read. | |
| 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 | |
| set_aside | Yes | |
| truncated | Yes | |
| fetched_at | Yes | |
| provenance | Yes | |
| http_status | Yes |
researchResearch the webRead-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 |
|---|---|---|---|
| aim | No | What the evidence must show, in at most 200 characters: the authority, artifact and version sought. Echoed in the receipt; it does not change the search or the ranking. | |
| 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. | |
| exclude_domains | No | Never read pages on these domains (at most 50). | |
| include_domains | No | Read only pages on these domains or their subdomains (at most 8), e.g. a standards body or the vendor's docs. Several pages from one domain may be read. | |
| published_after | No | YYYY-MM-DD. Leave out pages dated before this day. A page with no date is kept and counted in the receipt's `unknown_dates`. |
Output Schema
| Name | Required | Description |
|---|---|---|
| query | Yes | |
| backend | Yes | |
| receipt | Yes | |
| sources | Yes | |
| passages | Yes | |
| truncated | Yes |
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.
2 tool updates
v0.18.0- Changed
read_url4 fields changed- changed
Input schema / properties / query / descriptionPrevious value: -"When set, `matches` carries the passages most relevant to it, each with its heading and offset for citation."New value: +"When set, the content is built from the passages most relevant to it, and `matches` lists each one's heading, offset and score for citation; it carries no text. If nothing matches, `matches` is empty and the whole page is returned. On a long page the relevant passages can still be most of it, so set `max_tokens` to bound the read." - added
Output schema / $defs / Match / properties / endAdded value: +{ + "title": "End", + "type": "integer" +} - added
Output schema / $defs / Match / properties / idAdded value: +{ + "title": "Id", + "type": "string" +} - changed
Output schema / $defs / Match / requiredPrevious value: -[ - "heading", - "offset", - "score" -]New value: +[ + "id", + "heading", + "offset", + "end", + "score" +]
- Changed
research15 fields changed- added
Input schema / properties / aimAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What the evidence must show, in at most 200 characters: the authority, artifact and version sought. Echoed in the receipt; it does not change the search or the ranking.", + "title": "Aim" +} - added
Input schema / properties / exclude_domainsAdded value: +{ + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Never read pages on these domains (at most 50).", + "title": "Exclude Domains" +} - added
Input schema / properties / include_domainsAdded value: +{ + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Read only pages on these domains or their subdomains (at most 8), e.g. a standards body or the vendor's docs. Several pages from one domain may be read.", + "title": "Include Domains" +} - added
Input schema / properties / published_afterAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "YYYY-MM-DD. Leave out pages dated before this day. A page with no date is kept and counted in the receipt's `unknown_dates`.", + "title": "Published After" +} - added
Output schema / $defs / Passage / properties / clampedAdded value: +{ + "title": "Clamped", + "type": "boolean" +} - added
Output schema / $defs / Passage / properties / endAdded value: +{ + "title": "End", + "type": "integer" +} - added
Output schema / $defs / Passage / properties / fenceAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Fence" +} - added
Output schema / $defs / Passage / properties / idAdded value: +{ + "title": "Id", + "type": "string" +} - added
Output schema / $defs / Passage / properties / startAdded value: +{ + "title": "Start", + "type": "integer" +} - changed
Output schema / $defs / Passage / requiredPrevious value: -[ - "text", - "source_url", - "heading", - "score" -]New value: +[ + "id", + "text", + "source_url", + "heading", + "score", + "start", + "end", + "clamped", + "fence" +] - added
Output schema / $defs / ReceiptAdded value: +{ + "properties": { + "aim": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Aim" + }, + "clamped": { + "title": "Clamped", + "type": "boolean" + }, + "exclude_domains": { + "items": { + "type": "string" + }, + "title": "Exclude Domains", + "type": "array" + }, + "executed_query": { + "title": "Executed Query", + "type": "string" + }, + "failed": { + "title": "Failed", + "type": "integer" + }, + "filtered": { + "title": "Filtered", + "type": "integer" + }, + "hits": { + "title": "Hits", + "type": "integer" + }, + "hits_before_date": { + "title": "Hits Before Date", + "type": "integer" + }, + "hits_by_domain": { + "additionalProperties": { + "type": "integer" + }, + "title": "Hits By Domain", + "type": "object" + }, + "hits_excluded": { + "title": "Hits Excluded", + "type": "integer" + }, + "hits_outside_domains": { + "title": "Hits Outside Domains", + "type": "integer" + }, + "hits_same_domain": { + "title": "Hits Same Domain", + "type": "integer" + }, + "hits_unread": { + "title": "Hits Unread", + "type": "integer" + }, + "include_domains": { + "items": { + "type": "string" + }, + "title": "Include Domains", + "type": "array" + }, + "not_content": { + "title": "Not Content", + "type": "integer" + }, + "omitted": { + "title": "Omitted", + "type": "integer" + }, + "outside_domains": { + "title": "Outside Domains", + "type": "integer" + }, + "published_after": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Published After" + }, + "set_aside": { + "title": "Set Aside", + "type": "integer" + }, + "unknown_dates": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Unknown Dates" + }, + "usable": { + "title": "Usable", + "type": "integer" + } + }, + "required": [ + "executed_query", + "aim", + "include_domains", + "exclude_domains", + "published_after", + "hits", + "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" + ], + "title": "Receipt", + "type": "object" +} - added
Output schema / $defs / Source / properties / filteredAdded value: +{ + "anyOf": [ + { + "enum": [ + "excluded_domain", + "outside_domains", + "published_before", + "duplicate" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Filtered" +} - changed
Output schema / $defs / Source / requiredPrevious value: -[ - "url", - "title", - "status", - "page_status", - "threats", - "provenance", - "error" -]New value: +[ + "url", + "title", + "status", + "page_status", + "threats", + "provenance", + "error", + "filtered" +] - added
Output schema / properties / receiptAdded value: +{ + "$ref": "#/$defs/Receipt" +} - changed
Output schema / requiredPrevious value: -[ - "query", - "backend", - "passages", - "sources", - "truncated" -]New value: +[ + "query", + "backend", + "passages", + "sources", + "truncated", + "receipt" +]
1 tool update
v0.16.1- Changed
read_url2 fields changed- added
Output schema / properties / set_asideAdded value: +{ + "title": "Set Aside", + "type": "integer" +} - changed
Output schema / requiredPrevious value: -[ - "markdown", - "title", - "url", - "final_url", - "fetched_at", - "status", - "http_status", - "truncated", - "threats", - "matches", - "provenance" -]New value: +[ + "markdown", + "title", + "url", + "final_url", + "fetched_at", + "status", + "http_status", + "truncated", + "threats", + "matches", + "set_aside", + "provenance" +]
2 tool updates
v0.15.0- Changed
read_url1 field changed- added
Output schema / $defs / Threat / properties / seenAdded value: +{ + "title": "Seen", + "type": "integer" +}
- Changed
research1 field changed- added
Output schema / $defs / Threat / properties / seenAdded value: +{ + "title": "Seen", + "type": "integer" +}
2 tool updates
v0.13.0- Changed
read_url1 field changed- changed
Output schema / $defs / Threat / properties / type / enumPrevious value: -[ - "hidden_css", - "report_truncated", - "final_url_suppressed", - "detection_degraded", - "strip_incomplete", - "zero_width", - "bidi", - "tag" -]New value: +[ + "hidden_css", + "hidden_attribute", + "hidden_template", + "report_truncated", + "final_url_suppressed", + "detection_degraded", + "strip_incomplete", + "zero_width", + "bidi", + "tag" +]
- Changed
research1 field changed- changed
Output schema / $defs / Threat / properties / type / enumPrevious value: -[ - "hidden_css", - "report_truncated", - "final_url_suppressed", - "detection_degraded", - "strip_incomplete", - "zero_width", - "bidi", - "tag" -]New value: +[ + "hidden_css", + "hidden_attribute", + "hidden_template", + "report_truncated", + "final_url_suppressed", + "detection_degraded", + "strip_incomplete", + "zero_width", + "bidi", + "tag" +]
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
Scored across 4 tools
Each tool has a distinct purpose: status checks browser connectivity, search returns links only, read_url fetches a known URL, and research combines search and reading into one call. The descriptions explicitly call out the boundaries between search, read_url, and research, so an agent should not confuse them.
The names are readable and all lowercase, but they follow mixed conventions: read_url uses a verb_noun pattern, status is a noun, and search/research are bare verbs. There is no consistent structural pattern across the set, though the names themselves are not misleading.
Four tools is well-scoped for the server's purpose: setup diagnostics, link-only search, single-page reading, and combined research. Each tool fills a necessary role without redundancy or bloat.
The tool surface covers the full workflow: check browser readiness, discover URLs via search, read a specific page via read_url, and get synthesized passages via research. There are no obvious dead ends or missing operations for the stated web-research purpose.
Maintenance
Related MCP Connectors
Hosted Web Crawler MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
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.
Turn any public website into an MCP server for agents to search, read and navigate.
Related MCP Servers
- AlicenseAqualityAmaintenanceThis server enables LLMs to retrieve and process content from web pages, converting HTML to markdown for easier consumption.201228,373 PyPI91,381MIT
- AlicenseAqualityDmaintenanceA Model Context Protocol (MCP) server implementation that integrates with Firecrawl for advanced web scraping capabilities.26102,401 npm7,640MIT

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.16253,465,430 npm38,212Apache 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.19883Apache 2.0