ui-loop
ui-loop is an MCP server for context-budgeted visual verification of UI changes using structured summaries and diff-only crops.
Capture baseline (
ui_capture): screenshot a URL and save it as the baseline for a label; returns text summary and image only on first capture or when requested.Diff UI (
ui_diff): re-capture and pixel-diff against the baseline, returning only changed-region crops and a summary; the new capture becomes the baseline.Fetch region (
ui_region): get one higher-resolution crop from a stored capture by region index or box.Assert page state (
ui_assert): run text-only checks for text, visibility, count, console errors, overflow, and a11y violations.List captures (
ui_list): show labels and stored captures with ids and sizes.Forget captures (
ui_forget): delete stored captures for a label or all labels.Detect dev server (
ui_detect_dev_server): find local dev server URLs via env, Next.js lock, or common ports.Budget-aware output: all tools accept
maxTokens; text is always included, images fit remaining budget, and omissions are stated.
Provides Next.js-specific integration for route detection and dev server detection. The PostToolUse hook infers routes from Next.js App Router files under app/, strips route groups, maps page/layout/loading/error to their directories, and detects the Next.js dev server via .next/dev/lock. Dynamic segments are skipped with a note.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@ui-loop diff the settings page after my edit and show me what changed"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ui-loop
See the UI your coding agent just changed: structured summaries + diff-only crops in ~300 tokens instead of full screenshots.
Context-budgeted visual feedback for coding agents.
ui-loop is an MCP server, a CLI and a Claude Code plugin that lets a coding agent (Claude Code, Cursor, Codex) see the UI it just changed without flooding its context window. Instead of a full screenshot every turn, the agent gets a short structured text summary (console errors, failed requests, overflow, layout shifts, accessibility violations) plus small crops of only the regions that changed since the last capture, all fitted into a token budget you choose. Captures live on disk under .ui-loop/ with automatic eviction, so nothing accumulates in the conversation.

Real example: full screenshot costs ~1,366 tokens vs ui-loop's ~305 tokens (text summary + one diff crop)
Why
General-purpose browser automation servers are great at driving a browser. They are not designed to be cheap verification primitives:
Playwright MCP accessibility snapshots run 50–540 KB each, and the maintainers' position is that pruning history is the agent's job, not the server's (microsoft/playwright-mcp #1233).
Claude in Chrome screenshots are re-sent every turn. One report measured 18 screenshots at roughly 279K tokens per call, consuming 17% of a Max plan context window in 5 turns (anthropics/claude-code #27869).
Chrome DevTools MCP tool definitions alone cost about 17k tokens before the agent does anything.
None of them ship the thing a coding agent actually needs after an edit: diff-only crops + structured text + eviction as a single primitive. That is all ui-loop does.
Related MCP server: visual-qa-mcp
Install in 60 seconds
Claude Code (plugin, recommended)
claude plugin marketplace add djig/ui-loop
claude plugin install ui-loop@djig-ui-loopThis registers the MCP server, a PostToolUse hook that diffs the relevant route after you edit a UI file, and a skill that teaches Claude the capture → edit → diff loop.
Plain MCP server without the hook and skill:
claude mcp add ui-loop -- npx -y @djignesh21/ui-loop@0.1.2 serveCursor
Add to .cursor/mcp.json:
{ "mcpServers": { "ui-loop": { "command": "npx", "args": ["-y", "@djignesh21/ui-loop@0.1.2", "serve"] } } }Optional best-effort hook: copy examples/cursor/hooks.json to .cursor/hooks.json.
Codex
codex mcp add ui-loop -- npx -y @djignesh21/ui-loop@0.1.2 serveAny agent that reads mcp.json / plugin.json
The repo root carries an agent-plugins.org plugin.json and mcp.json, so Copilot, Codex and Cursor can install it from the GitHub URL.
In your project
npx -y @djignesh21/ui-loop@0.1.2 init # adds .ui-loop/ to .gitignore, prints config snippetsChromium is resolved from UI_LOOP_CHROMIUM, then Playwright's bundled Chromium, then common system paths (Google Chrome, Chromium). If none is found: npx playwright install chromium or set UI_LOOP_CHROMIUM=/path/to/chrome.
The loop
ui_detect_dev_server→http://localhost:3000ui_capture { url, label }before editing (baseline; returns one downscaled full image the first time)Edit the code
ui_diff { url, label }→ text summary + crops of changed regions within budgetui_region { label, region: 0 }only if a crop needs a closer lookFix,
ui_diffagain (each diff becomes the new baseline)ui_assert { url, checks }to close out with text-only checks
Example transcript (abridged):
> ui_capture { url: "http://localhost:3000/settings", label: "settings" }
ui_capture settings (2026-10-03T17-07-36-866Z) — first capture, saved as baseline
title: Settings
url: http://localhost:3000/settings
viewport: 1280×800
console: clean
a11y: no violations at impact ≥ serious
budget: ~1463/4000 tokens (text 97, images 1366)
[image 1280×800 → 960×600]
… agent edits SettingsForm.tsx …
> ui_diff { url: "http://localhost:3000/settings", label: "settings", maxTokens: 1500 }
ui_diff settings: 2026-10-03T17-07-36-866Z → 2026-10-03T17-07-38-786Z
pixels changed: 2.44% (25002/1024000) in 2 region(s)
#0 [20,360 1260×48] 18001px near <button> "Save changes"
#1 [40,140 160×48] 7001px near <main>
layout shifts: 2
main: 0,80 1280×324 → 0,80 1280×334
#cta: 40,140 160×48 → 640,360 160×48
console: 2 error(s), 0 warning(s), 0 uncaught exception(s)
✖ Boom: failed to hydrate widget (×2)
horizontal overflow: YES (scrollWidth 2420 > innerWidth 1280)
a11y: 1 violation(s) at impact ≥ serious
[critical] image-alt: Images must have alternative text (1 node) e.g. <img src="…">
budget: ~269/1500 tokens (text 226, images 43)
[crop #0 512×33] [crop #1 192×80]With the plugin installed, the PostToolUse hook runs this diff automatically after Edit/Write/MultiEdit on *.tsx|jsx|css|scss|mdx when a dev server is detected, and injects a bounded (≤ 1500 chars, no images) summary as additional context. It never blocks the tool call and stays silent when nothing applies.
Tool reference
Tool | Purpose | Returns images? |
| Screenshot + summary; becomes the baseline for | First capture of a label, or |
| Re-capture, pixel-diff against baseline, return summary + region crops within budget. No baseline → acts like | Region crops (largest first) |
| One crop at higher resolution | One image |
|
| Never |
| Labels and stored captures with sizes | Never |
| Delete a label's captures, or all | Never |
|
| Never |
Every tool also returns structuredContent (JSON) alongside the text for clients that prefer it.
The text summary always includes: page title, final URL, viewport, deduped console errors/warnings and uncaught exceptions (capped), failed requests with status ≥ 400 (capped), horizontal overflow (document.scrollWidth > innerWidth), layout shifts for watchSelectors (default h1,h2,nav,main,button,[role=dialog]), axe-core violations at impact ≥ serious by default (capped), and for diffs the percent of pixels changed plus each region's box and the nearest element (tag, role, text snippet) at its center.
Budget model
Every tool accepts maxTokens (default 4000). Allocation order:
Text summary — always included. Typically 100–300 tokens.
Region crops — by area, largest first, each padded by 16px and downscaled so the longest side ≤ 512px, included while the running total fits.
Omissions are stated —
3 more regions omitted (2, 3, 4); call ui_region …, so the agent knows what it did not see.Full screenshot — only on the first capture of a label or
includeFull: true, downscaled to whatever budget remains.
Image tokens are estimated with Anthropic's published formula, ceil(width × height / 750); text as ceil(chars / 4). Other models bill images differently (OpenAI and Gemini tile at 512px/768px), so treat the estimate as an upper-bound heuristic. The reported budget: line tells you what was actually spent.
Configuration
Environment variables:
Variable | Effect |
| Path to a Chrome/Chromium executable |
| Dev server base URL; used first by detection and by the hook |
| Route the hook should diff, overriding Next.js inference (needed for dynamic segments like |
| Default token budget (4000) |
| Captures kept per label (5) |
| Longest side of region crops in px (512) |
| Navigation timeout (30000) |
| Make the hook a no-op |
Optional ui-loop.config.json in the project root (all keys optional):
{
"keepPerLabel": 5,
"maxTokens": 4000,
"maxCropSide": 512,
"cropPadding": 16,
"maxRegions": 8,
"viewport": { "width": 1280, "height": 800 },
"watchSelectors": ["h1", "h2", "nav", "main", "button", "[role=dialog]"],
"a11yImpact": "serious",
"diffThreshold": 0.1,
"timeoutMs": 30000
}Storage: .ui-loop/captures/<label>/<timestamp>.png + .json. ui-loop init adds .ui-loop/ to .gitignore.
Hook route mapping
For an edited file, the hook picks the route in this order: UI_LOOP_ROUTE → Next.js App Router inference (files under app/; route groups (x) stripped; page/layout/loading/error map to their directory; _private and @slot folders fall back to the parent) → /. Dynamic segments ([id], [...slug]) are skipped with a one-line note instead of guessing.
CLI
ui-loop init
ui-loop capture <url> [--label L] [--json] [--full-page] [--include-full] [--max-tokens N] [--dark]
ui-loop diff <url> [--label L] [--json] [--max-tokens N] [--include-full] [--baseline ID]
ui-loop assert <url> --check noConsoleErrors --check "text=Save@button" --check a11y=serious
ui-loop list | forget [label] | detect
ui-loop hook [--cursor] # stdin: hook payload → stdout: bounded context JSON, exit 0 always
ui-loop serve # MCP over stdio (also the default when stdin is piped)Programmatic API: import { capture, diff, assert, detectDevServer } from '@djignesh21/ui-loop@0.1.2'.
Comparison
ui-loop | Playwright MCP | Chrome DevTools MCP | agent-browser | Claude in Chrome | |
Primary job | Verify a UI change cheaply | General browser automation | Debugging/perf via DevTools | Browser automation for agents | Drive the user's real Chrome |
Diff vs previous state | Yes, region crops | No | No | No | No |
Token budget per call | Yes, explicit | No | No | No | No |
Text health summary (console, network, a11y, overflow) | Yes, always | On request, separate tools | On request, separate tools | Partial | Partial |
Images per turn | Only changed regions; full only on request | Full screenshot on request | Full screenshot on request | Full screenshot | Full screenshot each turn |
Persistent store + eviction | Yes ( | No | No | No | No |
Click/type/navigate flows | No | Yes | Yes | Yes | Yes |
Tool surface | 7 small tools | ~25 | ~26 | Many | Many |
Honest framing: those projects are general browser automation and do far more than ui-loop. ui-loop is a narrow verification primitive meant to run alongside them (or alone, when all you need is "did my edit render correctly?").
Limitations
Static capture of a URL: no clicking, typing or auth flows. Use a dev-only route, query param or mocked state to reach the UI you care about.
Pixel diffs are sensitive to animations, carousels, timestamps and non-deterministic data. ui-loop pauses CSS animations/transitions, but content that changes on every load will show as changed.
fullPagediffs where the page height changes produce large regions; viewport captures are more stable.Route inference covers the Next.js App Router only. Other frameworks get
/unlessUI_LOOP_ROUTEorUI_LOOP_URLis set.Token estimates follow Anthropic's formula; actual billing varies by model and provider.
axe-core runs on the rendered DOM only (no keyboard-navigation checks).
The Cursor hook is best effort; Cursor's hook contract has shifted between versions.
Roadmap
ui_interactwith a strictly bounded action list (click/type/scroll) before capture.Vite/Remix/SvelteKit route inference.
Perceptual diff (ignore anti-aliasing and sub-pixel jitter) and ignore-regions config.
Mobile viewport presets and multi-viewport diffs in one call.
Optional HTML report of a session's captures for humans.
Contributing
git clone https://github.com/djig/ui-loop && cd ui-loop
npm install
npx playwright install chromium # or set UI_LOOP_CHROMIUM
npm run build && npm testUnit tests cover region clustering, budget allocation, token estimation, Next.js route inference, dev-server lock parsing, hook contracts and the summary formatter. The integration test spins up a local HTTP server with before/after fixtures and runs capture → diff → assert in a real Chromium; it skips itself when no Chromium is found.
Issues and PRs welcome. Keep dependencies small, keep tool output bounded, and add a test for behaviour that an agent will rely on.
License
MIT © 2026 Jignesh
More Claude Code tools by @djig: drift-guard (blocks stale React/Next/Tailwind patterns) · claude-onair (desk status light mod) · route-impact (which Next.js routes a diff affects)
Available Tools
7 toolsui_assertAssert page stateARead-onlyIdempotent
Run cheap text-only checks against a live page: text present, element visible/hidden, element count, no console errors, no horizontal overflow, no a11y violations. Returns pass/fail with short evidence. Never returns images.
Inputs: url (required), checks (required), waitFor, darkMode, +1 more.
ui_captureCapture UI baselineA
Screenshot a page and save it as the baseline for its label. Returns a compact text summary (title, console errors, failed requests, overflow, a11y violations). The image is returned only on the first capture of a label or when includeFull=true. Call this BEFORE editing so ui_diff has something to compare against.
Inputs: url (required), label, waitFor, darkMode, +4 more.
ui_detect_dev_serverDetect dev serverARead-onlyIdempotent
Find running local dev servers (UI_LOOP_URL env, Next.js .next/dev/lock, common ports 3000–3005/5173/4200/8080). Returns candidate base URLs.
Inputs: no named parameters.
ui_diffDiff UI against baselineA
Re-capture a page, pixel-diff it against the baseline for its label, and return ONLY what changed: a text summary plus cropped images of changed regions (largest first) that fit within maxTokens. Omitted regions are listed so you can fetch them with ui_region. The new capture becomes the baseline. If no baseline exists, behaves like ui_capture.
Inputs: url (required), label, waitFor, baseline, +5 more.
ui_forgetForget capturesADestructiveIdempotent
Delete stored captures for a label, or all labels when omitted.
Inputs: label.
ui_listList stored capturesARead-onlyIdempotent
List labels and their stored captures (ids, sizes) under .ui-loop/captures.
Inputs: no named parameters.
ui_regionFetch one region cropARead-onlyIdempotent
Fetch a single crop from a stored capture at higher resolution. region is either a diff region index (from ui_diff output) or a box {x,y,width,height} in page pixels.
Inputs: label (required), region (required), captureId, maxTokens.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 7 tools
Each tool has a clearly distinct role: ui_capture establishes a baseline, ui_diff compares against it, ui_region fetches crops, ui_assert runs text checks, ui_list/ui_forget manage stored state, and ui_detect_dev_server discovers servers. The capture-vs-diff distinction is explicitly documented (capture=before editing, diff=after), so misselection is unlikely.
All seven tools use a uniform ui_ prefix with snake_case verb_noun structure (ui_capture, ui_diff, ui_region, ui_assert, ui_list, ui_forget, ui_detect_dev_server). The only slightly longer name follows the same convention, so the pattern is fully predictable.
Seven tools is well-scoped for a visual-regression/diffing workflow: one tool each for capture, diff, inspection, assertion, listing, deletion, and server discovery. No tool feels redundant or missing at this count.
The lifecycle is well covered: capture a baseline, diff after edits, drill into regions, assert cheap checks, and manage labels via list/forget plus dev-server discovery. Minor gaps exist (e.g., no explicit label rename/config or batch operation), but agents can work around these easily.
Maintenance
Related MCP Connectors
Capture screenshots, detect visual regressions between page versions, and analyze with AI.
Give agents eyes on any web page: structured context, and changes explained in plain language.
Visual testing for coding agents: your agent sees every UI change it makes and triages each diff.
Hosted browser for AI agents: screenshots, post-JS DOM, console, WCAG. No install, no API key.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables coding agents to visually verify front-end changes by capturing screenshots at real breakpoints, identifying the single element responsible for overflow, and surfacing console and network errors.22MIT
- AlicenseNot gradedqualityBmaintenanceEnables coding agents to run an evidence-backed visual QA loop for browser interfaces, including inspecting a running app, retrieving findings with screenshots, and rechecking only the captures affected by a fix.MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI coding agents to audit rendered web pages for design and UI defects by measuring geometry, styles, contrast and motion in a local browser and judging them against evidence-tiered rules. Findings come back as short, fixable items expressed in the project's own design tokens, while only sanitized numeric measurements leave the machine.Apache 2.0
- FlicenseNot gradedqualityAmaintenanceEnables AI vision workflows to capture fresh webpage screenshots, specific DOM elements, and timed screencasts, automatically limiting resolution and tiling full pages into 1072x1072 chunks for LLM-friendly processing.-