frontend-eyes
# frontend-eyes
[](https://github.com/GeoCodeCrafter/frontend-eyes/actions/workflows/ci.yml)
[](LICENSE)
An MCP server that lets a coding agent see the page it just changed.
```
> make the pricing cards stack on mobile
[frontend-eyes] capture http://localhost:5173/pricing
mobile (360) overflows by 224px - div.card is 180px wider than its parent
tablet (768) ok
laptop (1280) ok
desktop (1920) ok
...editing Pricing.tsx...
[frontend-eyes] capture http://localhost:5173/pricing
mobile (360) ok
tablet (768) ok
laptop (1280) ok
desktop (1920) ok
```
That's the real output format, not a mock-up of one.
---
## Why
Coding agents write front-end code blind. They change a stylesheet, can't see
the result, and report success based on the diff they just wrote. Anyone who's
watched an agent confidently break a layout and then tell you it's fixed knows
the failure mode.
There are general browser-control servers, and they hand the agent a whole
browser with fifty tools. That's a lot of tokens and a lot of ways to get lost.
This one doesn't drive a browser. It answers the two questions that actually
come up after a front-end change:
1. What does it look like, at the sizes people use?
2. Did anything throw or fail to load?
Narrow beats general here. Small responses, cheap enough to call every turn.
## Tools
| Tool | Returns | |
| --- | --- | --- |
| `capture` | Per-viewport overflow detection naming the one element responsible; screenshots on request | ✅ |
| `diagnose` | Console errors, uncaught exceptions, failed requests | ✅ |
| `audit` | Accessibility violations via axe, ranked, with the offending selector | v0.2 |
| `compare` | Visual diff against the previous capture | v0.2 |
Responses are terse on purpose. Images come back only when asked for, because a
screenshot costs thousands of tokens and most turns only need "360px overflows
by 224px".
## The overflow bit
The interesting part is naming *one* element.
When a box is too wide, every ancestor is too wide as well, so a list of
offenders is mostly the path back to `<body>`. Worse, an oversized box shoves
its later siblings further right than itself — so sorting by "furthest past the
edge" picks a victim rather than the cause.
A culprit is an element that doesn't fit inside its own parent. Deepest one
wins, since its ancestors are only wide on its account.
I arrived at the same rule independently in [whylayout](https://github.com/GeoCodeCrafter/whylayout),
and the two agree on the same page — `div.tile.tile--wide`, 550px wider than its
parent at 1280. Two implementations reaching the same answer is about as much
confidence as you get without a spec to check against.
## Install
```bash
npm install -g frontend-eyes
```
```json
{
"mcpServers": {
"frontend-eyes": {
"command": "frontend-eyes",
"args": ["--allow", "http://localhost:*"]
}
}
}
```
## Safety
It only visits origins matched by `--allow`, which defaults to `localhost` and
`127.0.0.1`. This exists to look at the dev server on your own machine, not to
browse the web on an agent's behalf.
Out-of-allowlist URLs are refused outright rather than confirmed. The caller is
a model, so a confirmation prompt would just be answered by the model — refusal
is the only response that means anything.
## Checking it works
Point it at any dev server:
```bash
npm run build && node scripts/smoke.mjs http://localhost:5173
```
Against a page with a deliberately oversized grid item:
```
capture http://localhost:5173
mobile (360) overflows by 1409.1px - div.tile.tile--wide is 1338px wider than its parent
tablet (768) overflows by 1001.1px - div.tile.tile--wide is 930px wider than its parent
laptop (1280) overflows by 555.1px - div.tile.tile--wide is 550px wider than its parent
desktop (1920) overflows by 235.1px - div.tile.tile--wide is 550px wider than its parent
allowlist
allow http://localhost:5173
refuse https://example.com/
refuse http://localhost.evil.com/
```
That last refusal is the one worth having a test for — `localhost.evil.com` is a
real domain someone else can own, and a naive `startsWith('http://localhost')`
would wave it straight through.
## How it's built
Everything with a decision in it is a pure function over plain data: which
element is the culprit, how a viewport string parses, what the response says.
Those get unit tests. The Playwright layer that actually drives Chromium doesn't,
because mocking Playwright would only prove the mocks were called.
```
src/
browser/ the origin allowlist, breakpoints, one reused browser
dom/ the culprit rule - pure, over measurements
format/ responses, terse by default
tools/ capture and diagnose
```
21 unit tests. The browser half is verified by running `scripts/smoke.mjs`
against a real dev server.
## Not done yet
- `audit` and `compare`. The uniforms are planned in [PLAN.md](PLAN.md), the code
isn't written.
- No screenshot baseline storage, which `compare` needs first.
- Chromium only.
## Licence
MIT
TDQS
Scored across 2 tools
capture is solely concerned with layout overflow across viewport widths, while diagnose handles console errors, exceptions, and failed requests. Their purposes are clearly separated with no meaningful overlap.
Both tool names are lowercase imperative verbs and follow the same two-tool pattern, but they are generic single words rather than descriptive verb_noun pairs. This is still internally consistent, though slightly less informative.
Two tools is small, but the server appears intentionally scoped to frontend issue detection rather than broad browser automation. The count is slightly thin yet reasonable for the stated purpose.
The pair covers common frontend failure modes: visual overflow and runtime/network errors. Missing areas like accessibility or performance checks are not obviously required by the server's narrow focus, though the surface is minimal.