Skip to main content
Glama
sonthicse

VMware Workstation Pro MCP Server

by sonthicse
README.md
# VMware Workstation Pro MCP Server

A local MCP server (stdio) that lets Claude drive **VMware Workstation Pro 26** on Windows:
list VMs, read configuration, power, snapshots, clone, guest operations, networking,
and screenshots.

Built against **Workstation 26.0.0**, Python 3.14, `mcp` 2.0.

---

## Install

```powershell
git clone <this repo>
cd vmware-mcp-server
uv sync
```

### Claude Code (with `uv`)

```powershell
claude mcp add vmware -s user -- `
  C:\Users\<you>\.local\bin\uv.exe `
  --directory C:\path\to\vmware-mcp-server `
  run -m vmware_mcp
```

Two details matter:

- Use the **absolute path to `uv.exe`**. The client spawns the server directly and does not
  guarantee your shell's `PATH`.
- Pass **`--directory` with an absolute path**. A stdio server's working directory is chosen
  by the client and can be anywhere.

Then `/mcp` in Claude Code to check the connection.

### Claude Code (manual install, no `uv`)

`uv` is not required — this is an ordinary Python package (`requires-python = ">=3.14"`).
Any Python environment with the dependencies installed works:

```powershell
git clone <this repo>
cd vmware-mcp-server
python -m venv .venv
.venv\Scripts\pip install -e .
```

Then register the venv's interpreter instead of `uv.exe`:

```powershell
claude mcp add vmware -s user -- `
  C:\path\to\vmware-mcp-server\.venv\Scripts\python.exe `
  -m vmware_mcp
```

`claude mcp add` is itself just a convenience that writes the server entry into Claude
Code's config (`~/.claude.json` for `-s user`, or `.mcp.json` for `-s project`). Editing
that JSON by hand under `mcpServers.vmware` — `command`, `args`, `env` — has the identical
effect; `claude mcp add` is not required either.

### Claude Desktop

This is a standard stdio MCP server, so any MCP-compatible client can run it — including
Claude Desktop. Add an entry to its config file:

```
%APPDATA%\Claude\claude_desktop_config.json
```

```json
{
  "mcpServers": {
    "vmware": {
      "command": "C:\\Users\\<you>\\.local\\bin\\uv.exe",
      "args": [
        "--directory", "C:\\path\\to\\vmware-mcp-server",
        "run", "-m", "vmware_mcp"
      ],
      "env": {}
    }
  }
}
```

Swap `command`/`args` for the venv's `python.exe -m vmware_mcp` if installing without `uv`,
same as above. Restart Claude Desktop after editing the file so it reloads the server list.

## Configuration

All optional; the defaults suit a standard install. These are plain environment variables
read once at server startup (`config.py:load_settings`) — there is no separate config file
of the server's own, so they must be set wherever the MCP **client** launches the server
process, then the server (and the client) restarted for a change to take effect:

- **Claude Code, user scope** (`claude mcp add ... -s user`): edit the `env` object under
  `mcpServers.vmware` in `~/.claude.json` (`C:\Users\<you>\.claude.json`), e.g.
  `"env": { "VMWARE_MCP_ALLOW_DELETE_VM": "1" }`. Equivalently, pass `-e VAR=value` flags
  when running `claude mcp add`.
- **Claude Code, project scope** (`-s project`): same `env` object, in the project's
  `.mcp.json` instead.
- **Claude Desktop**: the `env` object under `mcpServers.vmware` in
  `%APPDATA%\Claude\claude_desktop_config.json`.
- **Manual/other clients**: set the variable in whatever environment spawns the process
  (a wrapper script, the OS-level user/system environment variables, etc.) — the server
  just reads `os.environ`.

| Variable | Default | Purpose |
|---|---|---|
| `VMWARE_MCP_VMWARE_DIR` | `C:\Program Files\VMware\VMware Workstation` | Where `vmcli.exe` and `vmrun.exe` live |
| `VMWARE_MCP_INVENTORY` | `%APPDATA%\VMware\inventory.vmls` | Workstation's VM library file |
| `VMWARE_MCP_VM_ROOTS` | common ancestor of registered VMs | `;`-separated allowlist of directories the server may touch |
| `VMWARE_MCP_TIMEOUT` | `120` | Seconds before a VMware command is killed |
| `VMWARE_MCP_MAX_OUTPUT` | `100000` | Byte cap on guest output |
| `VMWARE_MCP_ALLOW_DELETE_VM` | unset | Set to `1` to enable `delete_vm` |
| `VMWARE_MCP_CREDENTIALS` | `%APPDATA%\vmware-mcp\credentials.toml` | Guest credentials file |
| `VMWARE_MCP_GUEST_USER` / `_PASS` | unset | Fallback guest credentials |

### Guest credentials

Guest username and password are **never tool arguments**, so they never enter the model's
transcript. Put them in `%APPDATA%\vmware-mcp\credentials.toml`:

```toml
[default]
username = "user"
password = "secret"

[vm.ubuntu-server-26-04-lts]
username = "ubuntu"
password = "ubuntu"
```

Keys under `[vm.*]` are the `vm_id` values from `list_vms`.

## Tools

31 tools, grouped by risk. Annotations are per-tool, which is why operations of different
destructiveness are separate tools rather than one tool with an `action` switch.

Refer to VMs by the `vm_id` from `list_vms`; display names and absolute `.vmx` paths also
work. Paths outside the allowlist are rejected with a JSON-RPC `-32602`.

### Read-only

| Tool | What it does |
|---|---|
| `list_vms` | List every VM registered in VMware Workstation's inventory, running or stopped, plus any unregistered VM that happens to be running |
| `get_vm_info` | Full configuration of one VM: CPU, memory, disks, NICs, shared folders, display, USB/sound/floppy, guest isolation, tools, power state |
| `list_snapshots` | List a VM's snapshots with the `uid` needed to revert or delete them |
| `get_vm_ip` | Read the IP address a running VM's guest has been assigned |
| `capture_screenshot` | Capture what is currently on a running VM's screen |
| `guest_inspect` | Read directory contents, running processes, or environment inside a guest |

### Write

| Tool | What it does |
|---|---|
| `vm_power` | Start, stop, reset, suspend, pause, or unpause a VM |
| `create_snapshot` | Take a snapshot of a VM |
| `clone_vm` | Copy a VM to a new `.vmx` path — linked (fast, shares parent disks) or full (independent copy) |
| `create_vm` | Create a new VM, either blank or deployed from a `.vmtx` template |
| `register_vm` | Add an existing `.vmx` to the Workstation library, so `list_vms` sees it — Workstation must be closed |
| `unregister_vm` | Remove a VM from the library without deleting any files — Workstation must be closed |
| `set_vm_hardware` | Change a VM's vCPU count, cores per socket, and memory size |
| `manage_disk` | Create a standalone `.vmdk` file, or extend a disk already attached |
| `attach_disk` / `detach_disk` | Wire a `.vmdk` into a VM as a SATA or NVMe device, or unwire it |
| `set_network_adapter` | Change a NIC's connection type and plug or unplug its virtual cable |
| `set_display` | 3D acceleration, monitor count, video memory, guest resolution |
| `set_vm_option` | Toggle USB / sound / floppy / serial / parallel and guest-isolation settings |
| `manage_shared_folder` | Add or remove a share, and turn shared-folder sync on and off (read them from `get_vm_info`) |
| `set_device_connection` | Connect or disconnect a device, live and at power on |
| `send_keystrokes` | Type into a VM's console as if at the keyboard — needs no VMware Tools or credentials, but does need host privileges |
| `guest_run` | Run a program or a script inside a guest OS |
| `guest_copy_file` | Copy a single file between the host and a guest OS |
| `install_tools` | Mount the VMware Tools installer in a running VM (install still finishes inside the guest) |
| `manage_port_forwarding` | Inspect host virtual networks and manage NAT port forwarding rules |

### Destructive

| Tool | What it does |
|---|---|
| `revert_snapshot` | Restore a VM to a snapshot, discarding all changes made since it was taken |
| `delete_snapshot` | Remove a snapshot (VM must be powered off or suspended) |
| `guest_modify_path` | Create a directory, rename a file, or delete a file/directory inside a guest |
| `guest_kill_process` | Stop a process inside a guest OS by PID |
| `delete_vm` | Permanently delete a VM and all of its files from disk, sweeping up what `vmrun` leaves behind — cannot be undone; disabled unless `VMWARE_MCP_ALLOW_DELETE_VM=1` |

To take a VM out of the library but keep its files, use `unregister_vm`; that is Workstation's
*Remove from Library*, where `delete_vm` is *Delete from Disk*.

Everything under **Write** and **Destructive** needing guest access additionally requires
VMware Tools running in the guest and credentials configured in `credentials.toml` (see
above) — except `send_keystrokes`, which needs neither.

## Things worth knowing

These were all found by testing against a real Workstation 26 install, and each one is a
silent failure if you get it wrong.

**Snapshots are addressed by UID, not name.** `vmcli Snapshot Take` takes a *name*, but
`Revert` and `Delete` take a *uid*. `-n` on `Take`/`Revert` is the boolean `--native` flag,
not "name". `list_snapshots` therefore always returns `uid`; the tools accept a name and
resolve it, refusing ambiguous ones.

**`PowerState: off` is not a boolean.** PyYAML implements YAML 1.1, where `off`/`on`/
`yes`/`no` parse as booleans — so a stock `yaml.safe_load` silently turns the power state
into `False`. `yamlfix.py` restricts the bool resolver to `true`/`false`.

**vmcli output is not always valid YAML.** `HGFS query` prints a plain sentence before the
mapping. The parser splits that preamble off and keeps it.

**vmcli can fail while exiting 0.** `vmcli Power Start` requires administrator rights or
`__vmware__` group membership; without them it prints a refusal to *stdout* and exits 0.
All power actions therefore run through `vmrun`. Control commands on both CLIs are sniffed
for known failure phrasing — guest output is exempt, since a guest may legitimately print
the word "error".

**Host-level operations need Administrator.** Elevation is not only a `vmcli Power Start`
problem, and it is never reported as one. Run the server as Administrator, or as a member of
the `__vmware__` group, if you need:

| Operation | How it fails without privileges |
|---|---|
| `send_keystrokes` | `Insufficient permissions in the host operating system` |
| `manage_port_forwarding` (`set`/`delete`) | `Failed to refresh NAT configuration` — even with VMware NAT Service running |
| `vm_power` on some hosts | `User must be a member of the __vmware__ group` |

Reading is unaffected; `list_networks`, `list`, and every query tool work unprivileged.
Failures matching these patterns come back with an explanation appended, rather than as the
CLI's original wording alone.

**`vmcli Ethernet ConnectionControl` crashes vmcli.exe** — `0xC0000409`, for every argument
arrangement, on Workstation 26. It is not wrapped. `set_network_adapter` unplugs a cable
through `Ethernet SetStartConnected` plus `vmrun connectNamedDevice` instead.

**A device has two connection states.** `connectionStatus` is live and `startConnected` is
what the `.vmx` says; `get_vm_info` reports them as `connected` and `start_connected`. A
stopped VM shows everything as not connected, which is not a failed write — check
`start_connected` there.

**`vmcli VM Create` makes an unusable VM.** It writes `scsi0:0.present` into the `.vmx` but
never creates the `scsi0` controller, so the disk it just made is not attached, and it adds
no network adapter. vmcli has no way to create a SCSI controller — use `attach_disk`, which
puts the disk on SATA or NVMe.

**Neither CLI can register a VM.** `vmrun` has no `register` on Workstation and `vmcli VM`
only has `Create`, so `clone_vm` and `create_vm` produce VMs the library knows nothing about.
`register_vm` edits `inventory.vmls` directly, which is why it refuses while Workstation is
running: Workstation keeps the library in memory and rewrites it on exit.

**`install_tools` is Windows-only now.** Workstation 26 ships no `linux.iso`. Linux guests
install `open-vm-tools` from their own package manager.

**The two CLIs report errors on different streams.** vmcli writes to stderr, vmrun to
stdout. Both are captured.

**vmcli flags come before positional arguments,** and it consumes single-letter flags such
as `-c` itself, so `Guest run /bin/sh -c "..."` does not work. Use `guest_run` in `script`
mode for shell syntax.

**`guest_run` does not return the command's output.** VMware's guest API never pipes guest
stdout back to the host: program mode returns a PID, script mode returns an empty string.
To read output, redirect it to a file in the guest and fetch it with `guest_copy_file`.

**Guest passwords are visible in the host process list** while a guest command runs. Neither
vmcli nor vmrun accepts a password on stdin, so this cannot be fixed here.

**`send_keystrokes` needs neither VMware Tools nor credentials,** which makes it the only
way to drive a VM sitting at an installer, bootloader, or login prompt. It does need host
privileges (see above), and it cannot type a newline: `vmrun` rejects an embedded `\n` as an
invalid parameter. Send the text, then call again with `mode="sequence"` and `keys="enter"`,
which goes through `vmcli MKS sendKeySequence` — also the way to send `tab`, `esc`, arrow
keys, or `ctrl-alt-delete`.

## Development

```powershell
uv run pytest                              # parser and policy tests, no VM needed
uv run mcp dev src/vmware_mcp/__main__.py  # MCP Inspector
```

Fixtures under `tests/fixtures/` are real `vmcli ... query` output captured from
Workstation 26. When adding a wrapper, **read `vmcli <Module> <Command> --help` on the
installed build first** — the flags changed between 17.x and 26, and stale flags fail in
ways that look like something else.