Refract
MCP server for rendering responsive screenshots of URLs at multiple viewports. Enables agents to capture screenshots and detect visual issues like overflow, clipped elements, and missing alt text.
README
<p align="center"> <img src="docs/icon.svg" width="128" alt="Refract" /> </p>
Refract
Agent-first responsive screenshots. Give a coding agent (or yourself) one primitive: render this URL at N viewports, return the images, and tell me what visually broke. Built on Playwright, shipped as an MCP server + CLI + Node library.


🚧 Pre-release. The engine, CLI, and MCP tool work; npm packages aren't published yet, so install from source (
pnpm install && pnpm build).
Install
pnpm add @getrefractjs/core # library
pnpm add -g @getrefractjs/cli # CLI
# one-time browser download (deferred until first render):
pnpm exec playwright install chromium
CLI quickstart
refract https://example.com --viewports mobile,tablet,desktop --out ./shots
Outputs ./shots/{preset}.png, one per viewport, and prints findings under each.
Flags: --viewports, --out, --selector, --wait-for, --wait-for-function,
--wait-for-network-idle-ms, --freeze, --inject-css, --annotate, --dpr,
--concurrency, --storage-state, --engine.
Use --inject-css "#clock,.ad{visibility:hidden}" to hide dynamic or flaky elements
before capture — handy for clean, stable diffs (and the hidden elements stop showing
up as findings too).
Pass --annotate to draw outline boxes over the findings onto the screenshot (errors
red, warnings amber) — so the image itself shows what broke. Full-page only.
When the page is ready only after an app-specific signal, gate the capture with
--wait-for-function "window.__ready === true"; for slow pages, raise the
network-idle cap with --wait-for-network-idle-ms 30000 (it stays best-effort).
Viewports that render identically (e.g. iphone-17-pro and iphone-16-pro are both
402×874 @3) are rendered once and bundled into a single result, with the extra
device names listed as aliases — so "render every iPhone" doesn't shoot the same
pixels three times.
Library quickstart
import { render } from '@getrefractjs/core';
const shots = await render({ url: 'http://localhost:3000' });
for (const shot of shots) {
console.log(shot.preset, shot.savedPath);
for (const f of shot.findings) {
console.log(` [${f.severity}] ${f.type} ${f.detail}`);
}
}
MCP config block
{
"mcpServers": {
"refract": {
"command": "npx",
"args": ["-y", "@getrefractjs/mcp"]
}
}
}
The server exposes two tools, each with a description that tells an agent exactly when and how to call it — no docs required:
render_responsive— one call returns a text manifest of absolute saved paths, structured findings as JSON (see below), and a downscaled preview image per viewport (≤800px wide, so it won't blow the agent's context window); full-resolution PNGs are written to disk.diff_responsive— visual regression in-band: renders, compares against a saved baseline, and returns a per-viewport status (unchanged/changed/…) with the % of pixels changed, a downscaled diff image for each changed viewport, and areport.htmlpath. First run withupdate: trueto save the baseline, then compare after a change.
Load failures come back as teaching errors the agent can act on.
Working in this repo? A committed .mcp.json registers the local server
(node packages/mcp/dist/index.js) — run pnpm build first and restart your
client so it picks the tool up.
Authenticated pages
Most real apps live behind a login. Refract renders logged-in pages by reusing a Playwright storage-state file (cookies + localStorage) — the standard format, nothing Refract-specific. Generate one once by logging in:
npx playwright codegen --save-storage=auth.json https://your-app.com/login
# log in in the window that opens, then close it
Then point any surface at it:
refract https://your-app.com/dashboard --storage-state ./auth.json
await render({ url: 'https://your-app.com/dashboard', storageState: './auth.json' });
render_responsive({ url: 'https://your-app.com/dashboard', storageState: './auth.json' })
Already have Playwright auth states from your e2e suite (e.g. e2e/.auth/*.json)?
Pass one straight through — no regeneration needed. The file's cookies are sent to
the URL, so don't pair an auth file from one origin with an untrusted URL.
Findings
Every render returns structured findings per viewport alongside the screenshots — agents act on these instead of eyeballing pixels:
{ preset: "mobile", findings: [
{ type: "horizontal_overflow", severity: "error", detail: "scrollWidth=480 viewport=402",
selector: "div.card", rect: { x: 0, y: 120, width: 480, height: 90 } },
{ type: "tap_target_small", severity: "warn", selector: "button#tiny-btn", size: "28x24",
rect: { x: 16, y: 540, width: 28, height: 24 } },
]}
Most findings carry a selector and a rect (the culprit's box in document pixels, so you
can zoom straight to what broke); horizontal_overflow names the element that causes it.
| type | severity | fires when |
|---|---|---|
horizontal_overflow |
error | the page scrolls wider than the viewport (names the culprit element) |
element_clipped |
warn | an element sticks out past the viewport edge |
text_overflow |
warn | text is hard-clipped with no ellipsis (scrollWidth > clientWidth; intentional text-overflow: ellipsis truncation is ignored) |
tap_target_small |
warn | an interactive element is under 44×44 (mobile viewports) |
text_too_small |
warn | body text under 12px on a mobile viewport (short labels/badges are ignored) |
viewport_meta_missing |
error | the page has no <meta name="viewport">, so phones render it at desktop width and scale down |
image_no_alt |
warn | an <img> is missing its alt attribute |
The CLI prints them under each shot; the MCP tool returns them as JSON keyed by preset.
Token footprint
Findings-first responses and downscaled previews aren't just nice-to-haves — they
keep the agent's context window alive. Measured on the demo-site (3 viewports), one
render_responsive response costs roughly:
| response shape | Claude | GPT-4o | Gemini |
|---|---|---|---|
| findings only (no images) | ~620 | ~620 | ~620 |
| downscaled previews (default) | ~4.2k | ~4.0k | ~3.7k |
| full-resolution images (naive) | ~17.7k | ~4.3k | ~8.9k |
So Refract's default is ~76% smaller than dumping full-res screenshots on Claude,
and the structured findings alone are a few hundred tokens. Re-run anytime with
pnpm bench; details + per-model formulas in benchmarks/RESULTS.md.
Visual diff
refract diff catches unintended visual change across viewports — the "did my
CSS tweak break another page" check. Baselines are just PNGs in a folder, so it's
git-agnostic and trivial to wire into CI.
# 1. Save a baseline (once, or to accept new changes)
refract diff https://example.com --update # writes ./refract-baseline/{preset}.png
# 2. Later, compare a fresh render against it
refract diff https://example.com # exits 1 if anything changed
Each viewport is compared with pixelmatch.
Output per preset is unchanged, changed (with the % of pixels and a
{preset}.diff.png highlighting them), size_changed, or no_baseline. Alongside the
pixel diff, each preset reports a findings delta — which findings were fixed (gone
since the baseline) or regressed (new) — so you can confirm a fix landed without
introducing a new responsive issue. (The --update snapshot stores the findings too, in
findings.json; an older baseline without one just omits the delta.) A
report.html lands next to the shots with a baseline │ current │ diff grid.
The command exits non-zero when anything changed, so CI fails on a regression;
re-run with --update to accept the new look as the baseline. Flags: --baseline <dir>, --update, --threshold <0-1>, plus all the render flags above (--freeze
is recommended for deterministic diffs).
Use in CI
Commit your baselines (./refract-baseline/) and run refract diff against a
deployed preview on every PR — it exits non-zero on a regression, failing the job.
A ready-to-copy GitHub Actions workflow is in
examples/github-actions/visual-diff.yml:
it installs the CLI + Chromium, runs the diff, and uploads report.html + the diff
PNGs as an artifact when something changed. Refresh baselines by re-running with
--update and committing. (Agents working in an MCP client can do the same loop via
the diff_responsive tool.)
What this is not
- ❌ A browser extension or live-preview app (that's Responsively's job).
- ❌ A general-purpose browser-control MCP (that's
playwright-mcp's job). It renders URLs at viewports; it cannot click, type, or perform a login flow. It can reuse a saved auth state (--storage-state) to render a logged-in page, but it won't log in for you. - ❌ A visual-regression engine reinvented from scratch (it wraps
pixelmatch). - ❌ A real-device cloud. Playwright emulates viewport, DPR, UA, and touch, and
--engine webkitruns the real WebKit engine (close to iOS Safari) — but it's still desktop WebKit, not an actual iOS device or GPU. It does not replace BrowserStack.
Cross-browser
Renders on Chromium by default; pass --engine webkit (or engine: "webkit" in the
library / MCP) to render with the real Safari/WebKit engine — the closest local proxy
to iOS Safari, where a lot of responsive bugs actually show up.
npx playwright install webkit # one-time, ~70MB
refract https://example.com --engine webkit
It works everywhere a render does (CLI, library, MCP render_responsive / diff_responsive).
For visual diffs, keep a separate baseline dir per engine (--baseline ./baseline-webkit)
since engines render slightly differently. Firefox isn't supported yet (it can't emulate
isMobile and ignores DPR) — open an issue if you need it.
Roadmap
Where Refract is headed — deeper findings, shareable reports, more device/engine coverage — is in docs/ROADMAP.md. It's directional, not a promise; real usage steers it.
Security
Refract loads any URL you give it — including file:// (local files) and
internal/private hosts (e.g. cloud metadata endpoints) — and returns the rendered
pixels. Treat it like curl: don't point it, or an agent driving the MCP server,
at untrusted or sensitive URLs. There is no URL allow/deny list by default. See
SECURITY.md to report a vulnerability.
License
MIT
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。