fetch-mcp
by cmaurer
README.md
# fetch-mcp
An MCP server that fetches a URL, fully renders it in a headless Chromium browser (so JavaScript-heavy single-page apps built with Angular, React, Vue, etc. actually populate their DOM), and returns clean, scrapeable content. Plain-HTTP "fetch" MCP servers return pre-render HTML — often just an empty `<div id="root">` shell for a SPA. This server renders first, then scrapes.
## Install and build
```bash
npm install
npm run build
```
## Run a standalone sanity check
```bash
npm run build
npx @modelcontextprotocol/inspector node dist/index.js
```
This opens the MCP Inspector, where you can call `fetch_page` interactively against a real URL.
## Claude Desktop configuration
Add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"fetch-mcp": {
"command": "node",
"args": ["/absolute/path/to/fetch-mcp/dist/index.js"]
}
}
}
```
## Claude Code configuration
Add to `.mcp.json`:
```json
{
"mcpServers": {
"fetch-mcp": {
"command": "node",
"args": ["/absolute/path/to/fetch-mcp/dist/index.js"]
}
}
}
```
## `fetch_page` tool reference
**Input:**
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `url` | string (required) | — | URL to fetch and render |
| `waitForSelector` | string (optional) | — | CSS selector to wait for before extracting content |
| `timeoutMs` | number | `30000` | Total time budget in milliseconds (max `60000`) |
| `rawHtml` | boolean | `false` | Return raw rendered HTML instead of Markdown |
| `maxContentLength` | number | `50000` | Maximum characters of returned content |
**Output:**
| Field | Type | Description |
| --- | --- | --- |
| `requestedUrl` | string | The URL that was requested |
| `finalUrl` | string | The URL after any redirects |
| `httpStatus` | number | HTTP status of the final response |
| `title` | string \| null | Page title |
| `extractionMethod` | `"readability"` \| `"body-fallback"` \| `"raw-html"` | How content was derived |
| `waitCondition` | `"selector"` \| `"networkidle"` | Which wait strategy was used |
| `waitTimedOut` | boolean | Whether the wait phase timed out (content is still returned, best-effort) |
| `truncated` | boolean | Whether `content` was cut short at `maxContentLength` |
| `content` | string | Extracted Markdown, or raw HTML if `rawHtml: true` |
**Example call:**
```json
{
"url": "https://example.com/dashboard",
"waitForSelector": "#app-loaded",
"timeoutMs": 20000
}
```
## Security model
Every request URL — the initial navigation, and every subresource, redirect, or XHR/fetch the page's own JavaScript issues — is checked against a DNS-based SSRF guard before it's allowed through. A hostname is resolved via `dns.lookup` for *all* its addresses, and the request is blocked unless every resolved address falls in the `unicast` (i.e. public, globally-routable) range — this rejects loopback, RFC1918 private ranges, link-local addresses (including the `169.254.169.254` cloud metadata address), carrier-grade NAT, and other non-routable ranges.
**Known limitation:** this guard has a DNS-rebinding TOCTOU gap — the DNS check and the actual TCP connection are not atomically pinned to the same resolved IP, so a hostname could theoretically re-resolve to an unsafe address between the check and the connection. Closing this fully would require pinning the connection to the checked IP, which breaks TLS/SNI for HTTPS targets, so it isn't done for v1.
## Known limitations
- If Readability can't find article-like content (common for dashboard-style SPA shells), the full page body is converted to Markdown instead — this fallback is best-effort and may include navigation chrome or other non-content markup.
- `content` is truncated at `maxContentLength` characters; a `[content truncated]` marker and `truncated: true` are included when this happens.
- At most 3 pages render concurrently; additional concurrent `fetch_page` calls queue rather than fail.
## Development
```bash
npm run dev # run directly with tsx, no build step
npm run typecheck # tsc --noEmit
npm test # vitest
npm run build # tsc, then chmod +x dist/index.js
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues