MacControlMCP
Un servidor MCP que permite a un LLM controlar aplicaciones de macOS y el Simulador de iOS a través de la API de Accesibilidad.
Documentación
MacControlMCP
Un servidor MCP que permite a un LLM controlar aplicaciones de macOS — y el Simulador de iOS — a través de la API de Accesibilidad.
Apunta un cliente MCP (Claude Code, Codex, Gemini/Antigravity, el MCP Inspector, …) a este servidor y el modelo puede resolver o lanzar una aplicación, leer una instantánea compacta de su interfaz con referencias, y luego hacer clic, presionar, escribir, establecer valores, desplazarse, manejar menús y leer el resultado — de la misma manera que lo haría una persona, pero a través de Accesibilidad en lugar de píxeles.
Estado: experimental, y la automatización de macOS es genuinamente difícil (enfoque, Spaces, peculiaridades de Accesibilidad específicas de cada aplicación). Funciona bien para un conjunto creciente de aplicaciones; espera bordes ásperos. Issues y PRs son bienvenidos.
Lo que puede hacer
- Resolver o lanzar una aplicación por nombre, bundle id, pid o título de ventana — y auto-lanzarla si no está en ejecución.
- Leer una jerarquía de interfaz compacta — cada elemento obtiene un
refestable (e42), un rol, etiqueta, valor, banderas de estado y las acciones que soporta. Diseñado para ser eficiente en tokens y directamente manejable, no un volcado AX crudo. - Manejar elementos por referencia — presionar acciones, clics reales, escribir texto, establecer valores de sliders/steppers, alternar disclosure, desplazarse a la vista, expandir subárboles cargados perezosamente.
- Ingresar texto de manera robusta —
typeintenta una inserción directa de Accesibilidad (sin portapapeles), recurre a pulsaciones de teclas sintéticas, luego a un pegado del portapapeles, y te dice qué ruta usó. - Manejar la barra de menús por ruta de títulos (
File ▸ Export… ▸ PDF…). - Gestionar ventanas (mover/redimensionar/minimizar/elevar) y terminar aplicaciones (escalada elegante de
SIGHUP → SIGTERM → SIGKILL). - Capturar y leer la pantalla — capturas de pantalla (vía ScreenCaptureKit) y OCR en imagen (vía Vision).
- Manejar el Simulador de iOS vía
simctl(abrir URLs, establecer apariencia/barra de estado, lanzar/terminar aplicaciones). - Observar cambios — actuar-y-asentarse devuelve un diff en el mismo vocabulario de
ref;wait_forconsulta activamente una condición.
Requisitos
- macOS 14 (Sonoma) o posterior, Apple Silicon o Intel.
- Permiso de Accesibilidad otorgado al host (solicitado en el primer uso).
- Permiso de Grabación de Pantalla para la herramienta
screenshot(solicitado en la primera captura). - Instalar desde PyPI no necesita nada más — el wheel incluye una aplicación firmada y notarizada.
- Para compilar desde el código fuente: Xcode 16+, XcodeGen (
brew install xcodegen, o deja queinstall.shlo instale) para generar el proyecto desdeproject.yml, y una identidad de firma Developer ID Application — el servicio XPC Mach del host está limitado al equipo, por lo que la firma ad-hoc no funcionará.
Arquitectura
macOS solo permite que el proceso que recibió el permiso de Accesibilidad realmente lo use — y un cliente MCP (una CLI o aplicación) es el lugar equivocado para mantener ese permiso. Así que el servidor está dividido:
MCP client ──stdio──▶ MacControlRelay ──XPC──▶ MacControlHost (LaunchAgent)
(Claude/Codex/…) (tiny forwarder) (holds the Accessibility grant,
runs the MCPServer + all tools)
MacControlRelay— el pequeño binario stdio que tu cliente MCP lanza. Reenvía JSON-RPC al host a través de un servicio XPC Mach firmado con código y escribe las respuestas de vuelta. Se reconecta de manera transparente (y puede iniciar el host en frío) si es necesario.MacControlHost— un LaunchAgent sin rostro (LSUIElement) que posee los permisos de Accesibilidad / Grabación de Pantalla y ejecuta elMCPServerreal con cada herramienta.MacControlRegistrar/MacControlMCP.app— registran el LaunchAgent del host (víaSMAppService) y activan las solicitudes de permisos; la aplicación auto-inicializa la pila en el primer uso.
Por qué esto importa: otorgas Accesibilidad una vez, al host, y cada cliente MCP que lanza el relay la reutiliza. El relay no lleva permisos propios.
Estructura del código fuente (módulos SPM)
| Módulo | Rol |
|---|---|
MacControlMCPCore | Servidor MCP/JSON-RPC, el esquema compacto de interfaz + leyenda, diffing, temporización de quiescencia, herramientas de simulador y listado de aplicaciones |
AXKit | el motor de Accesibilidad: envoltorio de elementos, recorrido de árbol, familia de herramientas control_app, resolución de aplicaciones, actuar-y-asentarse |
InputKit | entrada sintética (clics, teclas, desplazamiento, arrastre, escritura Unicode, pegado) |
CaptureKit | capturas de pantalla + OCR |
HostKit | el servicio host XPC, el cableado completo del servidor y el registro de depuración |
MacControlHost / MacControlRelay / MacControlRegistrar / MacControlMCP | los ejecutables / la aplicación |
Herramientas
Manejar una aplicación (la superficie principal)
control_app es el punto de entrada: resuelve (o lanza) una aplicación y devuelve una jerarquía compacta con referencias, precedida por una leyenda que explica el formato y los verbos. Todo lo demás opera sobre los refs que devuelve.
| Herramienta | Qué hace |
|---|---|
control_app(identity, window?, timeout?, maxLines?, maxChars?) | Resolver por nombre/bundle id/pid/título de ventana → árbol con referencias. Auto-lanza si no está en ejecución. La salida está limitada en tamaño (maxChars predeterminado 40k, maxLines predeterminado 1200); cualquier corte se reporta en línea. |
launch_app(app, activate?, timeout?) | Lanzar por ruta de .app o bundle id, esperar la primera ventana, devolver el árbol. |
action(ref, action, refresh?) | Realizar una acción AX: press, menu, inc, dec, disclose, collapse, o una etiqueta de acción personalizada. |
click(ref, count?, refresh?) | Clic real en el elemento (trae su aplicación al frente). count:2 hace doble clic. |
type(text, ref?, via?, refresh?) | Ingresar texto: inserción AX directa → pulsaciones de teclas → respaldo de pegado del portapapeles. Reporta via y focused. |
change_text(ref, value, refresh?) | Establecer el valor de texto de un campo semánticamente (sin pulsaciones de teclas). |
change_value(ref, value, refresh?) | Establecer un control numérico (slider/scrollbar/stepper), con límites aplicados. |
focus_keyboard(ref, observe?) | Dar enfoque de teclado a un elemento (sin clic, no disruptivo). |
reveal(ref, observe?) | Desplazar un elemento a la vista. |
expand(ref, timeout?) / refresh(ref, timeout?) | Cargar perezosamente [N hidden] descendientes / re-leer un subárbol. |
window(ref, action, …) | move / resize / minimize / unminimize / raise. |
menu_pick(pid, path, observe?) | Manejar la barra de menús por ruta de títulos, p. ej. ["File","New"]. |
find_elements(pid, role?, titleContains?, identifier?, value?, actionable?, limit?) | Buscar en el árbol referencias coincidentes sin re-leerlo todo. |
element_detail(ref) | Atributos completos / acciones / atributos parametrizados para una referencia. |
focused_element() / element_at(x, y) | El elemento enfocado / prueba de impacto en un punto de pantalla. |
get_changes(pid, depth?) | Diff de la interfaz de la aplicación contra la última instantánea (añadido/eliminado/cambiado por referencia). |
wait_for(pid, mode, …) | Consultar hasta que idle / appears / disappears. |
kill(identity, signal?) | Terminar por pid/nombre/bundle id; escalada predeterminada SIGHUP → SIGTERM → SIGKILL. |
Entrada sintética (coordenadas crudas / teclas)
click_point(x, y, …), scroll(dy, dx?), key(keys), hover(x, y), drag(fromX, fromY, toX, toY) — nivel de coordenadas/pulsaciones de teclas. Prefiere los verbos basados en referencias anteriores; recurre a estos solo cuando tengas una coordenada explícita.
Captura, descubrimiento, simulador
screenshot(target, …), ocr(path), list_running_apps(), list_simulators(), sim(action, …), open(target, application?, background?, newInstance?).
open es el equivalente sin permisos de hacer doble clic en Finder: abre un archivo, carpeta, URL o aplicación vía /usr/bin/open. El objetivo se pasa como un elemento literal de la matriz de argumentos (nunca a través de un shell), y la forma de opción (-u para URLs, -a/-b para aplicaciones, -- antes de operandos de archivo) evita que un objetivo que comienza con - se lea como una bandera.
Instalación
Desde PyPI (sin Xcode, sin Developer ID)
uvx drews-mac-control-mcp --setup
El wheel incluye la aplicación firmada y notarizada. Se instala en ~/Applications en el primer uso, la abre para que macOS muestre las solicitudes de permisos e imprime el comando de registro del cliente.
Aprueba ambas solicitudes. La primera permite un nuevo elemento de fondo — ese es el agente host, y descartarlo deja al host incapaz de iniciarse, con cada llamada de herramienta posterior fallando sin explicación (re-habilítalo en Configuración del Sistema ▸ General ▸ Elementos de Inicio de Sesión y Extensiones). La segunda otorga Accesibilidad, además de Grabación de Pantalla si quieres capturas de pantalla.
Las versiones posteriores reemplazan la aplicación instalada por sí mismas: el envoltorio compara la versión que lleva contra la de ~/Applications en cada lanzamiento, por lo que una actualización de uvx trae la aplicación consigo. Nada en esta ruta necesita Xcode o una identidad de firma propia.
Requiere macOS 14 o posterior. El wheel está etiquetado como macosx_14_0_universal2, por lo que pip y uv se niegan a instalarlo en cualquier otro lugar.
Con Homebrew
brew install --cask drewster99/tap/maccontrol-mcp
La misma aplicación, instalada en /Applications en lugar de ~/Applications.
Las dos compilaciones son productos separados, no dos copias de uno: la compilación de ~/Applications lleva un sufijo de .user en sus identificadores de bundle, su etiqueta de LaunchAgent y su servicio Mach. Instala ambos si quieres — ninguno nota al otro. El costo es que macOS ve dos aplicaciones, por lo que cada una necesita su propio permiso de Accesibilidad (y Grabación de Pantalla). Consulta CLAUDE.md para saber por qué.
Cada release de GitHub también incluye MacControlMCP-<version>.zip para una instalación manual, y un SHA256SUMS que cubre cada descarga.
Actualización
uvx --refresh drews-mac-control-mcp # PyPI (or just wait ~10 min for it to self-update)
brew upgrade --cask drewster99/tap/maccontrol-mcp # Homebrew
./install.sh # from a source checkout
El envoltorio de PyPI se auto-actualiza por sí solo — en cada lanzamiento re-verifica el índice de PyPI (cacheado ~10 min) y cambia ~/Applications cuando aparece un wheel más nuevo; --refresh solo lo fuerza ahora. Homebrew y install.sh reemplazan la compilación de /Applications en su lugar. Cualquiera de estos surte efecto la próxima vez que tu cliente MCP genere el servidor, así que reconéctalo después (p. ej. /mcp reconnect maccontrol en Claude Code).
Desinstalación
uvx drews-mac-control-mcp --uninstall # PyPI install
brew uninstall --cask drewster99/tap/maccontrol-mcp # Homebrew install
Ambos eliminan el registro del agente host antes de borrar la aplicación, que es el único orden que funciona: borrar el bundle por sí solo deja el registro como un elemento de inicio de sesión que apunta a nada, localizable solo a mano en Configuración del Sistema. La aplicación expone --unregister-and-exit para exactamente esto, por lo que ninguna ruta necesita la GUI.
Compilar e instalar desde el código fuente
Un comando
./install.sh
Eso es todo. install.sh genera el proyecto Xcode, compila la aplicación Release, la firma con tu Developer ID, la instala en /Applications, la lanza (lo que registra el LaunchAgent del host y activa las solicitudes de permisos de macOS) y registra el relay con cualquier cliente MCP (claude, codex) que encuentre en tu PATH. Cuando termine, otorga Accesibilidad (y Grabación de Pantalla para capturas de pantalla) si no se te solicitó antes, y estás listo.
Prerrequisitos: macOS 14+, Xcode 16+, XcodeGen y una identidad de firma Developer ID Application en tu llavero — el servicio XPC Mach del host está limitado al equipo, por lo que la firma ad-hoc no funcionará. Si XcodeGen falta, el script ofrece brew install xcodegen por ti (--install-deps acepta de antemano, para ejecuciones sin supervisión).
Banderas útiles:
./install.sh --notarize # also notarize + staple (for distribution; needs a notarytool profile)
./install.sh --identity "Developer ID Application: …" # pick a specific signing identity
./install.sh --clients claude # only register Claude Code (or: codex / none / claude,codex)
./install.sh --prefix ~/Applications # install somewhere other than /Applications
./install.sh --no-launch # build + install but don't open the app
./install.sh --install-deps # install missing deps (xcodegen, via Homebrew) without asking
./install.sh --help # all options
Compilación manual
El script orquesta los mismos pasos que puedes ejecutar a mano:
./scripts/generate.sh # write the generated sources, then generate the .xcodeproj
# build the Release scheme in Xcode, then sign (+ notarize) the result:
./notarize-app.sh # produces a signed, notarized dist/MacControlMCP.app
cp -R dist/MacControlMCP.app /Applications/
open /Applications/MacControlMCP.app
notarize-app.sh es el bucle interno rápido — firma y notariza el bundle que Xcode ya compiló, en lugar de recompilar desde cero como hacen install.sh y scripts/build-release.sh.
Los tres delegan la firma en sí a scripts/sign-app.sh, que es el único lugar que conoce el orden interno, el identificador explícito del relay y las comprobaciones que se interponen entre una compilación y un rechazo de notarización — runtime endurecido, timestamp seguro, autoridad de Developer ID Application, sin get-task-allow, sin symlinks. Ejecútalo directamente contra cualquier bundle compilado:
./scripts/sign-app.sh --app dist/system/MacControlMCP.app # signed for distribution
./scripts/sign-app.sh --app dist/system/MacControlMCP.app --no-timestamp # offline/local
Deriva el identificador de firma del relay del propio bundle, por lo que firma el relay de una compilación de .user bajo el identificador que el host de esa compilación realmente requiere — equivocarse en esto solo se manifiesta como un "host no disponible" inexplicable en el primer uso.
scripts/gen-identity.sh escribe la identidad contra la que compila una compilación (--variant system|user) e imprime el IDENTITY_SUFFIX para pasar a xcodebuild. Es el único lugar donde se definen los identificadores; todo lo demás se deriva de él.
Para una verificación rápida solo de los objetivos de biblioteca/binario (sin el paquete de aplicación, no retendrá la concesión de Accesibilidad):
swift build # debug build of all targets
swift test # run the unit tests
La firma/notarización usa por defecto una identidad de desarrollador Nuclear Cyborg y un perfil de llavero
notarytool. Anula la identidad con--identity/CODESIGN_IDENTITYy el perfil con--profile/NOTARY_PROFILE. Los forks también deben cambiar los identificadores de paquete y el prefijo de equipo enpackaging/host.launchagent.plist.
Publicar una versión
scripts/build-release.sh es la otra mitad de install.sh: los mismos pasos de compilación y firma, pero empaqueta el paquete notarizado en la rueda de PyPI en lugar de instalarlo localmente.
./scripts/build-release.sh # build + verify, publish nothing
./scripts/build-release.sh --publish --testpypi # rehearse on TestPyPI
./scripts/build-release.sh --publish # PyPI + git tag + GitHub release
./scripts/build-release.sh --help # all options
Incrementa la versión de forma sincronizada en AppVersion.swift, project.yml y python/pyproject.toml, ejecuta las pruebas y luego compila dos veces — una por identidad de instalación, ya que las compilaciones de /Applications y ~/Applications son productos diferentes. Cada compilación se verifica (ambos segmentos de arquitectura, el plist incrustado y que lleva la identidad que declara), se firma y se notariza; la rueda recibe la compilación de usuario y el archivo cask la del sistema. Finalmente extrae la aplicación de la rueda terminada para confirmar que aún se verifica y se sella. Nada llega a PyPI, GitHub o origin sin --publish y un mensaje de confirmación.
La publicación produce tres artefactos de lanzamiento — la rueda, MacControlMCP-<version>.zip y SHA256SUMS — sube la rueda a PyPI y actualiza el cask de Homebrew en drewster99/homebrew-tap (--skip-tap opta por no participar). El cask se regenera a partir del digest del propio lanzamiento y se lee de vuelta desde el tap para confirmar que apunta a la etiqueta que se acaba de cortar.
Registrarse con un cliente MCP
Lo que registres depende de cómo instalaste, y la diferencia decide si recibes actualizaciones.
Instalado desde PyPI — registra el wrapper, no el relay.
claude mcp add --scope user maccontrol -- "$(command -v uvx)" drews-mac-control-mcp
codex mcp add maccontrol -- "$(command -v uvx)" drews-mac-control-mcp
El wrapper verifica en cada lanzamiento que la aplicación en ~/Applications coincida con la versión que lleva su rueda, y la reemplaza si no es así. uvx proporciona la otra mitad: almacena en caché el índice de PyPI durante los diez minutos que PyPI solicita (cache-control: max-age=600) y vuelve a resolver una vez que expira, por lo que una nueva versión se detecta por sí sola — en minutos, no necesariamente en la siguiente llamada. uvx --refresh drews-mac-control-mcp lo fuerza inmediatamente.
Apuntar un cliente a la ruta del relay funciona hoy y nunca se actualiza de nuevo.
Instalado vía Homebrew o install.sh — registra el relay directamente, ya que brew upgrade y install.sh son los que lo actualizan:
claude mcp add --scope user maccontrol /Applications/MacControlMCP.app/Contents/Helpers/MacControlRelay
codex mcp add maccontrol -- /Applications/MacControlMCP.app/Contents/Helpers/MacControlRelay
install.sh registra ambos clientes por ti, y uvx drews-mac-control-mcp --setup imprime los comandos para su propio canal. Cualquier cliente MCP que lance un servidor stdio funciona de la misma manera.
Registro
El relay y el host agregan ambos a una sola línea de tiempo en:
~/Library/Logs/MacControlMCP/maccontrol.log
Registra inicio / conexión / desconexión más cada solicitud y respuesta en su totalidad (cada línea etiquetada [process:pid], protegida por flock para que los dos procesos nunca se intercalen). Siempre activo; establece MACCONTROL_LOG=0 para deshabilitar o MACCONTROL_LOG_PATH=/abs/file para redirigir. Cada línea de inicio incluye la marca de tiempo de compilación del binario para que puedas confirmar qué compilación está activa.
Versionado
Hay una sola versión que incrementar, declarada en tres archivos que la compilación mantiene sincronizados:
Sources/MacControlMCPCore/AppVersion.swift— la fuente de verdad compilada que informa cada componente (la línea "Versión" de la GUI, elinitializeserverInfo.versionde MCP y la identidad de compilación del registro de inicio).project.yml—MARKETING_VERSION/CURRENT_PROJECT_VERSION, que alimentan losCFBundleShortVersionString/CFBundleVersionde los paquetes nativos.python/pyproject.toml— la versión de la rueda, para queuvx drews-mac-control-mcpy la aplicación que instala nunca sean dos historias diferentes.
install.sh y scripts/build-release.sh incrementan los tres por ti; configurarlos manualmente significa establecer los tres al mismo valor y luego volver a ejecutar xcodegen generate. Una fase de compilación previa "Verificar versión" falla la compilación si la constante Swift y project.yml no coinciden, y el script de lanzamiento vuelve a leer cada archivo que escribió — un sed que no coincide con nada aún sale con 0.
La ventana de configuración de la aplicación muestra su propia versión y, consultando el host en ejecución a través de XPC, la versión del agente en vivo — señalando una discrepancia (por ejemplo, un host obsoleto dejado registrado por una instalación anterior). La verificación de la versión del agente necesita la compilación firmada para satisfacer el requisito de llamador del host; una compilación de desarrollo sin firmar mostrará el agente como "no alcanzable".
Limitaciones conocidas
- La accesibilidad es por Espacio. AX enumera ventanas en el Espacio actual de Mission Control; una aplicación cuyas ventanas están en otro Espacio (o con la pantalla dormida) puede informar cero ventanas aunque existan.
- Las pulsaciones de teclas sintéticas llegan a la aplicación que tiene el foco de teclado; macOS 14 no permite que una herramienta en segundo plano robe el foco de teclado, por eso
type(ref)hace clic en el campo primero y recurre a un pegado de portapapeles para vistas de texto de AppKit. - La cobertura de AX específica de la aplicación varía — Catalyst, Electron y contenido web exponen árboles diferentes (a veces escasos). Consulta
docs/para las notas de diseño ydocs/ROADMAP.mdpara lo que está diferido.