Skip to main content
Glama

EASY UI MCP

Not just another browser controller — a local UI-testing MCP that gives your agent structured, shareable test reports.

easy-ui-mcp is a Dockerized MCP (Model Context Protocol) server for local UI testing. It exposes Playwright-based browser tools over HTTP/SSE so an AI agent (like Claude Code) can drive web UI flows step-by-step and get back a JSON + HTML report with screenshots — no server-side LLM, no test scripts to write.

The detailed guide is on the docs page (source: site/index.html, plain static files on Vercel).

The problem

General browser automation answers "can the agent click this?". Testing asks "did the flow work, and where is the proof?". A transcript of clicks is not proof: it has no verdict, no screenshots, and no list of the console errors and failed requests that happened along the way.

Related MCP server: Playwright MCP Server

Why use it

What you get

How it works

A report for every session

A JSON file and a self-contained HTML report: screenshots, step labels, pass/fail, the action log. See What the report shows.

Primitives made for testing

ui_step names each stage; ui_assert, ui_check and ui_wait_for are three different tools on purpose.

A verdict you can trust

A hard failure fails the session; a soft ui_check does not. Console errors, page errors, failed requests and HTTP 4xx/5xx are listed under Browser problems.

Local-first, one command

docker compose up -d --build. No cloud account, no API key, nothing leaves your machine.

No LLM inside the server

The server only runs tools and records the result. The agent does the reasoning.

Made for Claude Code

One claude mcp add over HTTP/SSE, up to 3 sessions side by side per connection, and a stdio entry for hosts that wrap stdio servers.

Fewer wasted calls

ui_get_page_state returns one short line per element with a ref checked against the live page; native dialogs are held open for the agent to answer.

Report size was measured too: a soft-check run went from 357 KB to about 4 KB once soft failures stopped capturing a full-screen PNG. See Measured.

Quick Start

docker compose up -d --build
curl http://localhost:8765/health
# {"status":"ok"}
claude mcp add --transport http easy-ui-mcp http://localhost:8765/mcp

Then ask Claude Code to navigate to a page and take a screenshot. Using it from another repo? Run claude mcp add from that repo's root too (guide).

Tools: ui_start_session, ui_end_session, ui_step, ui_navigate, ui_click, ui_fill, ui_select_option, ui_press_key, ui_hover, ui_set_checked, ui_scroll, ui_set_viewport, ui_type, ui_navigate_history, ui_click_outside, ui_dialog, ui_assert, ui_check, ui_wait_for, ui_get_page_state, ui_get_text, ui_get_logs, ui_tabs, ui_upload_file, ui_drag, ui_take_screenshot — plus a REST wrapper at POST /api/run-test. Details: Tools. With several open sessions, pass session_id to pick one (Several sessions).

Over stdio (Glama, mcp-proxy): build, then mcp-proxy -- node dist/stdio.js. See Running over stdio.

More

Networking and Scope (v1) are on the docs page. Also: AGENTS.md (architecture, full MCP connection guide), HARNESS.md (REST API), RUNBOOK.md (deploy and rollback), PRD.md (product intent).

Available Tools

26 tools
ui_assertui_assertA

Assert a JS expression against the current page. This is a HARD check: if it evaluates false the whole session is marked failed. Use it only for claims about the application under test. To wait for the page to become ready use ui_wait_for, and for a non-fatal observation use ui_check — do not call ui_assert in a retry loop, as the first false result permanently fails the run.

Inputs: condition (required), session_id.

ui_checkui_checkA

Check a JS expression against the current page WITHOUT failing the session if it is false. Use this for observations you want recorded in the report but which should not condemn the run. If the expression cannot run at all (no page open, or it throws) that is still a hard failure.

Inputs: condition (required), session_id.

ui_clickui_clickA

Click the single element matching the given selector. The selector may be a CSS selector or the selector value from ui_get_page_state, passed unchanged. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them. Optional: button (right/middle), clickCount 2 for a double-click, modifiers for Shift/Control-click, position for an offset inside the element, force to click through an element covering it. If the click opens a native alert/confirm/prompt, the reply says the browser is waiting: answer it with ui_dialog.

Inputs: selector (required), force, button, position, +3 more.

ui_click_outsideui_click_outsideA

Click just outside the single element matching the selector, the way a user closes a modal, drawer or popover by clicking its backdrop. Picks a point whose topmost element is not inside the target and not a control, and says what it hit. Pass the dialog panel, not the backdrop. Fails if no safe point exists (the element fills the viewport): use ui_press_key with Escape then. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: selector (required), session_id.

ui_dialogui_dialogA

Answer a native browser dialog (alert, confirm, prompt, beforeunload). When a tool call opens one, its reply says "Browser is waiting on a dialog" and every other tool is refused until you answer here. accept = OK / Leave; dismiss = Cancel / Stay; for a prompt add text. The reply also reports what the interrupted action did next. With no dialog waiting, this pre-sets the answer for the NEXT dialog, which is then answered at once (one answer, used once). An unanswered dialog is dismissed after a safety timeout.

Inputs: action (required), text, session_id.

ui_dragui_dragA

Drag the single element matching from onto the single element matching to with real pointer events: reorders HTML5 drag-and-drop lists, kanban cards and pointer-based sortables. Use the selector values from ui_get_page_state. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: to (required), from (required), session_id.

ui_end_sessionui_end_sessionA

End a session bracket: writes a JSON + HTML report and returns their paths. Pass session_id to choose which one; it can be omitted only when exactly one session is open.

Inputs: session_id.

ui_fillui_fillA

Fill the single input element matching the given selector with a value. The selector may be a CSS selector or the selector value from ui_get_page_state, passed unchanged. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: value (required), selector (required), session_id.

ui_get_logsui_get_logsA

Read the browser problems captured in this session so far: console errors, uncaught page errors, failed requests and HTTP 4xx/5xx responses (method + URL, never bodies or headers). Use it when an action seemed to do nothing. since: last (default) returns only what is new since the previous call; start returns everything. Pass level: warning to include console warnings. Repeats are grouped (×N); at most 50 lines.

Inputs: level, since, session_id.

ui_get_page_stateui_get_page_stateA

Return the current page's URL, title, visible interactive elements and visible alert/status messages. Each element is {role, name, selector, state?}: pass selector unchanged to ui_click/ui_fill (at most 200 elements); state lists only non-default fields (disabled, checked, value, expanded, selected, pressed, required, invalid; a password's value is never returned). Custom widgets (combobox, listbox, option, switch, slider, contenteditable, summary...) are listed too. overlays ({role, name, selector, modal}, topmost first) names each open dialog and is absent when none is open; focused ({role, name, selector}) names the element holding focus and is absent when focus is on the page body. To read page text use ui_get_text.

Inputs: session_id.

ui_get_textui_get_textA

Read the trimmed visible text of the single element matching the selector, or of the whole page body when no selector is given (cut at 4000 characters with a note of the real length). Use it to check content: a table value, a heading, an error under a field, a total. Fails if the selector matches no element, more than one, or an element that is not visible. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: selector, session_id.

ui_hoverui_hoverA

Move the pointer over the single element matching the selector, to reveal :hover or mouseenter content such as dropdown menus. Use the selector from ui_get_page_state. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: selector (required), session_id.

ui_navigateui_navigateC

Navigate the browser to the given URL and report the result.

Inputs: url (required), session_id.

ui_navigate_historyui_navigate_historyA

Go back or forward in the browser history, or reload the current page, and report the resulting URL and title. Unlike ui_navigate this keeps the history, so it can check that state survives a reload. Going back or forward with no such history entry is an error that says so.

Inputs: action (required), session_id.

ui_press_keyui_press_keyA

Press a key (Playwright key name such as Enter, Tab, Escape, ArrowDown, or a combo like Control+a). With selector it presses on that element; without, on whatever has focus. Enter in a text input submits its form. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: key (required), selector, session_id.

ui_scrollui_scrollA

Scroll one step. With selector, bring that element into view. Otherwise scroll by one viewport height (or width) in direction (down, up, left, right, or jump to top / bottom, default down): the page, or with container the scroll area matching that selector (a chat panel, a virtual table, a carousel). The result says when the end was reached (nothing more to scroll that way): stop looping then. A page or panel with nothing to scroll is not an error. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: selector, container, direction, session_id.

ui_select_optionui_select_optionA

Choose an option of the single matching the selector, by option value or visible label. For a this selects exactly that one option. Use the selector from ui_get_page_state. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: value (required), selector (required), session_id.

ui_set_checkedui_set_checkedA

Set the single checkbox or radio button matching the selector to checked or unchecked. Idempotent: already in that state is fine. A radio cannot be unchecked directly. Use the selector from ui_get_page_state. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: checked (required), selector (required), session_id.

ui_set_viewportui_set_viewportA

Resize the current page's viewport (320-3840 x 320-2160 CSS pixels), e.g. 375x812 for a phone or 768x1024 for a tablet. Layouts can change when the page is resized, so call ui_get_page_state again afterwards.

Inputs: width (required), height (required), session_id.

ui_start_sessionui_start_sessionA

Start a session bracket: opens a fresh browser context and begins logging subsequent primitive tool calls into a report. A connection can hold several sessions at once (up to MAX_SESSIONS_PER_CONNECTION, default 3): pass the returned session id as session_id to every other tool to choose which session it acts on. A session left unused for 10 minutes is closed automatically and its report is written with status timed_out. Optionally test at the sizes and settings real users have: device (a Playwright device name such as "iPhone 13": mobile user agent, touch, pixel ratio and size), viewport (overrides the device size), colorScheme, locale, timezoneId. With none of them the browser is a 1280x720 desktop. These apply to this session only; use ui_set_viewport to resize mid-flow.

Inputs: target (required), device, locale, viewport, +2 more.

ui_stepui_stepA

Label what you are about to do, in plain language. Every following action is grouped under this label in the report until the next ui_step. Call it before each meaningful part of the flow (e.g. "Log in as jcarlin", "Toggle Manual Invoice access on", "Reload and confirm it persisted") — this label is the only human-readable description the report has, so write it for the person who will read the result, not for yourself.

Inputs: label (required), session_id.

ui_tabsui_tabsA

Manage browser tabs of this session (numbered from 1). A link with target=_blank or window.open opens a new tab, announced on the next tool response but not switched to. list: index, url, title, current. switch: make that tab the one every tool acts on. close: close that tab (default: the current one) and fall back to the previous one; the last tab cannot be closed.

Inputs: action (required), index, session_id.

ui_take_screenshotui_take_screenshotA

Capture the current viewport and save it as a PNG to the reports output. With selector, capture just that element; with fullPage, the whole scrollable page (cut at 16000 device pixels tall and 3840px wide). With inline: true the image itself is also returned (scaled to fit 1280x8000px; the file on disk stays full size) so you can look at it. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: inline, fullPage, selector, session_id.

ui_typeui_typeA

Type text into the single editable element matching the selector one key at a time, like a user (keydown, input and keyup per character), appending to what is already there. Use it instead of ui_fill for autocomplete, search-as-you-type, masked inputs (phone, card) and OTP boxes. Use the selector from ui_get_page_state. Text typed into a password, one-time-code or card-number field is never stored in the report. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: text (required), selector (required), delayMs, session_id.

ui_upload_fileui_upload_fileA

Upload the built-in test file default_draft.png (a tiny PNG) to the single element matching the selector: an input[type=file] (hidden ones work), or a button/label whose click opens the file chooser. Takes no path: this is the only file it can ever upload. If the input's accept excludes images the response says so. Selectors containing "nth=" are positional: call ui_get_page_state again after the page changes before reusing them.

Inputs: selector (required), session_id.

ui_wait_forui_wait_forA

Poll a JS expression until it becomes true or the timeout elapses. Use this to wait for the page to render instead of retrying ui_assert. Returning true is recorded as one action; timing out fails the session.

Inputs: condition (required), timeoutMs, session_id.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.8/5.0

    Scored across 26 tools

    Disambiguation4/5

    Most tools target a distinct primitive (navigate, click, hover, drag, scroll, upload, screenshot, tabs, dialog), and the descriptions actively disambiguate the tricky pairs. The remaining overlaps are mild but real: ui_fill vs ui_type both write into an input, and ui_assert vs ui_check vs ui_wait_for all evaluate JS, so an agent must read carefully to pick correctly.

    Naming Consistency5/5

    Every tool is `ui_` prefixed snake_case with a verb-led name (ui_get_page_state, ui_set_checked, ui_press_key, ui_take_screenshot), including the session bracket pair ui_start_session/ui_end_session. No camelCase or naming-style drift anywhere in the set.

    Tool Count3/5

    26 tools is heavy for a single server and sits at the top of the range where consolidation starts to pay off — tab list/switch/close are one ui_tabs tool, but viewport, history and click_outside could plausibly fold into existing primitives. Each tool is nonetheless a genuine, non-redundant browser primitive, so it is justified rather than padded.

    Completeness4/5

    Coverage of the core browser-automation lifecycle is strong: sessions, navigation/history, full input primitives, dialogs, tabs, uploads, waiting, assertions, console/network log reading and screenshots. Notable gaps remain around iframe/frame switching, cookie and localStorage inspection, and download handling, which would block some real flows.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers