html-in-the-loop is a local MCP server for agent-generated interactive HTML.
It lets Claude Code or another MCP client:
- Create a local browser session.
- Render self-contained HTML in a sandboxed iframe.
- Capture user clicks, input changes, form submissions, and custom
AgentBridge.emit(...)events. - Return the structured browser interaction to the agent as the next input.
From this repo:
npm install
npm run buildAdd it to Claude Code:
claude mcp add html-in-the-loop --scope user -- node /absolute/path/to/html-in-the-loop/dist/index.jsFor this checkout, that will usually be:
claude mcp add html-in-the-loop --scope user -- node /Users/chenhong/project/html-in-the-loop/dist/index.jscreate_session: starts the localhost runtime and returns a session URL.render_html: renders or replaces the HTML for a session.wait_for_interaction: waits for the next structured user interaction.get_events: reads stored events, including events created after a wait timed out.get_session_state: inspects a session.list_sessions: lists active sessions.close_session: closes a session.
The local web runtime listens on 127.0.0.1. It uses port 17321 by default and tries nearby ports if that port is busy. Set HTML_IN_THE_LOOP_PORT to choose a different starting port.
Generated HTML can emit events manually:
<button onclick="AgentBridge.emit('choose_plan', { plan: 'pro' }, { step: 2 })">
Choose Pro
</button>Or use data attributes:
<button
data-agent-event="choose_plan"
data-agent-payload='{"plan":"pro"}'>
Choose Pro
</button>Forms emit all fields automatically. If data-agent-event is omitted, the event type defaults to form_submit:
<form data-agent-event="submit_preferences">
<input name="audience" value="developers">
<button type="submit">Continue</button>
</form>Standalone controls also emit committed input changes automatically:
<input name="topic" placeholder="What should the agent work on?">
<textarea name="notes"></textarea>
<select name="priority">
<option>low</option>
<option>high</option>
</select>
<div contenteditable data-agent-name="freeform_notes"></div>These controls emit field_change events, while contenteditable regions emit content_edit. Inputs inside a form include both the changed field and the current form fields in the payload. The bridge supports text, search, email, number, range, checkbox, radio, date/time, color, file metadata, textarea, select, multi-select, and contenteditable inputs. Add data-agent-input-event="custom_type" to override a field-level event type, or data-agent-ignore on a subtree to opt out.
The event returned to the agent has this shape:
type AgentHtmlEvent = {
id: number;
source: "html-in-the-loop";
session_id: string;
type: string;
payload: Record<string, unknown>;
state?: Record<string, unknown>;
timestamp: number;
html_version: number;
};Ask Claude Code something like:
Use html-in-the-loop to make an interactive PRD direction picker.
After I choose one direction in the browser, continue writing the PRD from that event.
The agent should:
- Call
create_session. - Generate HTML and call
render_html. - Tell you to open the returned URL.
- Call
wait_for_interaction. - Continue from the returned event.
- Generated HTML is rendered inside a sandboxed iframe.
- The iframe is not granted
allow-same-origin. - Events are accepted only from the session URL using a per-session token.
- Event payloads are capped at 64KB.
- HTML payloads are capped at 1MB.
- The local server binds to
127.0.0.1, not a public interface. - Do not put secrets into generated HTML.