Current package version: 0.15.0
Model Context Protocol server for LLui. Exposes debug tools for LLM-assisted development.
pnpm add -D @llui/mcp
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 })] })
| Tool |
Description |
get_state |
Get current component state |
describe_state |
Describe state shape and types |
search_state |
Search state tree by path or value |
| Tool |
Description |
send_message |
Dispatch a message to the component |
validate_message |
Check if a message matches the Msg union |
| Tool |
Description |
get_message_history |
List all dispatched messages |
export_trace |
Export message trace for replayTrace |
replay_trace |
Replay a trace and compare states |
| 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 |
| Tool |
Description |
decode_mask |
Decode a binding's dependency mask into state path names |
mask_legend |
Show the full mask-to-path mapping |
| Tool |
Description |
snapshot_state |
Save a named state snapshot |
restore_state |
Restore a previously saved snapshot |
| Tool |
Description |
list_components |
List all mounted component instances |
select_component |
Select a component for subsequent commands |
| 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 |
| 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) |
| 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 |
| 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 |
| Tool |
Description |
eval |
Arbitrary JS in page context; returns result + observability envelope |
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
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.
function mcpActiveFilePath(cwd: string = process.cwd()): string
Path for the per-launch HTTP bearer token used in --http mode. Lives
next to the active marker (same workspace-rooted cache dir) so a
same-user local client can read it, but is a SEPARATE 0600 file — the
token is never written into the world-readable marker. Lives here (not
in cli.ts) so tests can import the path without triggering the CLI's
top-level main() side effect.
function mcpHttpTokenPath(cwd: string = process.cwd()): string
Directory holding the MCP handshake state: the active-port marker and
the per-launch HTTP bearer token.
Defaults to the workspace root's cache (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.
LLUI_MCP_STATE_DIR overrides it. Both this package and
@llui/vite-plugin read that variable, so the two ends of the
handshake move together — which is the point: the default is a single
machine-global path per checkout, so two concurrent instances (two test
runs, two agents driving the same repo) otherwise overwrite each
other's marker and each connects the other's browser (issue #85).
Read per call, not memoized, so a process that sets the variable during
startup is still honored.
function mcpStateDir(cwd: string = process.cwd()): string
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.
*
* `0` asks the OS for a free port; read the assigned one back with
* `boundPort()` once `startBridge()` has resolved.
*/
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
/**
* Opt in to the arbitrary-eval tool (`llui_eval`). OFF by default.
*
* SECURITY: `llui_eval` runs caller-supplied JavaScript in the user's
* live browser session (RCE). It is registered only when this flag is
* true OR `LLUI_MCP_ENABLE_EVAL=1` is set; otherwise the tool is never
* registered and never listed.
*/
enableEval?: boolean
}
class LluiMcpServer {
registry: ToolRegistry
relay: WebSocketRelayTransport
requestedPort: number
mcp: McpServer
cdp: CdpSessionManager
notesRoot: string
devUrl: string | null
constructor(opts: LluiMcpServerOptions = {})
buildMcpServer(): McpServer
createSessionMcp(): McpServer
connect(transport: Transport): Promise<void>
connectDirect(api: LluiDebugAPI): void
setDevUrl(url: string): void
startBridge(): Promise<void>
boundPort(): number | null
stopBridge(): void
writeActiveFile(): void
removeActiveFile(): void
getTools(): ToolDefinition[]
handleToolCall(name: string, args: Record<string, unknown>): Promise<unknown>
}