@llui/mcp

Model Context Protocol server for LLui. Exposes debug tools for LLM-assisted development.

pnpm add -D @llui/mcp

Usage

The MCP server auto-connects to running LLui apps via the vite-plugin's mcpPort bridge (default port 5200). No manual setup needed -- just enable the plugin and point your MCP client at the server.

// vite.config.ts -- MCP is enabled by default
import llui from '@llui/vite-plugin'
export default defineConfig({ plugins: [llui({ mcpPort: 5200 })] })

Tools

State Inspection

Tool Description
get_state Get current component state
describe_state Describe state shape and types
search_state Search state tree by path or value

Messaging

Tool Description
send_message Dispatch a message to the component
validate_message Check if a message matches the Msg union

History and Replay

Tool Description
get_message_history List all dispatched messages
export_trace Export message trace for replayTrace
replay_trace Replay a trace and compare states

Bindings and DOM

Tool Description
get_bindings List all active bindings and their masks
why_did_update Explain which state change triggered a binding
trace_element Trace a DOM element back to its binding

Bitmask Debugging

Tool Description
decode_mask Decode a bitmask into state path names
mask_legend Show the full bit-to-path mapping

Snapshots

Tool Description
snapshot_state Save a named state snapshot
restore_state Restore a previously saved snapshot

Multi-Mount

Tool Description
list_components List all mounted component instances
select_component Select a component for subsequent commands

View and DOM

Tool Description
inspect_element Rich report: tag, attrs, classes, data-*, text, computed, box, bindings
get_rendered_html outerHTML of a selector (default = mount root), truncatable
dom_diff Compare expected HTML against rendered HTML
dispatch_event Synthesize a browser event; returns Msgs produced + resulting state
get_focus Active element info: selector, tag, selection range

Bindings and Scope

Tool Description
force_rerender Re-evaluate all bindings; returns indices that changed
each_diff Per-each-site add/remove/move/reuse per update
scope_tree Scope hierarchy with kind (root/show/each/branch/child/portal)
disposer_log Recent scope disposals with cause
list_dead_bindings Bindings that are dead or have never changed value
binding_graph state path -> binding indices (inverts compiler mask legend)

Effects

Tool Description
pending_effects Queued and in-flight effects
effect_timeline Phased log: dispatched -> in-flight -> resolved/cancelled
mock_effect Register match->response mock; next matching effect resolves with mock
resolve_effect Manually resolve a specific pending effect

Time Travel and Utilities

Tool Description
step_back Rewind N messages by replaying from init (pure mode default)
coverage Per-Msg variant fire counts + list of never-fired variants
diff_state Structured JSON diff between two state values
assert Evaluate eq/neq/exists/gt/lt/in against a state path
search_history Filter history by type, statePath change, effectType, or range

Eval

Tool Description
eval Arbitrary JS in page context; returns result + observability envelope

Functions

findWorkspaceRoot()

Walk up from start until we find a workspace root marker. Used by both the MCP server (writing the active marker) and the Vite plugin (watching it) so they agree on a single shared location regardless of which subdirectory each process happens to be running in. Strong markers (workspace root): pnpm-workspace.yaml, .git directory. If neither is found anywhere up the chain, falls back to the highest package.json above start. For pnpm monorepos this finds the workspace root from any subpackage; for single-package projects it finds the package root.

function findWorkspaceRoot(start: string = process.cwd()): string

mcpActiveFilePath()

Path where the MCP server writes its active port marker. Vite plugins watch this file to auto-trigger browser-side __lluiConnect() whenever the MCP server starts, regardless of whether Vite or MCP started first. Resolved relative to the workspace root (not the immediate cwd) so the MCP server and the Vite plugin always agree on a single location even when one runs from the repo root and the other from a subpackage.

function mcpActiveFilePath(cwd: string = process.cwd()): string

Interfaces

LluiMcpServerOptions

export interface LluiMcpServerOptions {
  /**
   * Port for the browser-relay WebSocket bridge. When the MCP transport
   * is stdio (the CLI default), the relay stands up its own server on
   * this port. When the MCP transport is HTTP, the relay attaches to
   * that HTTP server and the MCP protocol + bridge share a single port.
   */
  bridgePort?: number
  /**
   * Optional pre-existing `http.Server` to share with the bridge. When
   * provided, the bridge attaches to it via upgrade routing on
   * `/bridge`; `bridgePort` is ignored for server-creation purposes
   * (but still written into the marker file so consumers know where to
   * connect).
   */
  attachTo?: HttpServer
  /**
   * Optional dev-server URL for CDP fallback navigation. When provided,
   * the CDP session manager will use this URL as the target for Playwright
   * browser instances.
   */
  devUrl?: string
  /**
   * Whether to run the Playwright browser in headed mode (visible window).
   * Defaults to false (headless).
   */
  headed?: boolean
  /**
   * Filesystem root for the devmode-annotate notebook
   * (https://github.com/fponticelli/llui — docs/proposals/devmode-annotate/).
   * MCP notes tools (`llui_list_notes`, `llui_read_note`, …) read from
   * this directory.
   *
   * Resolution order: this option → `LLUI_NOTES_DIR` env var → workspace
   * root + `.llui/notes`.
   */
  notesRoot?: string
}

Classes

LluiMcpServer

class LluiMcpServer {
  registry: ToolRegistry
  relay: WebSocketRelayTransport
  bridgePort: number
  mcp: McpServer
  cdp: CdpSessionManager
  notesRoot: string
  devUrl: string | null
  constructor(optsOrPort: LluiMcpServerOptions | number = 5200)
  buildMcpServer(): McpServer
  createSessionMcp(): McpServer
  connect(transport: Transport): Promise<void>
  connectDirect(api: LluiDebugAPI): void
  setDevUrl(url: string): void
  startBridge(): void
  stopBridge(): void
  writeActiveFile(): void
  removeActiveFile(): void
  getTools(): ToolDefinition[]
  handleToolCall(name: string, args: Record<string, unknown>): Promise<unknown>
}

Constants

mcpToolDefinitions

Snapshot of all registered tool definitions. Kept as a named export for backward compatibility with downstream consumers that used to import the TOOLS array re-export under this alias.

const mcpToolDefinitions: ToolDefinition[]