mcp-vroid
This server lets an MCP client (e.g. Claude) fully drive the VRoid Studio desktop application on Linux/Wayland by seeing screenshots and sending real mouse/keyboard input.
Lifecycle control: launch VRoid Studio, check its status, release the desktop back to the user.
Visual perception: take screenshots (window or full screen), OCR the UI to find text, and find VRoid's blue/grey action buttons by colour.
Raw input: click, drag, scroll, type text, and send key combinations (e.g. Ctrl+S, Return, Escape) as real input events.
Editor navigation: switch between top tabs (Face, Hairstyle, Body, Outfit, Accessories, Look) and create a new character from a base model.
Parameter editing: set numeric sliders exactly and set colour swatches by hex values.
Export and saving: export the model as a .vrm file (VRM 1.0 or 0.0) with metadata and drive Wine's save dialog, and save .vroid projects.
Screen awareness: identify which screen is currently shown (start, editor, export, hair editor) to prevent and diagnose flow failures.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-vroid Launch VRoid Studio and export the current character as a .vrm file."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-vroid
Drive VRoid Studio from any MCP client, on Linux/Wayland. Launch the app,
look at it, find widgets in the picture, click and type, set parameters, and
export a .vrm — all as MCP tools.
Why
VRoid Studio has no scripting API, no CLI, no plugin surface. The only way in
is the one a person uses: look at the window and move the mouse. So that is
what this does — screenshot the window with grim, locate things with OCR and
colour matching, and inject real pointer and keyboard events at the compositor
level. The MCP client's model is the eyes; these tools are the hands.
grim ──► PNG ──► tesseract / cv2 ──► (x, y) ──► virtual pointer / XTEST
▲ │
└──────────────────── screenshot again ◄───────────────┘Related MCP server: Persona Motion Studio
Demo
Everything below was done by an MCP client calling these tools — no human at the mouse.
Setting body parameters by typing exact values into the Parameters panel
(vroid_open_tab("Body") → vroid_set_slider("Head Size", -0.15)):

Inside the hair editor, tuning procedural hair guides (vroid_click on the
group, vroid_set_slider on Height / Interval / Twist Intensity):

Filling the VRM Settings modal on the way to an export (vroid_export_vrm
walks the whole flow, including Wine's save dialog):

Requirements
Developed and tested on Arch Linux + Hyprland, with VRoid Studio 2.14.0 (English UI) running under Steam/Proton. What is actually load-bearing:
needed for | how portable | |
Hyprland ≥ 0.55 | window discovery, focus, workspaces, closing the screensaver — via | Hyprland-specific. All of it lives in |
| screenshots | any wlroots compositor ( |
| moving and clicking the real cursor | any wlroots compositor |
Xwayland ( | keyboard and wheel, via X11 XTEST | any Wayland session with Xwayland |
| OCR — the entire locating story | portable |
| building the pointer helper | portable |
VRoid Studio via Steam/Proton (appid | the app being driven | the Steam launch path is assumed; a native/Wine install needs the launch command changed |
Python 3.11+ and | the server itself | portable |
So: wlroots + Xwayland for the input and capture layer, Hyprland only for window management. On Arch:
sudo pacman -S grim tesseract tesseract-data-eng wayland gcc pkgconfQuickstart
git clone https://github.com/nhodges/mcp-vroid
cd mcp-vroid
uv sync # virtualenv + dependencies
bash native/build.sh # builds native/vpointer <-- REQUIRED, not optionalnative/build.sh compiles a ~150-line C client for the Wayland
virtual-pointer protocol (the protocol XML is vendored under
native/protocols/). Without it every pointer tool fails with
native/vpointer missing; vroid_status tells you whether it is there.
Register it with Claude Code:
claude mcp add vroid -- uv run --directory /path/to/mcp-vroid mcp-vroid…or with any client that takes an mcpServers block:
{
"mcpServers": {
"vroid": {
"command": "uv",
"args": ["run", "--directory", "/path/to/mcp-vroid", "mcp-vroid"]
}
}
}Then ask your client to call vroid_status, and if it looks healthy,
vroid_launch().
Clients often start servers with a sanitised environment. This server
recovers XDG_RUNTIME_DIR, WAYLAND_DISPLAY, HYPRLAND_INSTANCE_SIGNATURE
and DISPLAY from the runtime dir at startup
(src/mcp_vroid/session_env.py), so hyprctl / grim / XTEST work anyway.
vroid_status reports what it had to fill in; anything already in the
environment wins.
Optional environment variables:
var | default | meaning |
|
| where screenshots are written |
|
| default dir for exports/saves |
|
| path to the pointer helper |
|
| longest edge of images sent to the client (0 = never downscale) |
Tools
18 tools, in four groups.
Lifecycle
tool | what it does |
| Start VRoid via Steam if needed, park it on Hyprland workspace 9, remember the workspace you were on, focus + fullscreen it. |
| Window present/focused/title/geometry, active workspace, capture dirs, and whether |
| Switch back to the workspace the user was on. VRoid keeps running on ws 9. |
Seeing
tool | what it does |
| Capture the window (or the whole output, for the Wine save dialog), save it, and return it as MCP image content for the client's model to look at. Reports native size and the downscale factor applied for transport. |
| Fresh capture + tesseract; returns matching word boxes and centres in image px. Pass |
| Finds VRoid's solid |
|
|
Acting — raw input
tool | what it does |
| Glides the pointer in a few steps (so hover states fire) and clicks. |
| Press → 24-step glide → release. Right-drag orbits the camera, middle-drag pans. |
| Wheel, as X11 buttons 4/5 (6/7 horizontal). Park the pointer over the panel you mean to scroll. |
| Types into the focused widget over XTEST. |
|
|
Acting — flows
tool | what it does |
| Start screen → Create New → base → editor. |
| Face / Hairstyle / Body / Outfit / Accessories / Look. |
| Scrolls the Parameters panel to the row and types an exact value into its numeric box. |
| Same, for a |
| The whole Export-as-VRM walk, including the VRM Settings metadata modal and Wine's save dialog. |
| Ctrl+Shift+S to an explicit |
Every acting tool focuses VRoid first and refuses to act if the focused window is not VRoid Studio.
How it works
The loop is see → locate → act → see again:
vroid_launch()vroid_screenshot()— the image goes to the client's model, which looks at itvroid_find_text("Export")orvroid_find_button()for coordinatesvroid_click(x, y)— always with coordinates from a fresh capturevroid_screenshot()to confirm what actually happened
Seeing is grim on the Hyprland window geometry, then tesseract for word
boxes and OpenCV for solid-colour buttons (VRoid's primary pills are
#0096FA, and OCR reliably loses white-on-blue labels).
Acting goes down two different paths, for annoying reasons:
Pointer — a small C client (
native/vpointer.c) speakingzwlr_virtual_pointer_unstable_v1. It moves the real compositor cursor, so hover states and drags behave exactly as they do for a human, and it needs no permissions:ydotool's/dev/uinputroute is0600 root:rootand would need sudo or a udev rule.Keyboard and wheel — X11 XTEST through Xwayland, because the virtual-pointer protocol has no keyboard counterpart and VRoid is an Xwayland client anyway.
Coordinate spaces. Three are in play and they are all different:
space | size on the reference machine | who uses it |
Hyprland layout (logical) | 2048 × 1152 |
|
image pixels of a capture | 2560 × 1440 | tesseract, cv2, everything you see |
X11 pixels (Xwayland) | 2560 × 1440 | XTEST |
Tools take and return image px (space="image") by default and convert
internally, so vroid_find_text output can be handed straight to
vroid_click. If MCP_VROID_MAX_IMAGE_PX downscaled the picture you were
shown, multiply coordinates read off it by the inverse of the reported
downscale — or just ask vroid_find_text, which always reports native px.
Rules of thumb, learned the hard way:
Read the whole frame, not a crop. A "Close Hairstyle Editor" confirm modal sat in the middle of the screen through six failed clicks because the check only OCR'd the top 60 px.
Don't judge change by the 3D viewport. VRoid dithers every frame, so a full-window diff reads ~0.98 even when nothing happened. Watch a UI strip.
Prefer numeric boxes to slider drags.
vroid_set_slidertypes an exact value; dragging is for controls that have no box.Primary buttons are found by colour, not text. A grey pill where you expect blue is the app telling you a required field is empty.
A detailed map of VRoid's UI — tab strip, rails, panels, the export flow, the hair editor, with measured coordinates — is in docs/ui-map.md.
Limitations and brittleness
This is GUI automation with no API underneath. Be realistic about it:
OCR is the whole locating story, and it is imperfect. Small, letter-spaced or light-on-dark labels get split or dropped (
Export→E+xport). White-on-blue is lost entirely, which is whyvroid_find_buttonexists. Icons have no text at all — those anchors are hard-coded fractions of the window.Coupled to the UI version. Needles and fractional anchors were calibrated on VRoid Studio 2.14.0, English, at 2560×1440 / scale 1.25. A pixiv UI reflow, another language, or a different monitor can require re-measuring. (Japanese UI → kebab
⋮→ Settings → Language.)Modals appear outside your search region and swallow clicks silently.
Timing is guessed. The 3D viewport takes ~5 s after a base is chosen; export takes 5–30 s, longer for heavy models.
The Wine save dialog is a separate window with its own class and geometry — use
vroid_screenshot(whole_screen=true)there.Single instance, single session. One VRoid window, one desktop, no headless mode, no parallelism. It drives your screen.
The idle screensaver can grab the session mid-run. The guard refuses to type into it and closes that one window (and only that one) before acting.
Attended use is recommended. See below.
Security
This server injects real mouse and keyboard events into your live desktop session and takes screenshots of it. That is the entire point, and it is also the risk:
Screenshots may capture anything on the output —
whole_screen=truecaptures everything, and captures are written to disk unencrypted.Keystrokes go to whatever holds keyboard focus. The driver refuses to act unless VRoid Studio is focused, but a careless or hostile prompt can still click anywhere inside VRoid.
vroid_launch(restart=true)kills VRoid Studio and loses unsaved work.Nothing here is sandboxed and there is no confirmation step.
Run it attended, on a session you are watching. Don't run it on a shared
or multi-user machine, don't leave an agent driving it unsupervised, and treat
the captures directory as sensitive. vroid_release() gives the desktop back
when you're done.
Development
uv run python scripts/smoke_test.py # start the server, list tools, call vroid_status
uv run python scripts/smoke_test.py --screenshot # + one passive capture if VRoid is open
uv run vroid-driver shot # the original driver CLI, still herevroid-driver (mcp_vroid.driver.cli) is a shell interface to the same
engine — launch, shot, find, click, tab, slider, export, cam,
apply-params, … — handy for debugging without an MCP client in the loop.
Layout:
src/mcp_vroid/server.py MCP tool definitions (stdio)
src/mcp_vroid/session_env.py recovers the Wayland/X session env
src/mcp_vroid/driver/ the engine
window.py hyprctl: find / launch / focus / workspaces <- Hyprland-specific
capture.py grim + coordinate spaces
locate.py tesseract OCR + colour button matching
input.py vpointer (Wayland) + XTEST (X11)
actions.py the VRoid-specific flows
native/vpointer.c zwlr_virtual_pointer clientContributing
Issues and PRs welcome. Useful things to bring:
A port to another wlroots compositor. Only
driver/window.pyis Hyprland-specific; the rest already works anywheregrimandzwlr_virtual_pointerdo.Anchors for other resolutions or DPI scales, or for the Japanese UI.
Bug reports — include your compositor, VRoid Studio version, monitor resolution and scale, and the output of
vroid_status. A capture from the failing step helps enormously.
Nothing here is auto-formatted or linted by CI; match the surrounding style.
Licence
MIT — see LICENSE. VRoid Studio is a product of pixiv Inc.; this project is unaffiliated with pixiv and simply drives the app's UI.
Available Tools
18 toolsvroid_clickA
Click a point in the VRoid window with the real compositor cursor.
Refuses unless VRoid Studio is the focused window; it focuses the window itself first (workspace 9, fullscreen) and raises rather than clicking into somebody else's app.
The pointer glides to the target in a few steps so hover states fire, then clicks and settles ~0.35 s. Coordinates must come from a CURRENT capture - take a fresh vroid_screenshot or vroid_find_text right before clicking, because panels reflow and modals move.
Inputs: x (required), y (required), space, button, +1 more. Returns: object
vroid_current_screenA
Identify which VRoid screen is on top: start, editor, export_vrm, hair_editor, or unknown.
Cheap-ish (OCRs only the top strip, ~2 s) and worth calling whenever a flow tool fails - most failures are "you are not on the screen this tool expects". 'unknown' usually means a modal is up; screenshot the whole window and look.
Inputs: no named parameters. Returns: object
vroid_dragA
Press, glide and release - slider handles, the 3D camera, hair guides.
For Parameters sliders prefer vroid_set_slider, which types an exact value into the row's numeric box; dragging is only for controls that have no numeric box. The glide is 24 interpolated steps, which is what the app needs to register a drag rather than a click.
Inputs: x1 (required), x2 (required), y1 (required), y2 (required), +2 more. Returns: object
vroid_export_vrmA
Walk the entire Export-as-VRM flow and write a .vrm file.
Editor toolbar share icon -> 'Export as VRM' -> the blue Export pill -> the VRM Settings modal (fills Avatar Name and Creators, picks the export format, scrolls to the bottom and clicks Export) -> Wine's save dialog (types a Z:\ path and presses Return) -> waits for the file size to stop growing.
Must be started from the EDITOR screen with a model loaded. Takes 30 s to a few minutes depending on the model. Returns the written path and its size; raises with the path of a diagnostic screenshot if any step fails - read that screenshot before retrying, since a half-finished flow usually leaves a modal open that the next attempt will trip over.
Inputs: path (required), creator (required), avatar_name (required), timeout, +1 more. Returns: object
vroid_find_buttonA
Find VRoid's primary action buttons by their colour, not their text.
VRoid's confirm buttons ('Export', 'OK', 'Create') are solid #0096FA pills whose white labels tesseract regularly loses, so this matches the button chrome. Blobs come back biggest-first, in image px, ready for vroid_click.
A grey pill (color='disabled') where you expected a blue one means the
action is disabled - on the VRM Settings modal that means Avatar Name or
Creators is still empty.
Inputs: color, label, limit, region. Returns: object
vroid_find_textA
OCR the current VRoid window and return where query appears.
Takes its own fresh screenshot, so coordinates are current. Results are image px, ordered by OCR confidence, ready to pass straight to vroid_click (which defaults to space='image').
Known OCR weaknesses in this UI: small, letter-spaced or light-on-dark labels get split or dropped ('Export' -> 'E' + 'xport'), and white text on VRoid's blue primary buttons often disappears entirely - use vroid_find_button for those. Icons (toolbar, left rail) have no text at all; the README's UI map has their fractional positions.
If nothing is found, that is information: the screen may not be the one you think it is, or a modal is covering it. Take a screenshot and look.
Inputs: query (required), exact, limit, region. Returns: object
vroid_keyA
Press a key combination in the focused VRoid widget.
Useful ones: Return confirms a value box or a Wine dialog's default button; ctrl+s saves the project; ctrl+shift+s is Save As; ctrl+z/ctrl+y undo/redo in the editor.
Note Escape does NOT close VRoid's hamburger menu - click elsewhere to dismiss it.
Inputs: combo (required), times. Returns: object
vroid_launchA
Start VRoid Studio (Steam appid 1486350, Proton) and take control of it.
Idempotent: if the window already exists it is reused, not relaunched. Then the window is parked on Hyprland workspace 9, the workspace the user was on is remembered (vroid_release puts them back), and the window is focused and fullscreened so its geometry - and therefore every coordinate you will read off a screenshot - is stable.
Cold start over Proton takes 30-90 s; the call blocks until the window is up. It does NOT wait for the start screen to finish drawing, so take a vroid_screenshot and look before clicking anything.
Inputs: restart, timeout. Returns: object
vroid_new_characterA
From the start screen: Create New -> pick a base -> land in the editor.
Locates the 'Create New' tile by text (so it survives the Recently Edited grid growing), clicks the card above its caption, picks the base thumbnail, then waits up to a minute for the editor - the 3D viewport takes several seconds to appear after a base is chosen.
Inputs: base. Returns: object
vroid_open_tabA
Switch the editor to a top-level tab, located by OCR of the tab strip.
Only works on the editor screen (not the start screen, the export screen or the hair editor). Waits ~2 s for the panels to redraw and returns the path of a verification screenshot - take a vroid_screenshot if you want to see the result.
Inputs: name (required). Returns: object
vroid_releaseA
Hand the desktop back: switch to the workspace the user was on before.
Leaves VRoid running on workspace 9. Call this when you are done with a session, or before handing control back to the human.
Inputs: no named parameters. Returns: object
vroid_save_projectA
Save the .vroid project - plain Save, or Save As to an explicit path.
With name: presses Ctrl+Shift+S and drives Wine's save dialog the same
way the VRM export does, then waits for the file. Without name: opens
the hamburger menu and clicks Save, which overwrites the project's
existing file and opens the Wine dialog only if the project has never
been saved (in that case call this again WITH a name).
Worth doing before any risky experiment: nothing else in this server persists your work, and vroid_launch(restart=true) discards it.
Inputs: name, timeout. Returns: object
vroid_screenshotA
Screenshot VRoid and return the image, plus the path it was saved to.
LOOK at the returned image before you decide anything - this is the only way to see the app. Every capture is also written to the captures dir so it can be re-read later.
Coordinate space: the reported image_size is the native capture size
(2560x1440 on the reference machine) and that is the space every other
tool means by space="image". The transported image may be downscaled
(downscale in the text block says by how much); if you read a coordinate
off the picture by eye, divide it by that factor before clicking. Better:
get coordinates from vroid_find_text / vroid_find_button, which always
report native image px.
Gotchas carried over from the driver: capture the WHOLE window, not a crop, when checking "did that work" - modals appear in the middle of the screen and a top-strip-only check will miss them. And do not judge change by the 3D viewport, which dithers every frame; watch a UI strip instead.
Inputs: tag, region, whole_screen, full_resolution.
vroid_scrollA
Wheel-scroll a panel or zoom the 3D viewport.
Sent as X11 button 4/5 (6/7 horizontally): VRoid is an XWayland client and ignores Wayland virtual-pointer axis events, so this is the only wheel that works on it.
Long Parameters lists need this - a label that vroid_find_text cannot see is usually just below the fold. vroid_set_slider scrolls to its own row automatically, so you rarely need to do it by hand.
Inputs: x, y, dx, dy, +1 more. Returns: object
vroid_set_colorA
Set a colour swatch by typing a hex code into its #RRGGBB box.
Scrolls the right-hand panel to the labelled row, clicks the hex field just below the label, replaces its contents and presses Return. Same caveat as sliders: the row must belong to the currently open tab and sub-category (left icon rail).
Inputs: hex (required), label (required). Returns: object
vroid_set_sliderA
Set a Parameters slider exactly, by typing into its numeric box.
Scrolls the right-hand panel from the top until the labelled row is visible (so it works for parameters below the fold), clicks the numeric box at the end of that row, clears it, types the value and presses Return.
This is far more reliable than dragging the handle - use vroid_drag only for controls with no numeric box. Open the right tab first (vroid_open_tab): each tab has its own parameter list.
Inputs: label (required), value (required). Returns: object
vroid_statusA
Report whether VRoid Studio is running, focused, and where it sits.
Cheap and side-effect free (no OCR, no input) - use it before anything
else, and after anything that might have lost focus. focused false means
every acting tool will refuse until vroid_launch (or any acting tool's own
focus step) takes the window back.
Also reports whether the external helpers this server needs are present: the vpointer binary (built by native/build.sh), grim, tesseract, hyprctl.
Inputs: no named parameters. Returns: object
vroid_typeA
Type into whatever widget currently has keyboard focus.
Click the field first (vroid_click) - this tool has no idea where the caret is. Keystrokes go through X11 XTEST because the Wayland virtual keyboard is mis-read by this Proton client (a whole string arrives as a single character).
Always screenshot afterwards to confirm the text landed in the field you meant: VRoid's forms have several boxes with near-identical captions, and typing into the wrong one leaves the primary button greyed out with no other symptom.
Inputs: text (required), clear_first. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 18 tools
Each tool addresses a distinct capability: launching, screen identification, OCR, clicking, typing, saving, exporting, etc. Even the 'seeing' tools (screenshot, find_text, find_button) have clearly separated purposes: one captures, one finds text, one finds buttons. The interaction primitives (click, drag, scroll, type, key) are mutually exclusive and well-defined. No two tools could be confused for the same action.
All tools share the 'vroid_' prefix and mostly follow a verb_noun pattern (open_tab, export_vrm, set_slider), but a few are single verbs or nouns (status, release, screenshot, click, type). The naming is predictable and readable, with only minor deviations like 'status' and 'release' being not strictly verb_noun. Overall consistent enough for an agent to infer actions.
18 tools is on the higher side but justified for a GUI automation server that needs primitives for every interaction type plus higher-level workflows like export and save. The count feels well-scoped for the domain—each tool earns its place since there's no redundant functionality. Slightly heavy but still reasonable.
The surface covers the full lifecycle: launch, status, screen detection, navigation (open_tab, scroll), interaction (click, drag, type, key, sliders, colors), inspection (screenshot, OCR), persistence (save_project), export (export_vrm), and creation (new_character). No obvious gaps—even edge cases like disabled buttons and modal detection are addressed. The server appears fully equipped for its stated purpose of automating VRoid Studio.
Related MCP Connectors
Build editable 3D scenes, direct characters and cameras, and export AI video references with MCP.
Generate, edit, and deploy immersive 3D/WebGL web projects from any MCP assistant.
Generate game-ready 3D models, textures, and audio from natural language, over MCP.
Remote MCP for RunComfy: ComfyUI deployments, hosted models, LoRA training. 31 tools.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to control the Blockout previs desktop app for AI filmmaking, allowing staging of 3D worlds, character animation, camera framing, timeline control, and viewport screenshotting through MCP tools.7Apache 2.0
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to show, animate, and control a VRM character on the desktop, including posing and motion installation via MCP tools.1MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to control a desktop virtual character (VRM) by playing animations, showing/hiding the character, and checking runtime status through the MCP protocol.768,733 npm2MIT
- AlicenseNot gradedqualityCmaintenanceEnables Windows MCP clients to author MikuMikuDance scenes by controlling characters, cameras, lighting, physics, timeline editing, MME effect assignments, and AVI video output through natural language or scripted tool calls.45MIT No Attribution