Registering DeskVNC with Cursor
Cursor is the AI code editor that reads a project-scoped MCP config. Pairing it with DeskVNC's dvv gives the agent a real keyboard and a real mouse on a remote desktop, with the same VNC, RDP, or SSH protocol the remote machine already speaks. Nothing is installed on the remote target, and the user can take the wheel back with one click. The DeskVNC documentation names .cursor/mcp.json as the file Cursor reads, and gives the mcpServers block shape that dvv answers.
Prerequisite: the dvv binary
dvv is the executable that ships inside DeskVNCViewer. Install the viewer, open it once, and save a machine with its password so the agent has something concrete to reach. The binary lives next to the application:
- macOS:
/Applications/DeskVNCViewer.app/Contents/MacOS/dvv - Windows:
%LOCALAPPDATA%\DeskVNCViewer\dvv.exefor a per-user install, orC:\Program Files\DeskVNCViewer\dvv.exefor the all-users install - Linux:
/usr/bin/dvvon the standard.deb. The AppImage copiesdvvto~/.local/share/DeskVNCViewer/bin/dvv
The path you point Cursor at is the path the viewer installed.
The registration step
Cursor reads .cursor/mcp.json at the project root. The documented shape is a top-level mcpServers map whose value is the command plus args to spawn. On macOS:
{
"mcpServers": {
"deskvnc": {
"command": "/Applications/DeskVNCViewer.app/Contents/MacOS/dvv",
"args": ["mcp", "--stdio"]
}
}
}On Linux, swap the command path to /usr/bin/dvv. On Windows, use a path Cursor can resolve, ideally with the binary's directory on PATH so the entry stays portable across machines:
{
"mcpServers": {
"deskvnc": {
"command": "C:/Program Files/DeskVNCViewer/dvv.exe",
"args": ["mcp", "--stdio"]
}
}
}The top-level key here is mcpServers. That is the key the DeskVNC integration notes name for the .cursor/mcp.json path, and it matches the standard MCP shape Cursor reads. If your Cursor version expects a different top-level key, check the current Cursor MCP docs and use the equivalent block.
The shell form is dvv setup. It writes the same MCP entries, copies the skill into the skills directory Cursor reads, and points the agent at the right dvv. From a Cursor chat, /mcp then shows deskvnc as connected.
For the HTTP transport on any platform, replace command and args with a url and a headers block that carries the bearer token:
{
"mcpServers": {
"deskvnc": {
"url": "http://127.0.0.1:7333/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}A first agent session
Open a project that contains .cursor/mcp.json, then ask the agent to do something on a saved machine:
You: Open "build-server" and confirm the last Jenkins build status.The agent drives the four-call loop. Behind the scenes, Cursor forwards each MCP call to the dvv subprocess:
dvv_hosts {}
dvv_open {"hostId": "build-server", "perceive": true}
dvv_control {"limbId": "limb-c81a", "action": "acquire"}
dvv_screen {"limbId": "limb-c81a", "form": "full", "scale": 0.25}The screenshot comes back with a geometry_generation of, say, 23. The agent reads the picture, finds the Jenkins status pill, and acts:
dvv_click {"limbId": "limb-c81a", "x": 880, "y": 142, "generation": 23}
dvv_screen {"limbId": "limb-c81a", "form": "damage-crop"}The follow-up screenshot confirms the click landed on the status pill, and the agent reports the colour and the text. The whole observe-then-act cycle stays well under a second, so a real human workflow feels immediate.
The coordinate generation fence
Every dvv_screen carries two fences: a geometry_generation and a content_generation. Clicks must echo the geometry generation they were computed against. A click computed against a screen that has since resized is refused rather than landing in the wrong place. Typing and keys are fenced by the content generation, and dvv_type plus dvv_key are refused with SCREEN_CHANGED when something window-sized has repainted since the last dvv_screen. This is the fence that stops an agent from typing into a dialog that appeared over the editor the agent thought it was driving. One dvv_screen clears the fence, dvv_status does not (it reads no pixels), and there is no override. Terminal limbs are not fenced, because a PTY echoes what it is sent into a stream the agent reads back.
Human takeover
The person at the remote machine can take control back at any moment. The viewer shows an "agent driving" badge while the agent holds the lease, and one click in the viewer hands the desktop back to the person. The plane releases every held key and every held button the moment a person takes over, so a half-finished drag cannot strand the desktop. The recipient can revoke the agent's control or end the session outright. This is attended automation by design: the agent never has a stronger position than the person at the keyboard.
Troubleshooting
| Symptom | Likely cause | Try this |
|---|---|---|
LIMB_GONE from any tool |
The viewer closed the connection or the last dvv_close detached the limb |
Run dvv limbs to list live limbs, then dvv open <host> again |
SCREEN_CHANGED on dvv_type or dvv_key |
A window-sized region repainted since the last dvv_screen |
Call dvv_screen once, then retry the action |
Cursor does not list deskvnc in the MCP panel |
JSON parse error, wrong file path, or wrong top-level key | Validate the JSON, confirm the path is .cursor/mcp.json, and check the current Cursor docs for the expected key name |
dvv exits immediately on spawn |
The path in command is wrong or the file is not executable |
Run the command value in a shell and confirm dvv mcp --stdio starts |
| Click lands in the wrong place | Stale geometry_generation after a resize |
Take a fresh dvv_screen, then send the new generation with the click |
Links
- Project: <https://github.com/psmux/DeskVNC>
- Hub: <https://deskvnc-hub.pages.dev/>
- Raw agent skill: <https://github.com/psmux/DeskVNC/blob/main/skills/deskvnc/SKILL.md>
- Integration notes for every client: <https://github.com/psmux/DeskVNC/blob/main/docs/AGENTS.md>