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.
The pieces
Section titled “The pieces”| Piece | What it is | Lifetime |
|---|---|---|
| The MCP server | A Python process speaking MCP to your client (usually over stdio) | As long as your client keeps it |
| A VM | A qemu-system-* process, daemonized, with its own pidfile | Until something stops it |
| The QMP socket | A unix socket per VM, QEMU’s machine monitor | The VM’s lifetime |
| The guest-agent socket | A second unix socket per VM, backing a virtio-serial port | The VM’s lifetime |
qemu-img | A subprocess, spawned per image operation | One command |
| The registry | A JSON file plus a lock file on disk | Persistent |
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.
Decision one: VMs outlive the server
Section titled “Decision one: VMs outlive the server”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.
The guest agent is a separate channel
Section titled “The guest agent is a separate channel”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.
Images are qemu-img subprocesses
Section titled “Images are qemu-img subprocesses”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 bookkeeping, not truth
Section titled “The registry is bookkeeping, not truth”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.
What this means in practice
Section titled “What this means in practice”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.