rpc
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
rpc-agent
Screenshot a URL at real device viewports, optionally framed in a device bezel, so an AI coding agent can look at the UI it just built. It's a CLI and an MCP server in one package, with Playwright (Chromium) doing the capture.
The device catalog (49 phones, tablets, laptops and monitors) and the SVG
bezels come from the Responsive Point Check
Chrome extension. See NOTICE for where the artwork comes from.
Install
Requires Node 20.12 or newer. Run it without installing:
npx rpc-agent install # first run: download headless Chromium (~210 MB on disk)
npx rpc-agent shot localhost:3000 -d iphone-15,desktop-1440 --bezelsOr install it globally to get the rpc command:
npm i -g rpc-agent
rpc install # first run: download Chromium
rpc doctor # checks Chromium is installed and launches
rpc shot https://example.com --bezelsChromium is not downloaded when the package installs. rpc install (or
npx rpc-agent install) fetches it with rpc's own Playwright version, so
the browser always matches the one rpc launches. It installs only
Chromium's headless shell (plus Playwright's small ffmpeg), not the full
Chrome for Testing build, since captures never open a visible window
(rpc install --headed adds it for rpc login). Until you run it, every
command says Chromium is missing and prints that command, instead of
failing with Playwright's own error. Browsers go to Playwright's usual
cache, so other Playwright projects on the same version share them.
From source
git clone https://github.com/akintola4/rpc-agent.git
cd rpc-agent
pnpm install && pnpm build
node dist/cli.js install
pnpm link --global # optional: puts this build's `rpc` on your PATHWithout the link, run node /absolute/path/to/rpc-agent/dist/cli.js where
these docs say rpc, or pnpm rpc from inside the repo.
Related MCP server: site-shot-mcp
CLI

rpc # interactive: pick a URL, devices and options
rpc shot <url> [options]
rpc check <url> [options] # pre-launch audit, see Ship-check below
rpc record <url> [options] # a few seconds of video of the page or one element
rpc login <url> [--form] # sign in once; later shots of that origin use the session
rpc sessions [list|clear [origin]]
rpc devices [--json]
rpc doctor [--json] # is Chromium installed, and does it launch?
rpc install [--headed] # download Chromium with rpc's own Playwright version
rpc mcp
rpc help <command> # options and examples for one commandRun rpc with no arguments in a terminal and it asks for the URL
(defaulting to a dev server on localhost:3000, 5173, 4321 or 8080 if one is
running), lets you tick devices from a grouped list (iPhone 15 Pro, iPad Air
11" and Desktop 1440 are preselected) and pick bezels, full page, dark mode
or reduced motion. It shows a summary, asks before capturing, and prints the
equivalent rpc shot command at the end so you can re-run it.
rpc shot in a terminal shows each device's progress, a results table and
the files it wrote:
Option | Meaning |
| Comma-separated device ids or aliases. Default |
| Also write a copy framed in the device bezel |
| Capture the full scroll height, however tall (up to |
| With |
| Maximum slice width. Default 768 |
| Full-page cap in CSS px. Default 60000 |
| Emulate |
| Jump animations to their end state before capturing. See Animation-aware capture |
| With |
| Output directory. Default |
| Wait for a visible element before capturing |
| Extra settle delay after load |
|
|
| Navigation timeout. Default 30000 |
| Viewport and DPR only: no mobile user agent or touch |
| Print the result as JSON |
| Open the output folder when done ( |
|
|
|
|
| Load a Playwright storageState JSON |
| Don't use the session saved by |
| Also send headers and loaded cookies to this origin, repeatable |
The URL can be a full URL, a bare local host (localhost:3000 becomes
http://), a bare domain (example.com becomes https://) or a path to a
local .html file.
rpc shot localhost:3000 -d iphone-15,ipad,desktop-1440 --bezels
rpc shot https://example.com/pricing --full-page --slices --color-scheme dark
rpc shot localhost:5173 -d pixel --wait-for '[data-ready]' --jsonOutput files are named <host>[-<path>]-<device>.png, with -framed.png
for bezel copies and -slice-NN.jpg for slices, for example
.rpc-shots/localhost-3000-iphone-15.png. Re-running overwrites them, so the
paths stay the same between runs.
Aliases: iphone, mobile, phone (iphone-15-pro), android, pixel
(pixel-8), galaxy (galaxy-s24), ipad, tablet (ipad-air-11), macbook
(macbook-air-13), laptop (laptop-1280), desktop (desktop-1440), imac
(imac-24). Run rpc devices for the full list.
What a capture does
Each device gets a fresh browser context with its CSS viewport, device pixel ratio and, for phones and tablets, a mobile user agent plus touch. The page loads, then rpc waits for network idle (up to 10 s, so dev servers with HMR sockets don't hang it), web fonts and pending images. CSS animations, transitions and Web Animations are finished before the shot.
With --full-page, rpc first scrolls through the page one viewport at a time
so lazy images and IntersectionObserver content load, then scrolls back to
the top. A page that fits in one Chromium capture (16,384 device px) is taken
in one pass. A taller one is taken as clipped segments of about 8,000 device
px and stitched into one PNG in Node, with no height limit apart from
--max-height (60,000 CSS px by default; taller pages are cut there and
marked clipped). Both paths render the page as if the viewport were the
whole document, so sticky headers and fixed elements appear once, at the top,
and 100vh sections keep their real height.
Bezels frame the above-the-fold viewport. With --bezels --full-page you get
both: the full page raw, and the first screen framed.
Animation-aware capture
A stitched shot of a page with scroll animations used to come out wrong in three ways: pinned scroll sections left tall blank bands, text was caught halfway through its reveal, and carousels froze on whatever slide was up.
Settle
Before each capture, and at every step of the full-page warm-up
scroll, rpc waits until the visible part of the page stops moving: no
style or class change on a visible element for 150 ms (120 ms per
warm-up step), and, at the warm-up steps, no finite animation running on
one. The screenshot itself ends finite CSS and Web Animations (Playwright's
animations: 'disabled'), so the wait just before it only watches for
JS-driven changes, the kind a GSAP tween makes. It gives up after 2 s
(800 ms per step, 8 s for the whole warm-up), so a page that never stops
can't hang it. Anything still changing or animating when a step runs out
of time is treated as a loop (a JS marquee, a ticker, a long CSS
animation), and later steps stop waiting for it. A static page only pays the quiet period. The result is on each shot as
motion.settle: settled, ms, and the selectors still active.
Pinned sections
GSAP ScrollTrigger pins a section by wrapping it in a
.pin-spacer with padding below it, then fixing the section in place while
you scroll through that padding. A full-page capture renders the page at
scroll 0, where the section sits at the top of its spacer and the padding
is empty, so the shot gets a blank band as tall as the pin's scroll
distance. With --full-page, rpc collapses each pin it finds. The spacer
loses its padding, the section goes back into normal flow, and the shot
shows it once with the rest of the page right after it. A sticky section
that is the only child of a much taller parent gets the same treatment. The
rules go in an injected stylesheet as !important, so when the library
rewrites its inline styles during the capture, they still win. Each
collapsed pin is listed in motion.unpinned with the CSS px removed, and
the stitched height is measured after the collapse. --no-unpin turns it
off.
| default (after) |
Finish animations
--finish-animations is for JS-driven motion, which the screenshot can't
end on its own. It calls .finish() on every finite Web Animation (CSS
animations and transitions too, so layout measured before the capture is
the end state). If window.gsap exists,
tweens and timelines on its global timeline that don't repeat forever go to
progress(1). If window.ScrollTrigger exists, triggers that aren't
scrubbed or pinned get their animation set to progress(1). Scrubbed and
pinned ones follow the scroll position, so they keep their scroll-0 state.
Each library step runs in its own try/catch and does nothing when the
library isn't there. Infinite loops are left alone. The counts are in
motion.finished.
Reduced motion
--reduced-motion still emulates
prefers-reduced-motion: reduce. On a site that respects it, that's the
cleanest fix, because the animations never run.
Warnings
Each shot gets a warnings list in the JSON, and the terminal
prints them under the device:
ok desktop-1440 (1440x900 @1x)
.rpc-shots/localhost-3000-desktop-1440.png 1440x900
warn: 3 elements were still animating when captured: div.reveal:nth-of-type(1), div.reveal:nth-of-type(2), div.reveal:nth-of-type(3)
warn: carousel: use `rpc record` for motion: #carouselid | When |
| Elements were still changing (style or class writes, as a JS tween makes) when the settle ran out of time: "N elements were still animating when captured". CSS and Web Animations don't count here, because the screenshot ends them |
| Full page only. A pin was left in place ( |
| Content still at opacity 0, or offset by 24px or more, that looks like a reveal (see Motion) |
| A carousel or marquee is in the shot: "carousel: use |
Each warning has a fix naming the flag to try. Warnings never change the
exit code.
Terminal, pipes and CI
The banner, spinners, prompts and boxes only appear when stdout is an
interactive terminal. When output goes to a pipe or a file, when CI is
set, when TERM=dumb, and always with --json, rpc prints the same plain
lines as 0.2 (ok …, x … plus hint:), so agents and scripts that read
them see no change. NO_COLOR turns colour off; FORCE_COLOR=1 adds colour
to the plain lines. rpc mcp never prints anything but MCP messages.
When a capture fails
Each device fails on its own, with an error, a code and usually a hint.
In a terminal, devices that fail the same way share one hint:
Plain output (pipes, agents, CI):
x iphone-15 (393x852 @3x): Could not resolve host example.dev (net::ERR_NAME_NOT_RESOLVED)
hint: Check the URL for typos. If the host works in a normal browser, this process may be
blocked by a sandbox or network allowlist (e.g. the Claude Code sandbox: add the domain to
the allowed hosts, or run outside the sandbox). ...Covered: DNS failures, refused connections (on localhost: "nothing is
listening on port N"), proxy, tunnel and offline errors, TLS errors (https
against a plain-http dev server), missing files, redirect loops, navigation
and --wait-for timeouts.
Authenticated pages
Dashboards, billing and settings pages sit behind a login. rpc can carry a session so you don't need a hand-written "sign in, then screenshot" script.
Sign in once with rpc login
rpc login localhost:3000/sign-in
rpc shot localhost:3000/app/settings/billing -d iphone-15,desktop-1440 --bezelsrpc login opens a browser window at the URL. Sign in as you normally
would (SSO and 2FA work, it's a real browser), then press Enter in the
terminal or close the window. rpc saves the browser's storage state
(cookies and localStorage) to
~/.config/rpc-agent/sessions/<origin>.json, for example
http-localhost-3000.json. From then on, rpc shot on that origin uses it
automatically, and the output says so:
auth: session http://localhost:3000 (saved 2026-10-29 22:55)
ok iphone-15 (393x852 @3x)The window needs full Chromium, which rpc install doesn't download (it
fetches only the headless shell). Run rpc install --headed once, or have
Google Chrome installed; rpc falls back to it.
--save <file> writes the storage state to a file of your choice instead;
use it with rpc shot --storage-state <file>.
rpc sessions # origins, cookie counts, saved and expiry times
rpc sessions clear localhost:3000 # forget one origin
rpc sessions clear # forget all
rpc shot localhost:3000/app --no-session # capture as a signed-out visitorWhen the session is missing or expired
If the page redirects to a login URL (a path segment like /login,
/sign-in, /signin or /auth, different from the one you asked for),
rpc doesn't screenshot the sign-in form. The device fails with code
AUTH_REQUIRED:
x iphone-15 (393x852 @3x): Redirected to a login page (http://localhost:3000/sign-in) instead of http://localhost:3000/app/settings/billing
hint: Session missing or expired: run `rpc login http://localhost:3000`A saved session whose cookies have all passed their expiry isn't loaded, and gives the same hint.
CI and agents: rpc login --form
Without a person at the keyboard, --form signs in headlessly through a
standard email + password form:
export RPC_LOGIN_EMAIL=ci@example.test
export RPC_LOGIN_PASSWORD=... # from your CI secret store
rpc login https://staging.example.com/sign-in --form
rpc shot https://staging.example.com/app/billing --jsonIt loads the page, waits for it to settle (so a client-side form has
hydrated), fills the first match of --email-selector (default
input[type=email], then autocomplete=username, name=email,
name=username, #email) and --password-selector (default
input[type=password]), clicks --submit-selector (default
button[type=submit], input[type=submit], or presses Enter if there is no
button), and waits for the URL to leave the login page. If it doesn't
within --timeout, it fails and quotes any alert the page shows ("Invalid
email or password"). --email works as a flag too; see the security notes
before using --password.
Cookies and headers
rpc shot localhost:3000/app --cookie "session=$SESSION"
rpc shot app.example.com/admin --cookie "sid=$SID; Domain=example.com; Path=/"
rpc shot api.example.com/docs --header "Authorization: Bearer $TOKEN"
rpc shot localhost:3000/app --storage-state auth.json--cookieis host-only on the target unless you give aDomain, which must match the target host (or an--allow-originhost).Secure,HttpOnlyandSameSiteare accepted too.--headeris sent only on requests to the target origin. Images, scripts and API calls to other origins don't get it, and neither does a redirect to another host (every redirect hop is checked).--storage-stateand saved sessions load only the target origin's cookies and localStorage, even if the file holds others (an SSO domain, analytics).--allow-origin https://api.example.comwidens all of the above to another origin, for an app that calls a separate API host.A saved session is used unless
--no-sessionor--storage-stateis given. Cookies and headers are added on top of it.
Security notes
Values are never printed. Plain, rich and
--jsonoutput and MCP results show cookie and header names with•••. Console errors and failure messages are scrubbed of every value rpc injected. The--jsonresult gains anauthfield, for example{"cookies":[{"name":"sid","value":"•••","domain":"localhost","path":"/"}]}.Session files are credentials. They are written with mode 0600 in a 0700 directory (
$XDG_CONFIG_HOME/rpc-agent/sessionsif set;RPC_SESSIONS_DIRoverrides the location). Anyone who can read one can act as you on that site until it expires.rpc sessions cleardeletes them. Don't commit a--savefile.Keep passwords out of shell history. Prefer
RPC_LOGIN_PASSWORDfrom a secret store over--password, which lands in your shell history and is visible to other users in the process list; rpc warns when you use it. The same goes for tokens in--cookieand--header: pass them from an environment variable (--header "Authorization: Bearer $TOKEN"), not literally.Use a test account. Sign in as a user whose data you're happy for screenshots (and the agent reading them) to see.
Ship-check
rpc check <url> audits one page before you call it done. It loads the page
on a phone and a desktop width, the way rpc shot does, and lists what's
wrong: missing or broken social tags, layout shift, buttons a screen reader
can't name, fonts that hide text while they load, the mobile viewport bugs
that only show up on a phone, and motion a screenshot will get wrong.
rpc check localhost:3000
rpc check localhost:3000 --fail-on warn --json # CI
rpc check example.com/pricing -d pixel,iphone-se,desktop --shotsURLs, devices and auth work as in rpc shot: localhost:3000 needs no
scheme, -d takes ids or aliases (default iphone-15-pro,desktop-1440),
and --cookie, --header, --storage-state and the session saved by
rpc login all apply. A page that redirects to a login URL fails with
AUTH_REQUIRED. Each run checks one URL. It isn't Lighthouse: CLS is the
only performance number it reports.
Option | Meaning |
| Devices to check on. The mobile checks run on phones only |
|
|
| Save PNGs of the elements behind layout-shift and overflow findings into |
| Where |
| Print the report as JSON (see below) |
| As in |
| As in |
The exit code is 0 when there are no errors (with --fail-on warn, no
warnings either), 1 otherwise and 2 for a usage error.
How a check runs
Each device gets a fresh browser context, the same one rpc shot uses.
Before the page's own scripts run, rpc installs a PerformanceObserver for
layout-shift. It loads the page and waits for network idle (up to 10 s),
web fonts and pending images. Then it waits a fixed 500 ms, scrolls down one
viewport and back with a 300 ms pause each way, and reads the page. Because
the sequence is fixed, CLS comes out the same on every run; the tests check
the same page twice and allow 0.005 of difference.
When a finding is identical on several devices it's reported once, with
every device listed, so a missing lang appears one time, not two.
What it checks
Severity decides the exit code. error fails the run, warn fails it only
with --fail-on warn, and info never does.
Basics
id | Severity | When | Fix |
| error | The page answered 400 or above | Serve it with a 200 |
| error | The page didn't load on a device (DNS, refused, timeout) or landed on a login page | The error's own hint, e.g. |
| error (warn if the browser upgraded every request) |
| Load everything over https |
| error | Exceptions thrown during load, first three listed | Fix the first one; the rest often follow from it |
| warn |
| Fix or silence them |
| info | No | Add one to tint the mobile browser bar |
Meta and social
id | Severity | When | Fix |
| error, warn | No | 50 to 60 characters, key words first |
| warn, info | No meta description, or one over 160 characters | One or two sentences |
| warn | No | The absolute URL to index |
| error | No |
|
| error | No viewport meta, or one with |
|
| warn | No | Add an icon link |
| info | No | A 180x180 PNG |
| warn | That Open Graph tag is absent | Add it; link previews read it |
| warn | No |
|
| warn |
| Make it absolute; most scrapers don't resolve relative URLs |
| error |
| Point it at the image file |
| warn, info | Smaller than 600x315, or not the 1.91:1 shape of 1200x630 | Export it at 1200x630 |
rpc fetches og:image once per run and reads its pixel size from the file
header (PNG, JPEG, GIF or WebP). The result is in the report's ogImage.
Layout shift
id | Severity | When | Fix |
| error above 0.25, warn above 0.1, info above 0 | CLS for the load and the scroll, with the three elements that moved most and each one's share | Reserve space for late content: |
The score is computed the way Chrome computes it: the largest session
window, meaning shifts less than 1 s apart and at most 5 s in total. Chrome
marks shifts that follow input as hadRecentInput, including any in the
first 500 ms after navigation, and those don't count. Totals under 0.001
count as zero. Each device's score is also in devices[].cls.
Unlabelled controls
id | Severity | When | Fix |
| error | A button, link or |
|
| error | An | Describe the image, or |
| error | An input, select or textarea with no label |
|
| warn | A field labelled only by its placeholder | Add a visible label |
rpc works out accessible names in the page from aria-labelledby,
aria-label, <label>, alt, the text inside (minus anything
aria-hidden), an SVG <title> and title. It skips elements assistive
tech can't reach: display: none, visibility: hidden, aria-hidden and
inert.
Fonts
id | Severity | When | Fix |
| warn | A loaded |
|
| warn | A font file that sets text in the first viewport isn't preloaded |
|
| warn | A font preload without | Add |
| info | A preloaded font that no | Remove it, or match the URL exactly |
| error | A font file returned 400 or above | Fix the |
Every font finding names the file URL. The @font-face rules come from the
page's stylesheets; the page can't read cross-origin ones (Google Fonts, a
CDN), so rpc downloads and parses those itself. Faces with only local()
sources, like next/font's size-adjusted fallbacks, never block text and are
skipped.
Mobile viewport
These run on phone devices only.
id | Severity | When | Fix |
| error |
|
|
| warn | A rendered element sized with |
|
| warn | Links, buttons and form controls smaller than 44x44 CSS px. Inline links in a sentence are exempt, per the WCAG 2.5.8 inline exception: a link with | Padding, |
| warn | A text field with |
|
A headless phone has no browser bars, so 100vh and 100dvh measure the
same there. That's why vh-units reads the CSS instead of measuring
elements. The height: 100vh; height: 100dvh fallback pattern isn't
flagged: the browser keeps only the last value it understands.
Motion
After the scroll, rpc gives running animations up to 1 s to stop, then
looks at the viewport plus half a viewport above and below. These findings
are about whether a screenshot comes out right, and a page can ship with
them, so they are warn or info and never error.
id | Severity | When | Fix |
| warn | Finite animations (from |
|
| warn | Elements with text or media at opacity under 0.1, | Make sure the reveal fires, and that the content shows without JavaScript or animation |
| info | Infinite animations: spinners, pulses, CSS marquees | Stop them under |
| info | A |
|
| info | Swiper, Embla, Splide, Keen, Flickity, Slick or Glide roots, |
|
JSON report
--json prints this object, and the MCP check tool returns the same one.
schema goes up only when a field changes meaning or is removed; new fields
can appear without it.
{
"schema": 1,
"url": "https://example.com/",
"devices": [
{
"device": "iphone-15-pro",
"name": "iPhone 15 Pro",
"viewport": { "width": 393, "height": 852 },
"dpr": 3,
"mobile": true, // the phone-only checks ran here
"status": 200,
"finalUrl": "https://example.com/",
"title": "Example",
"cls": 0.012,
"layoutShifts": 2,
// only when the page didn't load on this device:
"error": "…", "code": "ERR_NAME_NOT_RESOLVED", "hint": "…"
}
],
"summary": { "errors": 1, "warnings": 4, "info": 2 },
"findings": [
{
"id": "horizontal-overflow", // stable; see the tables above
"check": "mobile", // basics | meta | layout-shift | labels | fonts | mobile
"severity": "error", // error | warn | info
"message": "The page scrolls sideways: 601px wide in a 393px viewport",
"fix": "Find the element that sticks out …",
"devices": ["iphone-15-pro"],
"selector": "div.pricing-table", // when one element is the answer
"url": "…", // when a file is (a font, og:image)
"value": 208, // the number behind it, if there is one
"items": [ // elements, files or log lines, when there are several
{
"selector": "div.pricing-table",
"text": "600px wide, 207px past the edge",
"value": 207,
"screenshot": "/abs/.rpc-shots/…-check-horizontal-overflow-1.png" // with --shots
}
]
}
],
"ogImage": { "url": "…", "status": 200, "contentType": "image/png", "width": 1200, "height": 630 },
"auth": { … } // as in rpc shot, values masked
}Findings are sorted by check, in the order of the tables above, then by
severity. id, check, severity, message, fix and devices are
always there; the other fields appear when they apply.
In CI
npx rpc-agent install
npx rpc-agent check https://staging.example.com --fail-on warn --json > ship-check.jsonThe exit code fails the job, and the JSON can go into a PR comment or a
build artifact. For a page behind a login, sign in first with
rpc login <url> --form (see CI and agents).
With a coding agent
Add the MCP server (below) and tell the agent to run check before it
reports UI work as finished, for example in CLAUDE.md or AGENTS.md:
Before you say UI work is done, run the rpc `check` tool on each page you
changed and fix every error. Fix the warnings too, or say why one doesn't apply.Recording motion: rpc record
A still can't show a carousel, a marquee, or a pinned section whose panels
slide as you scroll. rpc record takes a few seconds of video of the page,
or of one element on it.
rpc record localhost:3000 --selector '.hero-carousel' # 5 s of one element
rpc record localhost:3000 -d iphone --duration 10 --gif # whole phone viewport, plus a GIF
rpc record example.com --selector '#logos' --mp4 # mp4 too, if ffmpeg is on PATHOption | Meaning |
| Record this element. rpc scrolls it into view, waits for reveals around it to settle, and crops the video to its box. If it's bigger than the viewport, the visible part is recorded and the result says so |
| Seconds of video, 0.5 to 30. Default 5 |
| One device id or alias. Default |
| Also write a GIF: 10 fps, at most 640 px wide |
| Also write an mp4. This needs an ffmpeg with an H.264 encoder on PATH; without one, rpc skips the mp4 and says so |
| As in |
| As in |
| As in |
It writes <host>[-<path>]-<device>[-<selector>].webm (VP9) and a
-poster.png of the first frame, plus the .gif and .mp4 when asked:
ok iphone-15-pro (393x852 @3x) #carousel
.rpc-shots/localhost-3000-iphone-15-pro-carousel.webm 1080x600, 3s at 25 fps (90 frames captured, playwright ffmpeg)
.rpc-shots/localhost-3000-iphone-15-pro-carousel-poster.png 1080x600 poster
.rpc-shots/localhost-3000-iphone-15-pro-carousel.gif 640x356, 32 framesFrames come from a loop of page.screenshot() calls clipped to the
element's box. Headless Chromium returns those at device pixels and cropped
exactly, at about 30 fps on a DPR 2 or 3 phone and 60 fps at DPR 1. The two
obvious alternatives do worse. CDP screencast frames arrive at CSS pixel
size in headless mode (393x852 for a DPR 3 phone), so they lose the
pixel density and need a scaled crop. Playwright's recordVideo records the
whole viewport at CSS size with a fixed bitrate, and it can't crop.
The frames go to ffmpeg as MJPEG at a constant 25 fps. Each one is repeated
until the next one's timestamp, so the video is as long as you asked for,
whatever rate the screenshots came in at. The ffmpeg is Playwright's own
build, which rpc install already downloads next to Chromium. It writes
VP9 webm and PNG but has no H.264 encoder, which is why webm is the default
and mp4 needs a system ffmpeg. If Playwright's build is missing, rpc falls
back to ffmpeg on PATH. GIFs are encoded in Node (src/gif.ts) from PNG
frames that ffmpeg decodes out of the webm, so they add no dependency.
rpc doctor says which ffmpeg it found and whether mp4 will work.
MCP server
rpc mcp runs an MCP server on stdio with five tools.
screenshot
Param | Default | |
| required | http(s), file, bare host or local path |
|
| ids or aliases |
| false | framed copy of the first screen |
| false | whole page, stitched past 16,384 px |
| 10 | full page: slices returned inline per device |
| auto | full page: |
| 768 | full page: max slice width in px (raise it for small text on desktop) |
| 60000 | full page: CSS px cap |
| false | emulate |
| false | jump animations (Web Animations, GSAP on |
| true | full page: collapse pinned sections so each shows once |
| as in the CLI | |
| true | return images, not just paths |
|
| |
|
| |
| path to a Playwright storageState JSON | |
| true when one exists | use the session saved by |
| extra origins that also get the headers and cookies |
The result starts with a plain-text block: one line per device (or the error
and hint), followed by the JSON summary (file paths, pixel sizes,
scrollHeight, capturedHeight, segments, clipped, slices, HTTP status,
final URL, title, console errors, motion, warnings, error/code/hint).
Each motion warning also gets its own line under its device, followed by
the fix, like WARN [motion-carousel] carousel: use `rpc record` for motion at #carousel.
Then the images:
Without
fullPage: one JPEG preview per device (the framed shot whenbezels), at most 1024 px on the long side.With
fullPage: the framed first screen whenbezels, labellediphone-15 · first screen in bezel, then the whole page as top-to-bottom slices, each one viewport tall and labellediphone-15 · slice 2/7 · y=852–1704 css px. A three-device review of a typical marketing page is roughly 0.5 MB of JPEG.
When a page has more slices than maxSlices, the text says which ones were
skipped (slices 11-26 (y=8520-21656 css px) skipped inline), all slices are
written to disk and the summary gives their paths and the full PNG, so the
agent can Read the rest without slicing anything itself.
When auth is in play the text starts with an auth: line naming the
session, cookies and headers (values masked), and the JSON summary gains an
auth field. A capture that lands on a login page fails with
AUTH_REQUIRED and the rpc login hint. Signing in is something the user
does in a terminal (rpc login); the agent then just calls screenshot.
check
Runs Ship-check on one URL.
Param | Default | |
| required | http(s), file, bare host or local path |
|
| ids or aliases; the mobile checks run on phones |
| false | save PNGs of the elements behind layout-shift and overflow findings |
| as for | |
| as for |
The result is two text blocks. The first is a short summary: the counts,
then one line per error and warning with its id and selector, like
ERROR [button-no-name] 2 buttons with no accessible name at button.icon-menu.
The second is the JSON report described under JSON report.
The call is a tool error only when the page failed to load on every device;
findings, even errors, come back as a normal result for the agent to act on.
record
Runs rpc record on one URL.
Param | Default | |
| required | http(s), file, bare host or local path |
|
| one id or alias |
| whole viewport | CSS selector of the element to record |
| 5 | seconds, 0.5 to 30 |
| false | also write a GIF |
| false | also write an mp4, when a system ffmpeg has H.264 |
| as for | |
| as for |
The result starts with a summary line (ok iphone-15 #carousel: 3s webm 1080x600 at 25 fps <path>, then the poster, GIF and mp4 paths) and the
JSON result. The poster comes back inline as a JPEG, so the agent can see
what it recorded. An element that isn't there is a tool error with the code
SELECTOR_NOT_FOUND.
list_devices
Takes an optional category (apple-phone, android-phone, tablet,
laptop) and returns ids, sizes and aliases.
list_sessions
Lists the origins with a saved session: cookie count, when it was saved and whether it has expired. Never returns values.
Files go to outDir, which is resolved against the server's working
directory. Claude Code and Cursor start the server in the project root; in
Codex, set cwd on the server to choose the directory.
Claude Code
claude mcp add --scope user rpc -- npx -y rpc-agent mcpDrop --scope user to add it to the current project only. Run
npx rpc-agent install once first so the server has a browser. If
rpc-agent is installed globally, claude mcp add --scope user rpc -- rpc mcp skips the npx lookup on each start.
From a clone, point it at the build instead (the path must be absolute,
and Claude Code needs a restart or /mcp reconnect after rebuilding):
claude mcp add --scope user rpc -- node "$(pwd)/dist/cli.js" mcpIf you use the Claude Code sandbox, pages on hosts outside its network
allowlist fail with ERR_NAME_NOT_RESOLVED or a proxy error; the hint says
so. Add the domain to the allowlist.
Cursor
~/.cursor/mcp.json (global) or .cursor/mcp.json (per project):
{
"mcpServers": {
"rpc": {
"command": "npx",
"args": ["-y", "rpc-agent", "mcp"]
}
}
}From a clone, use "command": "node" and
"args": ["/absolute/path/to/rpc-agent/dist/cli.js", "mcp"].
Codex
codex mcp add rpc -- npx -y rpc-agent mcpOr add it to ~/.codex/config.toml (or .codex/config.toml in a trusted
project). The Codex CLI and IDE extension read the same file:
[mcp_servers.rpc]
command = "npx"
args = ["-y", "rpc-agent", "mcp"]From a clone, use command = "node" and
args = ["/absolute/path/to/rpc-agent/dist/cli.js", "mcp"]. /mcp in the
Codex TUI lists the servers it started.
Development
pnpm test # vitest: unit tests plus e2e captures of local fixtures
pnpm typecheck
pnpm build
pnpm smoke:mcp [url] [--full-page] [--devices a,b] [--max-slices n]
pnpm media # regenerate the README images and GIF (needs ffmpeg)The e2e tests serve test/fixtures locally. tall.html is 21,656 CSS px
(64,968 px at DPR 3): the test checks every row of the stitched PNG against
the known layout across all seams, that the sticky header and a fixed badge
appear once, that the IntersectionObserver section rendered, and the slice
count and labels, including through the MCP server. The missing-browser tests
point PLAYWRIGHT_BROWSERS_PATH at an empty temp dir.
The Ship-check tests serve test/fixtures/check: one page per check
(meta.html, shift.html, labels.html, fonts.html, mobile.html,
basics.html) and clean.html, which must produce no errors or warnings.
shift.html inserts a 300 px banner 800 ms after load, so CLS is known in
advance (300/852 on the phone, 300/1440 on desktop); the test runs it twice
and compares.
The README media are built from real captures by scripts/media/build.ts;
docs/media.md explains how. The CLI's design notes, and what it borrows
from the skills CLI, are in docs/cli-design.md.
Limitations
Chromium only, portrait only. There is no landscape mode, and none of RPC's fake browser chrome bars.
Scroll-driven animations are captured in their scroll-top state. Entrance animations that run once are fine after the warm-up scroll and settle. Ones that reverse when scrolled back (GSAP
toggleActionswith reverse, scrubbed timelines) may show their initial state, and a collapsed pin shows its section's first frame.--finish-animationsonly reaches GSAP when it's onwindow; GSAP bundled into the app's JavaScript isn't, so its tweens stay as they are.--reduced-motionhelps on sites that respect it.Pins are found by GSAP's
.pin-spacerclass,ScrollTrigger.getAll(), and sticky or fixed sections that are the only child of a tall parent. A pin built some other way is still captured with its blank spacer.rpc recordrecords what the page does on its own. It doesn't scroll or click, so a scrubbed section only moves if something else moves it. Frames come at the screenshot rate (about 30 fps on high-DPR phones), and the webm repeats frames to hold 25 fps.Very tall stitched PNGs are big files; review them through the slices.
The bezel geometry is RPC's, unchanged. On kind-default phones the page covers the dynamic island or notch, and generic monitors are drawn edge to edge with only the stand showing.
No scripted interaction before capture (beyond
rpc login --form).Login-page detection goes by the URL path. An app that shows its sign-in form at the requested URL without redirecting is captured as is.
Every MCP call launches a fresh Chromium, which adds about a second.
Ship-check reads
100vhonly from stylesheets the page can read and from inline styles; a100vhrule in a cross-origin stylesheet is missed. Tap targets are measured by their box, so a hit area enlarged with a pseudo-element still counts as small. CLS covers the load and one scroll, not shifts caused by clicking around.
License
MIT. Bezel artwork and device data come from Responsive Point Check; see NOTICE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Screenshot API for AI agents. Any public page as image or PDF, banners hidden, 227 countries.
Generate images, GIFs, videos, and PDFs from HTML, URLs, or templates — from your AI agent.
Screenshot any public web page from an AI agent. Free without signup, or with an API key.
Desktop and mobile website screenshots plus page context for AI agents and automation workflows.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI assistants to capture screenshots of web pages using automated browser sessions. Supports full-page and element-specific screenshots, device simulation, and JavaScript execution for comprehensive web testing and monitoring.69 npmMIT

site-shot-mcpofficial
AlicenseBqualityBmaintenanceEnables AI agents to capture full-page or viewport screenshots of any web page with options for ad removal, cookie banner blocking, and proxy country selection.2335 npm3MIT- AlicenseNot gradedqualityAmaintenanceEnables AI coding and vision tools to capture fresh webpage screenshots, automatically tile full pages into 1072x1072 chunks, capture CSS-selected elements, and record screencasts as PNG or animated WebP files.MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI coding tools to capture fresh webpage screenshots, including full-page tiled chunks, specific DOM elements, and timed screencasts optimized for vision models.MIT