Coding agents
Most UI is now written with a model at the keyboard. Left alone, it recalls the utility classes it was trained on. One command tells it this UI is Mlola, and an MCP server answers its questions from the same registry the CLI installs from, so what it writes is what exists.
One command
Initializing a project also writes the instructions its agents read. Every file is merged: anything already in AGENTS.md, CLAUDE.md or .mcp.json stays as it was, and the Mlola section sits between markers so a later run replaces only that. Pass --no-agents to init to skip them.
npx mlola-ui init # a new project: config, styles and agent instructions
npx mlola-ui agents # an existing project: agent instructions only, refreshed| File | What it does |
|---|---|
mlola.agents.md | The design guide: every class and data-* value, every token by purpose, and the rules for new UI. Generated from the stylesheet, refreshed by each run. |
AGENTS.md | A short section between markers, read by Codex, Cursor and most agents: this UI is Mlola, read the guide, compose first, never write a color by hand. |
CLAUDE.md | Imports AGENTS.md, so Claude Code reads the same section. |
.mcp.json | Starts the Mlola MCP server for Claude Code and other agents that read it. |
.claude/settings.json | A hook for Claude Code: after it edits a file, the check runs on that file and what it finds goes back to the agent to fix. Pass --no-hooks to leave it out. |
.cursor/mcp.json and .cursor/rules/mlola.mdc | When the project uses Cursor: the server and an always-on rule. |
.vscode/mcp.json | When the project uses VS Code: the server. |
.github/workflows/mlola-check.yml | With --ci only: the check on every pull request. |
The MCP server
npx mlola-ui mcp speaks the Model Context Protocol over stdio. It needs no key and no network: every answer comes from the registry bundled with the CLI, so it always matches what add installs. Your agent starts it from the project folder, since installing writes there. Run by hand, it says so on one line and then waits quietly for an agent to talk to it; Ctrl+C stops it.
It is listed in the official MCP Registry as io.github.mlolahq/mlola-ui, local and remote, so clients that browse the registry find it by name. Each release updates the listing.
Claude Code, when you did not run init:
claude mcp add mlola --scope project -- npx -y mlola-ui mcpCursor (.cursor/mcp.json), Claude Code (.mcp.json) and other agents that read the same shape:
{
"mcpServers": {
"mlola": { "command": "npx", "args": ["-y", "mlola-ui", "mcp"] }
}
}VS Code (.vscode/mcp.json):
{
"servers": {
"mlola": { "type": "stdio", "command": "npx", "args": ["-y", "mlola-ui", "mcp"] }
}
}Codex:
# ~/.codex/config.toml
[mcp_servers.mlola]
command = "npx"
args = ["-y", "mlola-ui", "mcp"]The remote server
For an agent that cannot run a command in your project, such as a chat app or a hosted editor, the same server answers at https://ui.mlola.com/mcp over Streamable HTTP. It needs no account. It has the same search, component, token, rule and markup-check tools, and gives the install commands to run instead of installing, since it cannot write to your project. Mlola Pro items are described there; their source arrives with a license through npx mlola-ui add.
In a chat app that takes custom MCP connectors, such as claude.ai or ChatGPT, add a connector with this address. The server reads only the public registry, so it asks for no sign-in. It answers up to 10 requests a second from one address, with room for a burst of 100, far more than an agent sends; past that it replies 429 for a moment. In Claude Code, or in any client that takes an HTTP server:
claude mcp add --transport http mlola https://ui.mlola.com/mcp{
"mcpServers": {
"mlola": { "type": "http", "url": "https://ui.mlola.com/mcp" }
}
}What it answers
| Tool | For | Server |
|---|---|---|
get_design_rules | How to build UI the Mlola way: compose first, color by role, measure with the scales, shared state words, touch and focus. Also the project setup (the theme on <html>, every theme's fonts, the Next.js root layout), what each layout primitive does, and the stylesheet link and script for a page with no build step. | Both |
search_components | Components, blocks, pages and templates by what they do, free and Mlola Pro. | Both |
get_component | The import, every prop with fixed values and its default, the classes it styles and the data-* values each reacts to, and the install command. Optionally the source of a free component. | Both |
get_tokens | The --ml-* tokens grouped by purpose: planes, color roles, spacing, type, density, shape, depth, motion, layers. | Both |
check_markup | Classes and tokens that do not exist, with the closest that do; variant classes; data-* values and React props a component does not take; a control's class on an element that cannot be operated; buttons, links, fields and images with no name; utility classes; a borrowed data-mode. In style attributes, <style> blocks and utility classes, what is typed by hand: colors, spacing, type sizes, radii, shadows, durations and layers, overridden theme tokens and text faded with opacity. Each issue names its line and the fix. | Both |
get_render_catalog | The Mlola Render catalog: the components an agent may compose at runtime as A2UI messages, each prop and value, and examples. Or the A2UI catalog itself. | Both |
check_render | Checks A2UI messages before they render: components, props and values that do not exist, colors set by hand, fields without a name, a broken tree, headings that skip a level. Each error names its path and the fix. | Both |
render_ui | Shows a checked surface in the conversation as an MCP App, where the host shows apps; a pressed button comes back as the person's next message. Elsewhere it answers with the check's report. | Both |
audit_page | Opens a page of the running app, or an HTML file, on a desktop and a phone, light and dark, and reports what only shows once it is drawn: text cut off, a control over text, fields that do not line up, sideways scroll, a menu past the window's edge, text below the contrast floor, an error the page threw. | Local |
add_components | Installs components with the CLI, dependencies and styles included. | Local |
init_project | Sets the project up, agent instructions included. | Local |
get_install_command | The commands to run in the project, with the license step for Mlola Pro items, and the link and script for a page with no build step. | Remote |
The local server also serves the guide as the resource mlola://guide (and mlola://guide/pro once Pro items are installed), and a build_ui prompt that walks an agent through the rules, the components and the check. The remote server serves mlola://guide. The two render tools are for showing interface at runtime instead of writing code, and render_ui serves its view as the resource ui://mlola/render: see Mlola Render.
Check before finishing
The instructions ask every agent to run check_markup on what it wrote. It reads classes, data-* values and inline styles against the contract generated from the stylesheet, and says how to fix each one:
<button className="ml-button ml-button-primary p-4" data-variant="huge">Save</button>error "ml-button-primary" is not a Mlola class.
Mlola has no variant classes: use "ml-button" with a data-* attribute (data-variant, data-tone, data-size).
warning Utility classes (p-4) do nothing here: Mlola ships no utility framework.
error data-variant="huge" is not a value ml-button ml-button-primary takes.
Allowed: danger, link, outline, primary, secondary, subtle.A class that exists can still be on the wrong element, and a color that reads a token can still read one that was made up. The check also knows which elements each class is drawn on, every token by name, each component's props, and what gives a control its name:
<div className="ml-button" onClick={archive}>Archive</div>
<button type="button" className="ml-button" data-size="icon"><IconTrash /></button>
<input className="ml-input" placeholder="Search projects" />
<p style={{ color: "var(--ml-text-secondary)", fontSize: 13 }}>Updated today</p>error ml-button is drawn on <button>; on a <div> it takes no focus and no key press.
Write <button type="button" class="ml-button">, or <a href> when it leads to another page.
error This button has no name: it holds no text, and no aria-label says what it does.
Give an icon-only control aria-label="…" (and a tooltip or title), or put the words in a <span class="ml-visually-hidden">.
error This field has no label: a placeholder disappears as soon as someone types.
Name it with <label for="its id">, by wrapping it in a <label>, or with aria-label when the design shows no label.
warning A type size is written by hand in style (fontSize: 13).
error --ml-text-secondary is not a Mlola token: the declaration that reads it does nothing.
Closest: --ml-text, --ml-text-muted, --ml-text-faint. List the tokens with get_tokens.Checked without being asked
An agent told to run a check sometimes forgets. In Claude Code, init adds a hook to .claude/settings.json: after each edit, the check runs on the file that changed. When it finds nothing it stays silent; when it finds something, the report goes back to the agent, which fixes it before going on. Files that hold no markup or CSS, and files outside the project, are left alone.
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write|MultiEdit", "hooks": [{ "type": "command", "command": "npx -y mlola-ui check --hook" }] }
]
}
}Look at the page
Source that passes every check can still draw a page with a title cut off on a phone. An agent cannot see what it built, so npx mlola-ui audit looks for it: it opens the page in a browser at 1280 and 390 pixels, in light and dark, and reports text cut off or running out of its card, a control sitting on another element's text, fields in one row that do not line up, sideways scroll, images that did not load, a menu or dialog past the window's edge, text below 4.5:1 (3:1 when large), and any error the page threw. These are the audits Mlola runs on its own components in every theme.
npx mlola-ui audit localhost:3000/settings # a desktop and a phone, light and dark
npx mlola-ui audit page.html --themes all # a file, in every theme
npx mlola-ui audit localhost:3000 --shots .audit # and a screenshot of each view--interact also presses what opens (menus, dialogs, tabs) and checks each state; it is off unless you ask, since it presses the page's real buttons. The local MCP server offers the same as audit_page. The browser is your project's own Playwright (npm install -D playwright): the CLI ships none and installs none. It exits with 1 when it finds a problem.
Check a whole project
An agent checks what it just wrote. npx mlola-ui check runs the same check over every HTML, JSX, TSX, Vue, Svelte, Astro and CSS file, so a screen written last month is held to the same system as the one written today. It ends with the drift: how many different colors, spacing values, type sizes and radii were typed by hand instead of read from the tokens. --fix rewrites the spacing that is exactly a step of the scale (16px, 0.5rem) as that step, and leaves a length between the steps for a person to decide.
npx mlola-ui check # the whole project
npx mlola-ui check src --json # one folder, every issue as data
npx mlola-ui check --fix # put lengths that are steps of the scale back on itsrc/app.css
2 warning Spacing is written by hand (margin-top: 18px).
Measure with the scale: var(--ml-space-2), var(--ml-space-4)… (get_tokens spacing), or a calc() of its steps; space that grows with the window is var(--ml-space-fluid-sm)… to -xl. An em tuned to the font is fine; a length invented between the steps is not.
3 error A color is written by hand in the stylesheet.
Read a token: var(--ml-text), var(--ml-primary-text), var(--ml-surface)… (get_tokens). The theme then keeps its contrast in every mode.
src/invite.tsx
3 error A color is written by hand in style.
Read a token: var(--ml-text), var(--ml-primary-text), var(--ml-surface)… (get_tokens).
3 warning Spacing is written by hand in style (padding: 18).
Measure with the scale: var(--ml-space-2), var(--ml-space-4)… (get_tokens spacing), or a calc() of its steps; space that grows with the window is var(--ml-space-fluid-sm)… to -xl. An em tuned to the font is fine; a length invented between the steps is not.
5 error "ml-button-primary" is not a Mlola class.
Mlola has no variant classes: use "ml-button" with a data-* attribute (data-variant, data-tone, data-size).
Checked 2 files: 3 errors and 2 warnings in 2 files.
Drift, typed by hand instead of read from the tokens:
2 colors: #6b7280, #fafafa
1 spacing value: 18px ×2
--fix puts the lengths that are exactly a step of the scale back on it.It works in any project, so it can measure one before it uses Mlola. In a Mlola project it leaves out what the CLI installed (--all takes the copied markup in; Mlola's own stylesheets, the built theme among them, are held by the library's gates and never checked). Where Tailwind, UnoCSS or Windi is installed it leaves utility classes alone and counts only the values typed into them, like bg-[#fafafa] or p-[13px].
It exits with 1 when it finds an error (with --strict, a warning too), and on GitHub Actions each issue is also an annotation on the pull request, so new UI meets the system before it merges:
# .github/workflows/ui.yml
name: UI
on: pull_request
jobs:
mlola:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx -y mlola-ui@latest check srcThe guide, without an agent
Everything the server knows is also plain Markdown, for any assistant or any person: mlola.agents.md in the project, @mlola-ui/engine/generated/agents.md in the package, and agents.json beside it with the same facts as data. Mlola Pro adds mlola-pro.agents.md with its own classes on the first Pro install.
The site answers in Markdown too: /llms.txt maps it, every component page is Markdown at its address plus .md (button.md), and /llms-full.txt holds the free contract and every free component in one file. Mlola Pro items appear there by what they do, never as source.