Motus 1.0.13 added a Model Context Protocol server, and 1.0.14 added coordinate input for surfaces the DOM cannot describe, plus page video recording. Put those together and an agent drives a real browser through the same engine your test suite uses.
Start with what you do not install. There is no Motus.Mcp package on NuGet. The server is a verb on the CLI tool: motus mcp.
Install and register
dotnet tool install --global Motus.Cli
motus install
claude mcp add motus -- motus mcp
Everything after -- is the command the client runs, so server options go there too: claude mcp add motus -- motus mcp --channel chrome.
Clients that read a JSON configuration file, Claude Desktop among them, take the same command and arguments under an mcpServers map:
{
"mcpServers": {
"motus": {
"command": "motus",
"args": ["mcp"]
}
}
}
If the client tells you the server failed to connect, the usual cause is that motus is not on the PATH the client inherits. Check with which motus, which should print something like ~/.dotnet/tools/motus. If it prints nothing, stop depending on PATH and give the absolute path instead, either as command in the JSON above or on the command line:
claude mcp add motus -- "$HOME/.dotnet/tools/motus" mcp
You will not find a start-session tool, and you do not need one. The browser and a "default" context are created lazily, on the first call that needs a page.
What the agent actually sees
snapshot returns the browser's accessibility tree as indented text. Two spaces per level, one line per node, and a ref on every node that maps to a real DOM node. Refs are e1, e2 and so on, assigned in document order.
- RootWebArea "Sign in" [ref=e1]
- navigation [ref=e2]
- button "Sign in" [ref=e3]
- form [ref=e4]
- textbox "Email" [ref=e5]
- textbox "Password" [ref=e6]
- button "Sign in" [ref=e7]
State follows the ref in brackets: [value="..."], then any of disabled, readonly, required, checked, selected, expanded, pressed. The interaction tools take the ref, not a selector:
type(ref: "e5", text: "ada@example.com")
type(ref: "e6", text: "hunter2", submit: true)
Refs belong to the latest snapshot. Use a stale one and you get back an error result that carries the fix, rather than a protocol failure: Ref 'e5' is not in the latest snapshot. Call snapshot to refresh refs, then retry. An agent reads that and re-snapshots on its own. This is why perception here is a tree rather than a picture. A screenshot shows an agent what a page looks like; a snapshot gives it something it can address.
The 51 tools
| Area | Tools |
|---|---|
| Navigation | navigate, go_back, go_forward, reload, wait_for |
| Perception | snapshot, screenshot, audit_accessibility, get_performance |
| Interaction | click, type, press, press_key, hover, focus, clear, select_option, set_checked, scroll_into_view, upload_files, wait_for_element |
| Coordinates | click_xy, hover_xy, move_xy, scroll_xy, drag, resize |
| Tabs and contexts | tab_list, tab_open, tab_select, tab_close, context_list, context_create, context_select, context_close |
| Scripting | evaluate |
| Dialogs | handle_dialog |
| Network | route_fulfill, route_abort, route_continue, unroute, route_list, network_requests |
| Console | console_messages |
| Recording and codegen | generate_pom, trace_start, trace_stop, har_start, har_stop, video_start, video_stop |
When the tree has nothing to point at
A page that paints to a <canvas> gives the accessibility tree nothing to describe. Rather than let an agent decide the page is empty, snapshot says so and names the way out: take a screenshot, find the control visually, then act on its position.
click_xy, hover_xy, move_xy, scroll_xy and drag take CSS pixels in the viewport, the same space screenshots and getBoundingClientRect() report. resize changes the viewport when a target sits past its edge. The session default is 1280x800, and --viewport WIDTHxHEIGHT changes it at launch.
I want to be honest about what you give up here. There is no element resolution and no actionability check. Input is still dispatched as trusted browser-level events, so frameworks that ignore synthetic JavaScript events respond to it, but the browser's own hit test decides what receives the event. An overlay with pointer-events: none gets passed through; an overlay that accepts pointer events takes the click instead of your target. drag accepts refs (start_ref, end_ref) or coordinates, one addressing mode per call, and it always emits intermediate moves, with steps and hold_ms for libraries that threshold a drag start.
The parts that come from a test framework
Three tools are here because the engine underneath is a test framework rather than a driver.
audit_accessibility runs Motus's nine built-in WCAG 2.1 A and AA rules against the browser's accessibility tree, and takes min_severity if you want to filter. get_performance returns the Core Web Vitals the suite asserts on: LCP, FCP, TTFB, CLS and INP, plus JS heap size and DOM node count. Both report exactly what a test would, so an agent's finding and a CI failure are the same measurement rather than two things you have to reconcile.
generate_pom closes the loop with the recorder and code generator. It analyzes the page the agent just explored and hands back a C# Page Object Model class inline, with namespace and class_name optional. That turns an exploration into a test instead of a transcript, which is the whole reason I wanted it in the server.
For evidence, trace_start and trace_stop, har_start and har_stop, and video_start and video_stop all follow one convention: stopping finalizes the file and returns its path, and if you leave the path out, one is generated under the temporary directory. Video is MJPEG in an AVI container, written without an external dependency, captured at viewport resolution, and no mouse cursor shows up in it. Convert it with ffmpeg -i in.avi -c:v libx264 out.mp4 when you need another format. If you would rather record everything, --record-video <dir> captures every page for its whole life, and in that mode video_start reports an error because the page is already recording.
Serving it to a team
The same tools run over Streamable HTTP, which is the transport for remote clients. It is not SSE.
# loopback only, no token needed
motus mcp --http
# reachable from other machines: a non-loopback bind requires a token
motus mcp --http --host 0.0.0.0 --port 8931 --token "$MOTUS_MCP_TOKEN"
Each connected client gets its own session with its own browser, and a session that sits idle for thirty minutes is torn down along with the browser it holds. Binding a non-loopback host without a token is refused at startup rather than warned about, and when a token is set every request is compared against it in constant time. stdio inherits the trust of whoever launched it and needs no token. HTTP does not inherit anything, so treat that token like a credential.
Worth knowing before you start
The tools are registered by hand rather than found by scanning, which keeps reflection out of schema generation. The server targets net8.0 and net10.0 and rolls forward, so a machine carrying only the .NET 10 runtime runs it fine.
Firefox is available through --channel firefox, but not all of this is. Tracing, network interception and the accessibility tree are Chrome DevTools Protocol capabilities, so the tools built on them return a diagnostic instead of a result on a Firefox session. Drive Chromium unless Firefox is the thing you are testing.
The MCP server guide has the full option table, and the site publishes a machine-readable index of every documentation page at llms.txt for an agent that would rather read the manual itself. Point a client at it, give it a page you know well, and tell me where the snapshot let you down.