QMP-MCP

Crear, ejecutar y gestionar máquinas virtuales qemu

Documentación

qmp-mcp

CI

qmp-mcp es un servidor de Model Context Protocol (MCP) que le da a un agente de IA los controles de una única máquina virtual QEMU. El agente describe el hardware que quiere; el servidor construye esa máquina, la arranca y expone un conjunto de herramientas para manejarla: pausarla y reanudarla, reiniciarla, ver su pantalla, enviarle comandos QEMU de bajo nivel, reaccionar a sus eventos y apagarla cuando haya terminado.

Todo el diseño se apoya en una idea: las herramientas son el límite. El agente nunca entrega argumentos crudos a QEMU ni accede a tu sistema de archivos. Completa una descripción estructurada y validada de la máquina; el servidor la convierte en una línea de comandos QEMU restringida y media cada solicitud. Todo lo que el agente puede tocar — imágenes de disco, medios de arranque, los comandos que puede ejecutar contra la VM en vivo, los puertos que puede abrir — pasa por listas de permitidos que tú controlas. La VM es el radio de explosión, y las herramientas son las paredes.

Se distribuye como dos implementaciones intercambiables — una en TypeScript, otra en Rust — que se comportan de manera idéntica. Esta página explica qué es el servidor y cómo piensa; los README de cada implementación cubren la instalación, ejecución y despliegue de cada una.

¿Nuevo en el vocabulario? CONTEXT.md es el glosario de una página. Las palabras siguientes — Instancia, Invitado, Especificación de Hardware, Política de Comandos, Almacén de Imágenes, Visor — tienen cada una un significado específico, y este README las usa deliberadamente.

Cómo funciona

Una máquina a la vez: la Instancia

El servidor gestiona exactamente una Instancia — el proceso qemu-system-* en ejecución junto con su configuración de hardware y la conexión de control en vivo hacia él. Nunca hay más de una; pedir crear otra mientras existe una es rechazado. La vida de una Instancia está ligada a la del servidor: apaga el servidor y este derriba la VM con él, de modo que nada queda huérfano.

Una Instancia recorre un pequeño ciclo de vida — de nada, a iniciando, a en ejecución, opcionalmente pausada y de vuelta, a detenida, y de vuelta a nada:

NONE → STARTING → RUNNING ⇄ PAUSED → STOPPED → NONE

Si el proceso QEMU subyacente sale por sí solo — un apagado del invitado, un fallo, un kill externo — el servidor lo nota y reconcilia de vuelta a NONE, de modo que la siguiente solicitud parte de cero.

Lo que se ejecuta dentro de la Instancia — el sistema operativo o la carga de trabajo — es el Invitado. El servidor gestiona la máquina; lo que instales y ejecutes en ella depende de ti y de tu agente.

Describiendo la máquina: la Especificación de Hardware

El agente no ejecuta QEMU. Envía una Especificación de Hardware — una descripción estructurada y validada de la máquina que quiere: tipo de máquina y CPU, cuántas vCPUs y cuánta memoria, qué discos y medios de arranque, la red, la pantalla, el acelerador. El servidor valida cada campo y genera la línea de comandos QEMU a partir de ella. El agente nunca proporciona argv crudo.

Una especificación es solo los argumentos JSON para create_instance:

{
  "machine": "q35",
  "cpu": "host",
  "vcpus": 2,
  "memoryMb": 2048,
  "accel": "auto",
  "disks": [{ "image": "root.qcow2" }],
  "cdrom": { "iso": "debian-13.iso" },
  "boot": "dc",
  "display": "vnc"
}

La validación no es una formalidad — es el límite de seguridad. Los campos se verifican por rango y caracteres, y cualquier cosa que pudiera colar una opción extra en la línea de comandos (una coma suelta en una entrada de disco, por ejemplo) se escapa o se rechaza. Los tamaños están limitados, con techos que tú estableces para disco, memoria y vCPUs. Si una especificación es inválida, create_instance falla antes de que QEMU se lance, con un mensaje que dice exactamente qué estaba mal.

Hay una vía de escape — extraArgs, que añade banderas QEMU crudas a la línea de comandos generada — pero está desactivada a menos que la habilites explícitamente. Está pensada para entornos confiables de un solo inquilino donde hayas decidido que el agente puede recibir las llaves.

Qué arquitectura emulas se deduce de la machine: el servidor elige el emulador por ti — q35/pc lanzan qemu-system-x86_64, mientras que virt y las placas raspi* lanzan qemu-system-aarch64 — así que cambiar de arquitectura es solo una machine diferente, sin reiniciar. QMP_MCP_QEMU_BINARY anula esa elección para cada Instancia (por ejemplo, una compilación personalizada o qemu-system-riscv64), y accel: auto solo usa KVM cuando la arquitectura invitada coincide con la del host, cayendo a TCG entre arquitecturas (ADR-0013).

Algunas máquinas no arrancan desde un disco en absoluto. Las placas Raspberry Pi de QEMU (raspi3b y amigas) tienen hardware fijo — una CPU, número de núcleos y RAM determinados — y esperan que el kernel se les entregue directamente en lugar de leerse desde un cargador de arranque de tarjeta SD. Para esas, la especificación añade tres campos opcionales: kernel y dtb (una imagen de kernel y un blob de árbol de dispositivos, cada uno un nombre en el Almacén de Imágenes) y appendCmdline (la línea de comandos del kernel). El servidor emite -kernel/-dtb/-append y, como el hardware de la placa es fijo, omite -cpu/-smp/-m; adjunta la imagen SD con "interface": "sd" (dimensionada a una potencia de dos, o QEMU la rechaza). Estas placas tampoco tienen bus PCI, así que la NIC predeterminada no puede conectarse — elige network.model usb-net (su NIC USB) o network.mode none; el servidor rechaza una NIC no conectable de antemano en lugar de dejar que QEMU aborte. Nada de esto es exclusivo de Pi — cualquier arranque directo de kernel (una máquina virt desnuda, por ejemplo) puede usar kernel/appendCmdline junto con la configuración habitual de CPU y memoria.

Qué tan rápido corre: el acelerador

accel: "auto" (el predeterminado) usa KVM por hardware cuando el host puede alcanzar un /dev/kvm, y de lo contrario cae a emulación por software TCG — informando cuál eligió. Pide kvm explícitamente y falla ruidosamente si KVM no está disponible; pide tcg y siempre obtienes emulación portátil y sin privilegios. KVM nunca es obligatorio — es una mejora de rendimiento en la que optas, no un privilegio que el servidor exija.

Manejando la VM en ejecución: la Sesión QMP

Una vez que una Instancia está activa, el servidor habla con ella a través de la Sesión QMP — el propio Protocolo de Máquina de QEMU, un canal de control JSON en un socket privado que el servidor posee y nunca expone en la red. El servidor negocia la sesión al lanzar (lee el saludo, envía qmp_capabilities), y desde entonces cada herramienta de "manejar la VM" es un comando QMP por debajo: pause_instance detiene las CPUs, get_status pregunta a QEMU su estado de ejecución, screendump toma una captura del framebuffer, y así sucesivamente.

Para cualquier cosa sin una herramienta dedicada, está qmp_execute — un "ejecuta este comando QMP" genérico — lo que nos lleva a la salvaguarda sobre él.

Lo que el agente puede ordenar: la Política de Comandos

qmp_execute podría en principio ejecutar cualquier comando QMP, lo cual es a la vez poderoso y peligroso. La Política de Comandos decide cuáles realmente pasan. De fábrica es una lista de permitidos segura por defecto; comandos genuinamente peligrosos — migrate, dump-guest-memory, human-monitor-command, y sus semejantes — están detrás de una lista de denegados dura que no se puede reactivar. Puedes ampliar o reducir el término medio con una variable de entorno o un archivo de política.

Una sutileza: la política filtra comandos por nombre, no por sus argumentos. Así que un comando cuyos argumentos podrían ser peligrosos — una captura de pantalla que escribe en un archivo del host, por ejemplo — no se expone a través de la herramienta genérica en absoluto. Recibe una herramienta dedicada que valida los argumentos por ti.

Dónde viven los archivos: el Almacén de Imágenes y el Almacén de ISOs

El agente se refiere a discos y medios de arranque por nombre, nunca por ruta del host — y esos nombres se resuelven dentro de dos carpetas que tú designas:

  • El Almacén de Imágenes es un único directorio de lectura-escritura para imágenes de disco invitadas. El agente puede listar lo que hay y crear nuevas imágenes en blanco en él, y los discos en una especificación se buscan por nombre dentro de él.
  • El Almacén de ISOs es un directorio separado de solo lectura para ISOs de instalación y arranque. Mantenerlo distinto significa que los medios de instalación nunca pueden ser escritos.

Ambos se aplican con contención de ruta real: un nombre que intente salirse — ../, una ruta absoluta, un enlace simbólico que apunte a otro lugar — es rechazado. Estas dos carpetas son la vista del agente del sistema de archivos — la única excepción es un compartido de carpeta virtio-9p opcional que el operador puede habilitar (QMP_MCP_HOST_SHARE_DIR), al que una especificación se adhiere con share: true; también está configurado por el operador (el agente nunca nombra la ruta del host) y es de solo lectura por defecto (ADR-0014).

Red en sandbox

Los invitados obtienen red en modo usuario por defecto — una pila NAT en sandbox, sin privilegios del host, sin puente. Para alcanzar un servicio dentro del invitado añades reenvíos de host, y esos están acotados: solo puertos en un rango sin privilegios, vinculados a loopback.

{ "network": { "hostForwards": [{ "hostPort": 2222, "guestPort": 22 }] } }

La red a nivel de host (tap/bridge) existe pero está bloqueada a menos que la actives — necesita privilegios que no encajan con la postura sin privilegios del servidor.

Viendo qué pasa: eventos, la Pantalla, el Visor — y grabación

Tres formas de ver qué está haciendo la VM:

  • Eventos. QEMU emite eventos asíncronos — un reinicio, un apagado, un cambio de dispositivo. El servidor mantiene un búfer circular acotado de los recientes para la Instancia actual, y el agente lo lee en modo pull: get_events drena lo nuevo desde un cursor, wait_for_event bloquea hasta que un evento nombrado llega (o expira). Sin manguera de incendios que gestionar.
  • La Pantalla y el Visor. Pide una Pantalla vnc en la especificación y QEMU expone la pantalla del invitado a través de VNC, solo en loopback. Activa el Visor — un puente noVNC opcional dentro del proceso — y puedes ver y controlar esa pantalla en un navegador. El Visor está protegido por contraseña y lee la Pantalla solo; nunca toca la Sesión QMP. Es ideal para vigilar un instalador de SO, o simplemente para ver lo que el agente ve. La mayoría de las máquinas (virt, q35, …) no tienen pantalla integrada, así que combina display: vnc con una displayDevice — virtio-gpu (una GPU real con DRM, para que los escritorios Wayland/X rendericen), vga, o ramfb. Usa vga para una ISO en vivo o cualquier arranque donde el menú de arranque / consola temprana deba ser visible: virtio-gpu no muestra nada hasta que el invitado carga su controlador DRM, así que el cargador de arranque de una ISO no puede dibujar en él. Las placas raspi* renderizan a través de su framebuffer integrado, así que permanecen displayDevice: none. (Arrancar una distro de esta manera también requiere initrd junto con kernel — el kernel habitual + initramfs + rootfs.)
  • Grabación. Con una Pantalla vnc activa, start_recording captura la pantalla a un archivo de video: un bucle de capturas de pantalla QMP canalizadas a un ffmpeg co-ubicado, codificado para contenido de pantalla a una tasa de fotogramas limitada con CRF de calidad constante (ADR-0017). Está limitado por capacidad, no por permiso — grabar es captura, nada se escribe en el Invitado — y está disponible solo cuando ffmpeg es ejecutable y has establecido QMP_MCP_RECORDING_DIR. El agente solo nombra la salida; el archivo aterriza como <name>.mkv bajo esa raíz propiedad del operador, nunca en una ruta del host, y los videos no se devuelven en línea — los recoges del host. stop_recording finaliza el archivo; get_recording informa si la grabación está disponible (y exactamente por qué no cuando no lo está) más el códec activo, CRF, límite de fps y formato de píxeles. Las imágenes Docker incluyen ffmpeg, así que la grabación funciona de fábrica en contenedor; en metal desnudo es una instalación opcional.

Hablando con el servidor: transportes y autenticación

El servidor habla MCP sobre stdio (el predeterminado — así es como la mayoría de los clientes lanzan un servidor directamente; sin red, sin autenticación) o sobre HTTP (para un despliegue en red), o ambos a la vez. El transporte HTTP es fail-closed: se niega a iniciar sin autenticación — una clave API, o un token HS256 firmado — a menos que optes explícitamente por el modo inseguro para uso local. Un servidor que puede construir y ejecutar VMs no tiene por qué ser accesible sin autenticación. Se ejecuta como un usuario no root en todos los modos y nunca necesita --privileged.

Las herramientas

El vocabulario del agente — las acciones que puede realizar:

HerramientaQué hace
create_instance / destroy_instanceconstruir y lanzar la Instancia desde una Especificación de Hardware / derribarla
get_instance / get_statusla Instancia actual + estado del ciclo de vida / el estado de ejecución del invitado en vivo
get_shareinformar la configuración de uso compartido de carpetas host↔guest + el comando de montaje 9p exacto para el invitado
get_serial / read_serial / write_serialinformar la configuración del Puerto Serie + dispositivo de consola / drenar la salida serie del invitado / escribir entrada en la consola (controlado por QMP_MCP_ALLOW_SERIAL_WRITE)
pause_instance / resume_instancecongelar / descongelar las CPUs del invitado
reset_instance / powerdown_instancereinicio forzado / solicitar un apagado ACPI elegante
list_block_devices / query_cpuslos discos y medios de respaldo de la VM / información por CPU
screendumpuna captura de pantalla PNG de la Pantalla
start_recording / stop_recording / get_recordinggrabar la Pantalla en un archivo de video bajo la raíz de grabación del operador (necesita QMP_MCP_RECORDING_DIR + un ffmpeg ejecutable) / finalizar el archivo / informar la capacidad de grabación + configuración del codificador
get_events / wait_for_eventeventos QEMU recientes / bloquear hasta que llegue uno nombrado
qmp_executeun comando QMP crudo, controlado por la Política de Comandos
create_image / list_images / list_isoscrear una imagen de disco / listar discos / listar ISOs de arranque
list_iso_catalog / download_iso / get_downloadlistar el catálogo de descarga de imágenes de SO / obtener una en el Almacén de ISOs (controlado por QMP_MCP_ALLOW_DOWNLOAD) / consultar el progreso de descarga

Para las tablas de herramientas exactas por implementación, consulta los READMEs de TypeScript y Rust.

Inicio rápido: escenarios comunes

El servidor se ejecuta dondequiera que QEMU esté instalado. Primero consigue que una de las implementaciones se ejecute y apunta tu cliente MCP hacia ella — ejecuta la variante TypeScript o ejecuta la variante Rust — luego pídele a tu agente que haga algo. Los escenarios a continuación son cómo se ve eso: cada uno es una Especificación de Hardware (los argumentos para create_instance) más lo que tuviste que poner en su lugar primero.

1. Una VM de prueba para jugar

Nada que configurar — solo pide una máquina pequeña y manéjala.

"Arranca una VM Linux de 1 GB y dime su estado de ejecución."

El agente llama a create_instance con una especificación mínima, luego get_status; destroy_instance limpia:

{ "machine": "q35", "cpu": "host", "vcpus": 1, "memoryMb": 1024, "accel": "auto" }

(Con sin disco ni ISO no hay nada que arrancar — perfecto para una prueba de humo; agrega medios para lo real.)

2. Instalar un SO desde un ISO

Coloca el ISO del instalador en tu carpeta Almacén de ISOs; el agente crea un disco en blanco para él y arranca desde el CD primero (boot: "dc").

"Crea un disco de 20 GB e instala Debian desde debian-13.iso en él."

Llama a create_image (en el Almacén de Imágenes), luego create_instance:

{
  "machine": "q35", "cpu": "host", "vcpus": 2, "memoryMb": 2048, "accel": "auto",
  "disks": [{ "image": "debian.qcow2" }],
  "cdrom": { "iso": "debian-13.iso" },
  "boot": "dc",
  "display": "vnc"
}

Porque pidió display: "vnc", puedes ver el instalador ejecutarse — consulta el escenario 4.

3. Un servidor sin cabeza al que puedas hacer SSH

Agrega un reenvío de host para que un puerto en tu host llegue a un puerto en el invitado.

"Ejecuta mi imagen de servidor sin cabeza y reenvía el puerto host 2222 al invitado 22."

{
  "machine": "q35", "cpu": "host", "vcpus": 2, "memoryMb": 2048, "accel": "auto",
  "disks": [{ "image": "server.qcow2" }],
  "network": { "hostForwards": [{ "hostPort": 2222, "guestPort": 22 }] }
}

Una vez que arranque, ssh -p 2222 user@localhost desde el host llega al SSH del invitado.

4. Verlo en un navegador

Configura QMP_MCP_VIEWER_PASSWORD, pide una pantalla vnc, y abre el Visor. Los detalles de configuración están en los READMEs de TypeScript / Rust; cualquier especificación con "display": "vnc" entonces obtiene una pantalla en vivo e interactiva en http://<host>:6080/.

5. Emular una arquitectura diferente

Elige una máquina y CPU ARM — el emulador qemu-system-aarch64 se elige automáticamente desde el machine (sin necesidad de QMP_MCP_QEMU_BINARY).

"Levanta una máquina virtual ARM64."

{ "machine": "virt", "cpu": "cortex-a72", "vcpus": 2, "memoryMb": 2048, "accel": "tcg" }

En un host x86 accel: auto ya resuelve a TCG (un invitado aarch64 no puede usar KVM x86). En un host ARM usaría KVM, que solo acepta una CPU host/max — así que un modelo nombrado como cortex-a72 allí necesita accel: tcg (como arriba), y las placas raspi* siempre se ejecutan bajo TCG (su CPU integrada no se puede virtualizar).

(Si también necesitas construir el binario Rust para un host no x86, consulta su guía de compilación cruzada.)

6. Emular una placa Raspberry Pi

Las máquinas Raspberry Pi de QEMU arrancan un kernel directamente y renderizan un framebuffer que puedes ver en el Visor del navegador. Coloca el kernel extraído y el árbol de dispositivos en el Almacén de Imágenes (las máquinas raspi* seleccionan qemu-system-aarch64 por ti), y:

"Arranca una Raspberry Pi 3 y muéstrame la consola."

{
  "machine": "raspi3b",
  "accel": "tcg",
  "kernel": "kernel8.img",
  "dtb": "bcm2710-rpi-3-b.dtb",
  "appendCmdline": "console=tty1 root=/dev/mmcblk0p2 rootwait rw",
  "disks": [{ "image": "raspios.img", "interface": "sd", "format": "raw" }],
  "network": { "model": "usb-net" },
  "display": "vnc"
}

console=tty1 pone la consola en el framebuffer, así que el Visor noVNC muestra la Pi arrancando — logos y todo. Sin cpu/vcpus/memoryMb: el hardware de la placa es fijo. La Pi no tiene bus PCI, así que la NIC predeterminada no puede conectarse — usa "network": { "model": "usb-net" } para su NIC USB, o "network": { "mode": "none" } para sin red en absoluto. (En una Pi 3, fusiona el overlay del árbol de dispositivos disable-bt en el dtb primero, o la consola se queda pegada a la UART compartida con Bluetooth en lugar de la pantalla.)

Elegir una implementación

Las dos son intercambiables — mismas herramientas, mismas especificaciones, mismo comportamiento, verificadas continuamente entre sí. Elige por ecosistema:

TypeScriptRust
Construido sobreNode + mcp-frameworkrmcp + tokio
Se distribuye comoun paquete npm / node dist/index.jsun solo binario autocontenido
Ponlo en marchaEjecútalo →Ejecútalo →
En DockerDocker →Docker →

Todo lo específico de despliegue y uso vive en esos dos READMEs:

Configuración

Ambas implementaciones se configuran completamente a través de variables de entorno QMP_MCP_* — los mismos nombres y valores predeterminados para cada una. La referencia completamente comentada es .env.example, y el formato del archivo de política de comandos es policy.example.yaml. Las que usarás:

VariablePredeterminadoQué hace
QMP_MCP_TRANSPORTstdiostdio, http, o both
QMP_MCP_API_KEYS(sin configurar)claves API para el transporte HTTP (requeridas a menos que sea inseguro)
QMP_MCP_QEMU_BINARY(derivado de machine)generalmente sin configurar — el emulador se deriva del machine (q35→x86_64, virt/raspi*→aarch64, ADR-0013); configúralo para forzar un emulador para cada Instancia
QMP_MCP_IMAGE_DIR / QMP_MCP_ISO_DIRrutas XDGlas carpetas del Almacén de Imágenes / Almacén de ISOs
QMP_MCP_ALLOW_DOWNLOADfalsehabilita download_iso para obtener imágenes de SO en el Almacén de ISOs (desactivado por defecto; el agente nunca puede activarlo)
QMP_MCP_ISO_CATALOG(integrado)ruta a un JSON de catálogo de descargas personalizado (sin configurar ⇒ la lista integrada de 24 distribuciones)
QMP_MCP_RECORDING_DIR(sin configurar)habilita la grabación de Pantalla: la raíz absoluta del host bajo la cual se escriben los archivos <name>.mkv (sin configurar ⇒ start_recording no disponible)
QMP_MCP_FFMPEG_BINARYffmpegel ffmpeg con el que la grabación codifica — un nombre PATH o ruta absoluta (incluido en las imágenes Docker; opcional en metal desnudo)
QMP_MCP_VIEWER_PASSWORD(sin configurar)habilita el Visor de navegador
QMP_MCP_VIEWER_USER(sin configurar)nombre de usuario opcional aplicado en la autenticación HTTP Basic del Visor (predeterminado: nombre de usuario ignorado, solo contraseña)
QMP_MCP_HOST_SHARE_DIR(sin configurar)directorio absoluto del host compartido en los invitados a través de virtio-9p cuando una especificación establece share: true (sin configurar ⇒ uso compartido desactivado; ADR-0014)
QMP_MCP_GUEST_SHARE_DIR(sin configurar)punto de montaje previsto del invitado (informativo) — get_share informa el comando mount -t 9p exacto
QMP_MCP_ALLOW_SHARE_WRITEfalsemontar el recurso compartido en lectura-escritura (predeterminado solo lectura; el agente nunca puede escalar)
QMP_MCP_ALLOW_RAW_ARGSfalsepermitir el extraArgs de una especificación (la vía de escape)

…más límites en disco/memoria/vCPUs, el rango de puertos de reenvío de host, las perillas del codificador de grabación (códec, CRF, max-fps, formato de píxeles), las listas de permitir/denegar de la Política de Comandos y el archivo de política, y el tamaño del Búfer de Eventos. Consulta .env.example para la lista completa, o la sección de Configuración de cada variante en contexto (TypeScript · Rust).

Para desarrolladores

Estructura

qmp-mcp/
├── typescript/          the Node / mcp-framework implementation
├── rust/                the Rust / rmcp implementation
├── testdata/            shared golden fixtures both implementations assert
├── docs/                design notes and rationale
├── CONTEXT.md           the domain glossary — the shared vocabulary
├── .env.example         every QMP_MCP_* variable, commented
└── policy.example.yaml  the command-policy file format

Las dos implementaciones son bases de código independientes que comparten tres cosas en la raíz: el modelo de dominio (CONTEXT.md — léelo primero), los fixtures dorados (testdata/), y la superficie de configuración (.env.example).

Cómo las dos se mantienen idénticas

La paridad aquí no es una promesa, es una prueba. testdata/ contiene fixtures dorados neutrales al lenguaje que fijan la línea de comando QEMU exacta que cada Especificación de Hardware debe producir y el veredicto exacto que la Política de Comandos debe devolver — y ambas implementaciones se prueban contra ese mismo corpus. Cambia cómo una especificación se convierte en una línea de comando, o lo que la política permite, y actualizas el fixture compartido; la suite TypeScript y la suite Rust tienen que estar de acuerdo, o la compilación falla. Enseña a una implementación un nuevo truco y agregas el fixture que la otra tiene que satisfacer.

Trabajar en una variante es autocontenido en su carpeta — desarrollar TypeScript · desarrollar Rust. La carpeta docs/ contiene el razonamiento más extenso detrás de las decisiones más complicadas.

Licencia

MIT.