webshot-mcp
by Brandon-35
README.md
# webshot-mcp
**Give your agent eyes — without giving it a browser or your network.**
An MCP server with two tools, `screenshot` and `page_text`, that connects to a
Chrome you already have running (over CDP) and captures full pages or single
elements. No browser is ever launched or downloaded; private-network targets
are blocked by an SSRF-aware URL policy.
## Why this exists
Two lessons from running visual check → fix loops with coding agents:
1. **Launching a browser per screenshot is the slow, flaky way.** A persistent
headless Chromium (Docker sidecar, CI service, your desktop Chrome with
`--remote-debugging-port`) is warm, shared, and reliable. The missing piece
is a disciplined client: connect over CDP, open a fresh context per shot,
close only what you opened, never kill the shared browser. That discipline
is this repo.
2. **A screenshot tool inside your network is an SSRF proxy with a camera.**
An agent that can be talked into "screenshot `http://169.254.169.254/…`"
or `http://redis.internal:6379/` will happily photograph your cloud
metadata endpoint. So the URL policy blocks loopback, RFC-1918,
link-local, CGNAT, and v6-local targets — including hostnames that
*resolve* to them — unless you allowlist the hostname explicitly. Local
dev servers are the legitimate exception, and you name them one by one.
## Quick start
```bash
npm install
npm test # URL-policy test suite — no browser needed
npm run demo # policy walkthrough + a real screenshot if a CDP Chrome is reachable
```
Get a CDP-enabled Chrome (any one of):
```bash
# your installed Chrome, headless
chrome --headless --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1 about:blank
# or a Docker sidecar you keep running
docker run -d -p 127.0.0.1:9222:9222 chromedp/headless-shell
```
Wire into Claude Code:
```bash
claude mcp add webshot \
-e WEBSHOT_CDP_URL=http://127.0.0.1:9222 \
-e WEBSHOT_ALLOW_HOSTS=localhost,127.0.0.1 \
-- npx tsx src/server.ts
```
Then ask the agent things like *"screenshot the hero section of
localhost:5173 — just `.hero` — and tell me what's off"*: `selector` crops to
one element, which keeps the agent's attention (and your tokens) on the part
under discussion instead of a 4,000-px page.
## Tools
| Tool | Contract |
|---|---|
| `screenshot` | `url`, optional `selector` (element crop), `fullPage`, viewport `width`/`height`, `settleMs`. Returns the PNG as MCP image content + `{finalUrl, title, httpStatus}`. Refuses to capture HTTP ≥ 400 — a screenshot of an error page silently poisons whatever the agent does next. |
| `page_text` | Body `innerText` — the cheap check when the agent needs to *read*, not *see*. Same URL policy. |
## Design notes
- **Fresh context per shot, shared browser for life.** Contexts are cheap and
isolated (cookies, cache, viewport); the browser is expensive and shared.
`browser.close()` on a CDP connection disconnects without killing the
remote browser — the server can restart all day without touching Chrome.
- **DNS is checked, rebinding is documented.** Non-IP hostnames are resolved
and every returned address re-verified against the private ranges. A
malicious DNS server that answers differently on the browser's own second
resolution (classic rebinding) is out of scope for a screenshot tool —
noted here rather than hand-waved.
- **`deviceScaleFactor: 2`** — retina captures, because agents (and humans)
misread blurry text in screenshots more often than you'd think.
- **Policy is fail-closed and testable.** `evaluateUrl` is a pure function
with the entire threat table in its test file; the server never calls the
browser before the policy says yes.
## Stack
TypeScript · `@modelcontextprotocol/sdk` (stdio) · playwright-core (CDP
client only — no bundled browsers) · zod · vitest.
## License
MIT © Brandon Ta
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues