Skip to content

Your first virtual machine

In this tutorial we install mcqemu, create a blank disk, install Alpine Linux onto it from an installer ISO while watching the screen, and finish with a VM whose guest agent answers, so the agent can run commands inside it.

We will drive everything through an LLM agent (the examples assume Claude Code). Each step shows the tool call to ask for, and what the result should look like, so you can tell at a glance whether the step worked.

Expect this to take around twenty minutes, most of which is the operating system installer doing its own thing.

A Linux host with QEMU installed (qemu-system-x86_64 and qemu-img on your PATH), uv, and about 2 GB of free disk space. Access to /dev/kvm makes everything much faster, but the tutorial works without it under software emulation.

Download the Alpine “virt” ISO before starting, from alpinelinux.org/downloads. It is around 60 MB. We will assume it is at ~/isos/alpine-virt.iso, and we will keep our VM disk at ~/vms/alpine-agent.qcow2, so create those directories now:

Terminal window
mkdir -p ~/isos ~/vms
  1. Check that the server starts. uvx fetches mcqemu and runs it; the version banner appears on stderr and then it waits for a client, which is what an MCP server is supposed to do.

    Terminal window
    uvx mcqemu

    Press Ctrl-C to stop it. Seeing mcqemu v... is all we needed.

  2. Register it with Claude Code:

    Terminal window
    claude mcp add mcqemu -- uvx mcqemu
  3. Start Claude Code in any directory and run /mcp. The list should include mcqemu as connected. If it does not, Install and connect covers the usual causes.

  4. Ask the agent to call list_vms. On a fresh install it answers with an empty list:

    {"vms": [], "registry_warnings": []}

    An empty registry_warnings means the bookkeeping is healthy. We now have a working server with nothing to manage yet.

  1. Ask the agent to create the disk:

    image_create(path="~/vms/alpine-agent.qcow2", size="8G")

    The result reports format: qcow2 and a virtual size of 8 GB. Check the file on the host and you will see it occupies only a couple of hundred kilobytes. qcow2 images grow as the guest writes, so a generous virtual size costs almost nothing up front.

  2. Launch the VM with the installer ISO attached:

    launch_vm(name="alpine", disks=["~/vms/alpine-agent.qcow2"],
    iso="~/isos/alpine-virt.iso", memory_mb=2048)

    The result tells us the VM is running, which acceleration it got (kvm or tcg), the process ID, and paths to its logs. When a VM has both a disk and an ISO, it boots the ISO this once and the disk on every later boot, so we will not have to detach anything by hand.

  3. Give it fifteen seconds, then look at the screen:

    vm_screenshot(name="alpine")

    The agent gets a PNG of the guest display back. You should see Alpine’s boot messages or a localhost login: prompt. Nothing is installed inside the guest to make this work; QEMU is handing over the framebuffer directly, which is why screenshots work in bootloaders and BIOS menus too.

    If the screen still shows a bootloader, wait and take another screenshot. Watching, rather than assuming, is the habit this whole workflow is built on.

The installer is an interactive console session, so we type into it the same way a person at a keyboard would.

  1. Log in as root (Alpine’s live image has no root password):

    vm_type_text(name="alpine", text="root", enter=true)

    Take a screenshot to confirm we landed at a shell prompt before continuing.

  2. Start the installer:

    vm_type_text(name="alpine", text="setup-alpine", enter=true)
  3. Work through the questions with vm_type_text, taking a screenshot after each answer to see what is being asked next. Most answers can be the default (press Enter with vm_send_keys(name="alpine", keys=["ret"])). The three that matter:

    • Set a root password when asked, and remember it. We need it to log in later.
    • When asked which disk to use, answer vda. Virtio disks appear under that name inside the guest.
    • When asked how to use it, answer sys. That installs to the disk rather than running from RAM.

    Alpine asks for a final y to erase the disk. Nothing else on your machine is at risk; the only disk the VM can see is the image file we created.

  4. When the installer reports it is done, shut the guest down cleanly:

    stop_vm(name="alpine")

    stop_vm presses the virtual power button and waits for the guest to shut itself down, so filesystems get flushed properly.

  1. Launch again, this time with no ISO, and forward a host port to the guest’s SSH port so we have a way in later:

    launch_vm(name="alpine", disks=["~/vms/alpine-agent.qcow2"],
    port_forwards=["auto:22"])

    auto:22 means “pick any free host port”; the result reports which one it chose, for example port_forwards: ["43617:22"]. You can also ask for a specific one with "2222:22", and mcqemu checks up front that the port is free instead of letting QEMU fail obscurely.

  2. Screenshot after a few seconds. This time the login prompt comes from the installed system on the disk, not from the ISO.

  3. Log in with root and the password you set:

    vm_type_text(name="alpine", text="root", enter=true)
    vm_type_text(name="alpine", text="<your password>", enter=true)

Everything so far worked through the screen and keyboard, with no cooperation from the guest operating system. The guest_* tools are different: they need qemu-guest-agent running inside the guest. mcqemu wires up the host side of that channel on every launch, so installing the package is the only step left.

  1. Confirm the agent is genuinely missing, so the difference is visible:

    guest_ping(name="alpine")

    This fails, and the error says the guest does not have the agent installed or running.

  2. Type these three commands into the guest console, one at a time, with vm_type_text(..., enter=true):

    apk add qemu-guest-agent
    rc-update add qemu-guest-agent default
    rc-service qemu-guest-agent start

    The VM has outbound network access (we did not restrict it), so apk can reach Alpine’s mirrors. Take a screenshot after the first command to check the download succeeded; if apk cannot find the package, run setup-apkrepos -c -1 to enable the community repository and try again.

  3. Ask again:

    guest_ping(name="alpine")

    Now it answers guest_agent: responding.

  4. Run a command inside the guest without touching the keyboard:

    guest_exec(name="alpine", command="uname", args=["-a"])

    The result carries the guest’s stdout, stderr, and exit code. Note that command is an executable and args are its arguments; this is exec, not a shell. For pipelines, use command="/bin/sh" with args=["-c", "your | pipeline"].

  5. Shut the VM down. We want the image quiescent for the next tutorial.

    stop_vm(name="alpine")

~/vms/alpine-agent.qcow2 is now an installed Alpine system with a working guest agent. Along the way we saw the two ways to interact with a VM:

  • Through the display and keyboard (vm_screenshot, vm_type_text, vm_send_keys), which works on any guest at any stage of boot, including installers and bootloaders, because it operates below the operating system.
  • Through the guest agent (guest_ping, guest_exec), which is faster and gives you structured output, but only after you have put the agent inside the guest.

We also saw that disks and VMs are separate things: image_create makes a disk, launch_vm runs a VM around it, and the same disk can be booted again later with different settings.

Keep that image. In Disposable sandboxes we use it as a base to spin up throwaway clones in seconds, run untrusted things in them, and delete them without the base ever changing.

If you want to install something bigger than Alpine next, read Drive an installer for the screenshot and input loop in detail, including mouse control for graphical installers.