Website Content
FreeNot checkedMCP server that fetches and converts website pages into clean markdown for agents, with sitemap-based discovery, disk caching, and polite crawling (robots.txt,
About
MCP server that fetches and converts website pages into clean markdown for agents, with sitemap-based discovery, disk caching, and polite crawling (robots.txt, rate limiting).
README
Give an AI agent a reliable, site-scoped view of a website: clean content now,
and deterministic evidence when it changes. It is a free, self-hosted
MCP server (not a general-purpose scraper), so
the agent only reads the website you configure, respects robots.txt, and can
show the hash and HTTP validators behind a result.
What you can do
- Monitor a site with evidence. Read selected competitor or market pages on
a schedule in a separate workflow, then compare content hashes,
ETags, prices and availability facts before alerting a human or agent. - Keep an agent current on your own site. Let a support, sales or content agent list pages, fetch the current Markdown, and answer from what is live rather than from a stale upload.
- Build a research foundation. Turn a site's sitemap and page content into a bounded, attributable input for briefs, audits, catalog analysis or change review without granting the agent arbitrary web-fetch access.
Unlike Firecrawl and generic scraping APIs, this project is site-scoped, robots-compliant, deterministic about change evidence, self-hosted and free. It is the content-access layer; scheduling, snapshots, diffs and alerts belong in the workflow you build around it.
Quick start: one minute to useful search
Use stdio for a local desktop agent. Add this to its MCP configuration:
{
"mcpServers": {
"website-content": {
"command": "npx",
"args": ["-y", "-p", "@sarutobi-sasuke/website-content-mcp", "website-content-stdio"],
"env": { "SITE_BASE_URL": "https://example.com" }
}
}
}
Then ask the agent to call content_refresh once. When it finishes, ask it to
call content_search for a topic. The cache begins empty, so search has no
pages to search until you refresh it or fetch pages individually.
Use HTTP when several approved remote agents need a shared endpoint. The
public HTTP transport is deliberately read-only: populate search with
STARTUP_REFRESH_LIMIT or a restricted operator endpoint rather than exposing
content_refresh anonymously.
A real agent flow
Prompt: “Summarise what is new on astraeus.ie.”
- The agent calls
content_list_pagesto discover the site structure.- It calls
content_get_pagefor the relevant current pages.- It summarises the returned Markdown and cites the source URLs, retaining
contentHash,ETagandfetchedAtfor the next comparison.
For a recurring change workflow, persist those deterministic fields outside the MCP server, re-read the same pages later, and only ask AI to classify or summarise a verified difference.
Offline regression tests
Run npm ci followed by npm run check for typecheck, lint and tests. The
saved synthetic HTML under tests/fixtures/ covers article formatting, relative
links, code blocks, Unicode spacing, client-rendered shells and malformed
structured data. A fake fetcher also checks response truncation, content hashes
and cache reuse. No live website or API key is needed. The existing live-site
integration test remains opt-in and is skipped by default.
Features
- Clean extraction: HTML -> markdown via Mozilla Readability + Turndown (real DOM parsing, never regex). Extraction runs once per page and is cached.
- Deterministic change evidence: each page includes a SHA-256 of the complete normalised markdown plus upstream
ETag/Last-Modifiedvalidators when available. - Extraction-quality evidence: results state whether content came from Readability, the cleaned body, plain text or metadata-only fallback, with actionable warnings for client-rendered shells.
- Commerce-aware metadata: bounded schema.org
Product/OfferJSON-LD is returned as structured product, SKU, GTIN, brand, price, currency and availability facts. - Discovery: page listing from
sitemap.xml, sitemaps advertised inrobots.txt, or a configured page list. - Disk cache: fetched pages cached with a configurable TTL and a size bound; reads prefer cache, then a conditional revalidation, then stale-on-error.
- Polite by default: respects
robots.txtdisallow rules, rate-limits to ~1 request/second, honoursRetry-After, and sendsIf-None-Match/If-Modified-Sinceso unchanged pages cost a304. - Scoped to one site: fetches are refused for any host outside the configured site.
- Public-network safe by default: allowed hostnames are resolved before every request and redirect hop; private, loopback, link-local and special-use destinations require an explicit operator opt-in.
- Two transports: Streamable HTTP and stdio.
What a page result looks like
content_get_page returns clean Markdown plus compact metadata that another
workflow can retain for comparison. Fields are omitted when the source does not
provide them.
{
"url": "https://shop.example/products/blue-widget",
"title": "Blue Widget",
"canonicalUrl": "https://shop.example/products/blue-widget",
"markdown": "# Blue Widget\n\nA useful blue widget.",
"contentHash": "8f3c...64-character-sha256...a91d",
"contentLength": 38,
"truncated": false,
"fetchedAt": "2026-08-16T16:00:00.000Z",
"fromCache": false,
"extractionMethod": "readability",
"extractionQuality": "full",
"sourceTrust": "untrusted-web-content",
"etag": "W/\"widget-v4\"",
"lastModified": "Sat, 16 Aug 2026 12:00:00 GMT",
"products": [{
"name": "Blue Widget",
"sku": "BW-1",
"brand": "Widget Co",
"offers": [{ "price": "19.99", "priceCurrency": "EUR", "availability": "https://schema.org/InStock" }]
}]
}
Tools
| Tool | Purpose |
|---|---|
content_list_pages |
List discoverable pages (sitemap → robots.txt sitemaps → configured list). Returns URL, title, last-modified when known. |
content_refresh |
Walk the discoverable pages and warm the cache so content_search has something to search. Skips robots-disallowed pages. Available on stdio; opt-in on HTTP. |
content_get_page |
Fetch a page URL, strip to clean markdown, return content + metadata, content hash and any schema.org Product/Offer facts. Supports max_length; forced refresh is disabled on public HTTP by default. |
content_search |
Keyword search over cached pages. Returns URL, score, snippet and the last completed refresh time. |
content_get_sitemap |
Return the raw sitemap structure (URLs + last-modified/priority/change-frequency when present). |
content_health |
Server status: configured site, allowed hosts, cache directory, cache size, last fetch time and last completed refresh time. |
On HTTP, up to 50 discoverable pages are warmed after startup and refreshed every
5 minutes by default. Each pass advances through the page list, so larger sites
are covered over successive passes. content_search reports last_refresh_at;
content_health reports lastRefreshAt. Set STARTUP_REFRESH_LIMIT=0 to disable
automatic HTTP refreshes. Stdio users can run content_refresh (about 1 request
per second) or fetch pages individually with content_get_page.
Requirements
- Node.js 22+
Install
MCP clients can run the stdio transport without cloning the repository:
{
"mcpServers": {
"website-content": {
"command": "npx",
"args": ["-y", "-p", "@sarutobi-sasuke/website-content-mcp", "website-content-stdio"],
"env": { "SITE_BASE_URL": "https://example.com" }
}
}
}
For source development:
git clone https://github.com/SarutobiSasuke8/website-content-mcp.git
cd website-content-mcp
npm install
npm run build
Configuration
Configuration is via environment variables (see .env.example):
| Variable | Required | Default | Description |
|---|---|---|---|
SITE_BASE_URL |
✅ | - | The site whose content is exposed. |
SITE_SITEMAP_URL |
<base>/sitemap.xml |
Sitemap location. | |
SITE_PAGES |
- | Comma-separated fallback page list (absolute or base-relative). | |
SITE_ALLOWED_HOSTS |
- | Extra hosts that may be fetched. The base URL's host is always allowed. | |
CACHE_DIR |
.cache |
Disk cache directory. | |
CACHE_TTL_SECONDS |
3600 |
Cache TTL (0 disables caching). | |
CACHE_MAX_ENTRIES |
500 |
Cache size bound, evicting oldest-first (0 = unbounded). | |
FETCH_TIMEOUT_MS |
10000 |
Per-request timeout. | |
FETCH_MIN_INTERVAL_MS |
1000 |
Minimum spacing between fetches (~1 req/sec). | |
FETCH_MAX_BYTES |
5000000 |
Hard cap on a single response body. | |
FETCH_MAX_RETRIES |
1 |
Retries on 429/503, honouring Retry-After. |
|
FETCH_ALLOW_PRIVATE_NETWORK |
false |
Permit configured hosts to resolve to private/special-use IPs. Use only for deliberate internal-site deployments. | |
USER_AGENT |
website-content-mcp/0.4.0 … |
Outbound User-Agent. | |
HOST |
127.0.0.1 |
HTTP bind host. | |
PORT |
3215 |
HTTP bind port. | |
HTTP_ALLOW_REFRESH |
false |
Expose content_refresh and permit forced origin revalidation over HTTP. Enable only behind an authenticated or restricted reverse proxy. Stdio always permits refresh. |
|
HTTP_RATE_LIMIT_WINDOW_MS |
60000 |
Per-IP request window for /mcp. |
|
HTTP_RATE_LIMIT_MAX_REQUESTS |
60 |
Maximum /mcp requests per IP in each window; excess requests receive 429 and Retry-After. |
|
HTTP_TRUST_PROXY |
empty | Comma-separated trusted proxy IPs, CIDRs or aliases such as loopback. X-Forwarded-For is ignored unless a matching proxy is configured. |
|
STARTUP_REFRESH_LIMIT |
50 |
Pages refreshed on startup and each scheduled HTTP pass. Set to 0 to disable automatic refresh. |
|
HTTP_REFRESH_INTERVAL_SECONDS |
300 |
Seconds between scheduled HTTP passes. Set to 0 for startup-only warming. |
Run
Streamable HTTP (default transport):
# Using a .env file (recommended):
cp .env.example .env
npm start
# Or passing environment variables inline:
# POSIX / Bash:
SITE_BASE_URL=https://example.com npm start
# PowerShell:
$env:SITE_BASE_URL="https://example.com"; npm start
# -> website-content-mcp listening on http://127.0.0.1:3215/mcp (site: https://example.com/)
The MCP endpoint is POST /mcp; a plain health probe is available at GET /healthz.
Streamable HTTP is deliberately public read-only by default. It omits
content_refresh, refuses content_get_page(refresh: true), and redacts the
local cache path from MCP health output. Expired pages are revalidated on
content_get_page reads. The bounded background refresh keeps search current
without exposing a public cache-warming tool. content_search describes the
actual HTTP refresh mode and returns the last completed refresh time.
Inbound /mcp requests are limited per client IP. The server uses the socket IP
unless HTTP_TRUST_PROXY explicitly lists the reverse proxy, so untrusted
X-Forwarded-For values cannot bypass the quota. Behind local nginx, set
HTTP_TRUST_PROXY=loopback; use the proxy's specific address or CIDR for other
topologies.
For a loopback Node process behind nginx/systemd, see docs/production-deployment.md.
stdio (for local MCP clients):
# POSIX / Bash:
SITE_BASE_URL=https://example.com npm run start:stdio
# PowerShell:
$env:SITE_BASE_URL="https://example.com"; npm run start:stdio
Example MCP client entry from a local source checkout (stdio):
{
"mcpServers": {
"website-content": {
"command": "node",
"args": ["/path/to/website-content-mcp/dist/src/stdio.js"],
"env": { "SITE_BASE_URL": "https://example.com" }
}
}
}
Troubleshooting
Startup refresh fails or logs DNS errors (ENOTFOUND)
When starting the HTTP transport with STARTUP_REFRESH_LIMIT enabled (default 50), the server performs an initial cache-warming pass from the site sitemap. If SITE_BASE_URL cannot be resolved (for example when offline or targeting an unresolvable hostname), a warning is logged:
Startup cache refresh failed: getaddrinfo ENOTFOUND ...
The server continues running and responds to HTTP and MCP requests. To disable automatic startup warming, set STARTUP_REFRESH_LIMIT=0 in .env.
Missing SITE_BASE_URL error on startup
SITE_BASE_URL is the only mandatory configuration setting. If omitted, startup fails with a validation error:
[ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": [ "SITE_BASE_URL" ], "message": "Required" } ]
Create a .env file from .env.example and configure SITE_BASE_URL before launching.
Keyword search returns empty results
content_search queries only pages present in the local disk cache. On a fresh server instance, the cache begins empty:
- On stdio: run
content_refreshto warm the cache, or fetch individual target pages usingcontent_get_page. - On HTTP: allow startup cache warming to complete (
STARTUP_REFRESH_LIMIT), or fetch pages individually withcontent_get_page.
Private IP address or loopback destination refused
Outbound network fetches to private, loopback (127.0.0.1, localhost), link-local, or cloud metadata IP ranges are blocked by default to prevent Server-Side Request Forgery (SSRF). When intentionally pointing the server at an internal service or local test environment, set FETCH_ALLOW_PRIVATE_NETWORK=true in .env.
Pages refused by robots.txt
If content_get_page or content_refresh reports RobotsDisallowedError, the requested URL is restricted by the remote site's robots.txt. The server strictly enforces origin robots policies and does not bypass crawl restrictions.
Development
npm run dev # HTTP transport with --watch
npm run dev:stdio # stdio transport
npm run check # typecheck + lint + build + test
Testing
- Unit tests cover HTML→markdown conversion, the disk cache (TTL and eviction), robots parsing, sitemap parsing, the fetcher (size cap, conditional headers,
Retry-Afterretries, rate limiting) and the content service (host scoping, robots enforcement, truncation, cache warming, 304 revalidation, stale fallback). All offline, against a local fixture and a stubbed fetch. - A live integration test runs against
https://example.com, exercisinghealth,list_pages,get_page, andsearch.
npm test
Live checks are opt-in. To validate real deployment targets, set
RUN_LIVE_TESTS=1 and provide a comma-separated LIVE_SITE_URLS list of
sites you own or operate before running npm test, for example:
RUN_LIVE_TESTS=1 LIVE_SITE_URLS=https://astraeus.ie npm test
Without LIVE_SITE_URLS, the live check falls back to https://example.com.
Dogfood monitoring
The source repository includes a bounded snapshot-and-diff proof harness. It stores hashes, validators, extraction quality and warnings without retaining page bodies:
npm run dogfood
npm run dogfood
The first run establishes baselines and the second reports unchanged or changed pages. See docs/dogfood-monitoring.md. Scheduling, production retention and alert routing remain outside the core.
The published CLI can run the same bounded proof without a source checkout:
npx -y -p @sarutobi-sasuke/website-content-mcp website-content-dogfood
Security & etiquette
- Binds to
127.0.0.1by default. - No authentication and no API keys; intended for public content only.
- Scoped to the configured site: a URL on any other host is refused, so the
server cannot be used as a general-purpose fetcher. Widen deliberately with
SITE_ALLOWED_HOSTS. - Redirects are followed manually and every destination is checked against the same host allowlist before a network request is made.
- DNS results are checked before each request and redirect hop; private,
loopback, link-local and special-use destinations are refused unless
FETCH_ALLOW_PRIVATE_NETWORK=trueis explicitly set by the operator. - Returned page text is labelled
untrusted-web-content; clients must treat it as source data, never as instructions that override their own policy. - Streamable HTTP defaults to a read-only tool surface. Keep
HTTP_ALLOW_REFRESH=falsefor anonymous deployments. - Respects
robots.txt, fetched and enforced per origin; disallowed paths are refused. - Rate-limited to ~1 request/second against the target site.
- Response bodies are capped at
FETCH_MAX_BYTESand the cache atCACHE_MAX_ENTRIES. - Never logs full page bodies (only URLs, status codes, and sizes).
What this server is not
This project is the content-access foundation for monitoring workflows; it is not itself a scheduler or alerting service. Competitor monitoring additionally needs durable snapshots, deterministic diffs, a scheduler, notifications and an evidence-retention policy. AI can classify and summarise verified changes, but should not replace the underlying hashes, fields and source records.
License
Installing Website Content
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/SarutobiSasuke8/website-content-mcpFAQ
Is Website Content MCP free?
Yes, Website Content MCP is free — one-click install via Unyly at no cost.
Does Website Content need an API key?
No, Website Content runs without API keys or environment variables.
Is Website Content hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Website Content in Claude Desktop, Claude Code or Cursor?
Open Website Content on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
GitHub
PRs, issues, code search, CI status
by GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Unyly Deploy
Deploy sites and MCP servers to Unyly straight from your AI agent
by UnylyAmap Maps Mcp Server
MCP server for using the AMap Maps API
by duxiaohuiPayMCP
Lightweight payments layer for MCP servers: turn tools into paid endpoints with a two-line decorator. [PyPI](https://pypi.org/project/paymcp/) · [npm](https://w
by blustAIServer For Power BI
Enables exploring Microsoft Fabric / Power BI workspaces and semantic models, and executing ad-hoc DAX queries.
by Mitsubishi-FusoGpt Image 2 Mcp
RunAPI GPT Image 2 MCP server for image generation: create tasks, poll results, and check pricing across 1 model from Claude Code, Codex, Cursor, and VS Code.
by runapi-aiTemplate MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
by mcpdotdirectSupabase
Database, auth and storage
by SupabaseCompare Website Content with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All development MCPs
