Skip to content

Architecture

mcqemu is a thin control plane. It does not emulate anything, it does not proxy guest traffic, and it does not keep VMs alive. Everything it does is arranged so that the interesting state lives in the operating system (processes, sockets, files) rather than in the server’s memory, because the server is the part most likely to be restarted at an awkward moment.

PieceWhat it isLifetime
The MCP serverA Python process speaking MCP to your client (usually over stdio)As long as your client keeps it
A VMA qemu-system-* process, daemonized, with its own pidfileUntil something stops it
The QMP socketA unix socket per VM, QEMU’s machine monitorThe VM’s lifetime
The guest-agent socketA second unix socket per VM, backing a virtio-serial portThe VM’s lifetime
qemu-imgA subprocess, spawned per image operationOne command
The registryA JSON file plus a lock file on diskPersistent

The server owns none of the VMs in any operating-system sense. It is not their parent process, it does not hold their sockets open, and if you kill it nothing happens to them. The registry is a notebook, not an authority. When a tool needs to know whether a VM is running, it does not consult the notebook, it looks at /proc and at the socket.

The two decisions that shape everything else are the process model and the session model.

VMs are launched with -daemonize and -pidfile. QEMU forks, the foreground process exits, and the VM continues as a session leader unattached to anything mcqemu owns. Restarting your editor, upgrading mcqemu, or crashing the server leaves a Debian install halfway through its partitioner exactly where it was.

This matters more than it might sound. An agent driving a VM will typically be in the middle of a long operation (an OS installation, a package build, a boot that takes two minutes under TCG emulation) and the MCP server is attached to an editor session that gets reloaded for unrelated reasons. A model where the VM dies with its parent would make those operations quietly unreliable in a way that is very hard to attribute.

-daemonize also buys a clean launch verdict. QEMU’s foreground process exits zero only after the VM is fully initialized and the QMP socket is listening, so awaiting that process gives a synchronous answer with real stderr on failure, rather than the usual “spawn it and poll for a socket” dance. When QEMU fails because a disk is locked or a machine type does not exist, the launch call returns that message directly. Daemonized QEMU routes later errors to its -D log file instead of stderr, so if the exit status is non-zero but stderr is empty, the launcher reads the last line of the log.

Liveness, and why the pidfile is not enough

Section titled “Liveness, and why the pidfile is not enough”

Reading a PID from a pidfile and calling kill(pid, 0) is the obvious liveness check and it is wrong. PIDs are recycled. A pidfile written days ago, on a host that has since rebooted or simply wrapped its PID space, can name a process that is very much alive and has nothing to do with your VM. Acting on that (sending a signal, reporting the VM as running) is a bug with real consequences.

So liveness is two checks. First, /proc/<pid>/comm must read back as something starting with qemu-system; a recycled PID belonging to a shell or a browser fails immediately. Second, for VMs mcqemu launched, /proc/<pid>/cmdline is parsed for the -name argument and compared with the registry key. That second check is deliberately generous: it returns “matches” whenever it cannot prove otherwise (unreadable cmdline, no -name present), because its job is to veto a positive identification, not to manufacture a negative one.

Attached VMs are judged differently. mcqemu did not choose their -name and may not know their PID at all, so when no PID was supplied their liveness comes from connecting to the QMP socket. A stat is not enough there either: a SIGKILLed QEMU leaves its socket file on disk, so the check opens a connection and sees whether anything accepts.

Decision two: QMP sessions are opened per call

Section titled “Decision two: QMP sessions are opened per call”

QEMU’s QMP unix socket accepts exactly one client at a time. That single fact drives the session model. If mcqemu connected once at launch and held the connection for the VM’s lifetime, it would own the monitor exclusively and everything else would be locked out: no qmp-shell for a human looking over the agent’s shoulder, no second tool, no debugging. It would also leave connection state to reconcile whenever either side restarted, which for a server whose VMs outlive it is a recurring problem rather than an edge case.

Instead every tool call opens a session, runs its commands, and disconnects. In between calls the socket is free. You can attach qmp-shell to a VM the agent is driving, poke at it, and detach, and the next tool call simply connects.

The cost is that concurrent calls against the same VM would collide, and they would collide badly: the second connect would sit there until its five second timeout, then report the VM unreachable, which reads exactly like a VM that has exited. So sessions are serialized with a lock keyed by VM name. It is a mutex table over resources rather than shared mutable state, which means two calls against two different VMs still run in parallel, and only same-VM calls queue.

That lock exists inside one server process. It cannot serialize a second mcqemu instance or your qmp-shell, and that is exactly why the connect-failure message is careful (see Reliability): when a connect fails but the process is demonstrably alive, the error says another client may hold the monitor and explicitly tells the caller not to relaunch.

Every launched VM gets a second socket wired to a virtio-serial port named org.qemu.guest_agent.0. guest_exec, guest_file_read, guest_file_write, guest_ping and guest_info all speak to that socket, never to QMP.

Keeping the channels separate is worth the extra socket. Guest-agent calls depend on software inside the guest and are therefore allowed to hang, time out, or never work at all; monitor calls control the machine from outside and must keep working when the guest is wedged. Sharing one channel would let a frozen guest interfere with the tools you use to deal with a frozen guest.

The guest agent happens to speak the QMP wire format, so the same client library works with greeting and capability negotiation switched off. The protocol’s mandatory guest-sync handshake, which echoes a random token, doubles as the probe for “is there actually an agent in there”: if the token does not come back within three seconds, the guest does not have a working qemu-guest-agent and the tools say so rather than timing out on the real command later. See Install the guest agent for the guest side.

Image work shells out to qemu-img and parses its JSON output. There is no library binding and no attempt to open qcow2 files directly.

The reason is safety rather than convenience. qemu-img participates in QEMU’s image locking protocol, so an attempt to convert or resize a disk that a running VM has open fails with a lock error instead of corrupting the image. mcqemu recognises that error and rewrites it into a sentence that names the actual problem. The registry’s own view of which disks are busy is advisory only (it does not know an attached VM’s disks and does not walk backing chains), so the image lock is treated as the authority and the registry view as a hint.

The registry is a JSON file under the state directory holding one record per VM: socket paths, pidfile, PID, the exact argv used, the launch config, log paths. It exists so that a freshly started server can find VMs that a previous one launched.

Two properties matter more than speed. It must never take the server down, and it must tolerate more than one writer.

More than one writer is not hypothetical. A second MCP client, a stray uvx mcqemu, or two clients configured against the same state directory all share one file. So every write is a read-modify-write under an exclusive flock, and the temp file used for the atomic rename carries the writing process’s PID in its name. Without the lock the last writer’s snapshot silently erases the other’s VMs; with a shared temp name two writers could interleave and publish the mixture.

Never taking the server down is the other half, and it is covered in Reliability: an unreadable registry is quarantined and the server starts empty, reporting the damage through list_vms rather than refusing to start.

VMs survive server restarts, so list_vms after a restart shows what is really running rather than an empty list. The monitor socket is available to other tooling between calls. Two agents can share a host without clobbering each other’s records, though they cannot serialize each other’s monitor access, so same-VM contention shows up as a retryable error rather than a wedge.

Paths for the state and runtime directories are documented in Configuration, and every tool named here is listed with its parameters in the Tool reference.