Skip to main content
Glama
Brandon-35
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