MacControlMCP
Um servidor MCP que permite que um LLM controle aplicativos macOS e o Simulador iOS por meio da API de Acessibilidade.
Documentação
MacControlMCP
Um servidor MCP que permite que um LLM controle aplicativos macOS — e o Simulador iOS — através da API de Acessibilidade.
Aponte um cliente MCP (Claude Code, Codex, Gemini/Antigravity, o MCP Inspector, …) para este servidor e o modelo pode resolver ou iniciar um aplicativo, ler um snapshot compacto e com referências da sua interface, e então clicar, pressionar, digitar, definir valores, rolar, navegar por menus e ler o resultado de volta — da mesma forma que uma pessoa faria, mas via Acessibilidade em vez de pixels.
Status: experimental, e automação de macOS é genuinamente difícil (foco, Spaces, peculiaridades de Acessibilidade específicas de cada aplicativo). Funciona bem para um conjunto crescente de aplicativos; espere arestas. Issues e PRs são bem-vindos.
O que ele pode fazer
- Resolver ou iniciar um aplicativo por nome, bundle id, pid ou título de janela — e iniciá-lo automaticamente se não estiver em execução.
- Ler uma hierarquia de interface compacta — cada elemento recebe um
refestável (e42), um papel, rótulo, valor, flags de estado e as ações que suporta. Projetado para ser eficiente em tokens e diretamente acionável, não um dump bruto de AX. - Acionar elementos por referência — ações de pressionar, cliques reais, digitar texto, definir valores de slider/stepper, alternar disclosure, rolar para a visualização, expandir subárvores carregadas lentamente.
- Inserir texto de forma robusta —
typetenta uma inserção direta de Acessibilidade (sem área de transferência), recorre a teclas sintéticas e, em seguida, a uma colagem da área de transferência, e informa qual caminho usou. - Navegar pela barra de menus por caminho de título (
File ▸ Export… ▸ PDF…). - Gerenciar janelas (mover/redimensionar/minimizar/elevar) e encerrar aplicativos (escalonamento gracioso
SIGHUP → SIGTERM → SIGKILL). - Capturar e ler a tela — capturas de tela (via ScreenCaptureKit) e OCR na imagem (via Vision).
- Controlar o Simulador iOS via
simctl(abrir URLs, definir aparência/barra de status, iniciar/encerrar aplicativos). - Observar mudanças — agir-e-assentar retorna um diff no mesmo vocabulário
ref;wait_forconsulta ativamente por uma condição.
Requisitos
- macOS 14 (Sonoma) ou posterior, Apple Silicon ou Intel.
- Permissão de Acessibilidade concedida ao host (solicitada no primeiro uso).
- Permissão de Gravação de Tela para a ferramenta
screenshot(solicitada na primeira captura). - Instalar via PyPI não precisa de mais nada — o wheel carrega um aplicativo assinado e notarizado.
- Para compilar a partir do código-fonte: Xcode 16+, XcodeGen (
brew install xcodegen, ou deixeinstall.shinstalá-lo) para gerar o projeto a partir deproject.yml, e uma identidade de assinatura Developer ID Application — o serviço XPC Mach do host é limitado à equipe, então assinatura ad-hoc não funcionará.
Arquitetura
O macOS só permite que o processo que recebeu a permissão de Acessibilidade realmente a use — e um cliente MCP (um CLI ou aplicativo) é o lugar errado para manter essa concessão. Então o servidor é dividido:
MCP client ──stdio──▶ MacControlRelay ──XPC──▶ MacControlHost (LaunchAgent)
(Claude/Codex/…) (tiny forwarder) (holds the Accessibility grant,
runs the MCPServer + all tools)
MacControlRelay— o pequeno binário stdio que seu cliente MCP inicia. Ele encaminha JSON-RPC para o host através de um serviço XPC Mach assinado por código e escreve as respostas de volta. Ele se reconecta de forma transparente (e pode iniciar o host a frio) se necessário.MacControlHost— um LaunchAgent sem rosto (LSUIElement) que possui as concessões de Acessibilidade / Gravação de Tela e executa oMCPServerreal com cada ferramenta.MacControlRegistrar/MacControlMCP.app— registram o LaunchAgent do host (viaSMAppService) e acionam os prompts de permissão; o aplicativo auto-inicializa a pilha no primeiro uso.
Por que isso importa: você concede Acessibilidade uma vez, ao host, e cada cliente MCP que inicia o relay a reutiliza. O relay não carrega permissões próprias.
Estrutura do código-fonte (módulos SPM)
| Módulo | Função |
|---|---|
MacControlMCPCore | Servidor MCP/JSON-RPC, o esboço compacto da interface + legenda, diffing, temporização de quiescência, ferramentas de simulador e listagem de aplicativos |
AXKit | o motor de Acessibilidade: wrapper de elemento, percorredor de árvore, família de ferramentas control_app, resolução de aplicativos, agir-e-assentar |
InputKit | entrada sintética (cliques, teclas, rolagem, arrastar, digitação Unicode, colar) |
CaptureKit | capturas de tela + OCR |
HostKit | o serviço host XPC, a fiação completa do servidor e o log de depuração |
MacControlHost / MacControlRelay / MacControlRegistrar / MacControlMCP | os executáveis / aplicativo |
Ferramentas
Dirigindo um aplicativo (a superfície principal)
control_app é o ponto de entrada: ele resolve (ou inicia) um aplicativo e retorna uma hierarquia compacta com referências, prefixada com uma legenda explicando o formato e os verbos. Todo o resto opera nos refs que ele retorna.
| Ferramenta | O que faz |
|---|---|
control_app(identity, window?, timeout?, maxLines?, maxChars?) | Resolve por nome/bundle id/pid/título de janela → árvore com referências. Inicia automaticamente se não estiver em execução. A saída é limitada em tamanho (maxChars padrão 40k, maxLines padrão 1200); qualquer corte é relatado inline. |
launch_app(app, activate?, timeout?) | Inicia por caminho .app ou bundle id, aguarda a primeira janela, retorna a árvore. |
action(ref, action, refresh?) | Executa uma ação AX: press, menu, inc, dec, disclose, collapse, ou um rótulo de ação personalizada. |
click(ref, count?, refresh?) | Clique real no elemento (traz o aplicativo para a frente). count:2 dá duplo clique. |
type(text, ref?, via?, refresh?) | Insere texto: inserção AX direta → teclas → fallback de colagem da área de transferência. Relata via e focused. |
change_text(ref, value, refresh?) | Define o valor de texto de um campo semanticamente (sem teclas). |
change_value(ref, value, refresh?) | Define um controle numérico (slider/scrollbar/stepper), com limite de intervalo. |
focus_keyboard(ref, observe?) | Dá foco de teclado a um elemento (sem clique, não disruptivo). |
reveal(ref, observe?) | Rola um elemento para a visualização. |
expand(ref, timeout?) / refresh(ref, timeout?) | Carrega lentamente [N hidden] descendentes / relê uma subárvore. |
window(ref, action, …) | move / resize / minimize / unminimize / raise. |
menu_pick(pid, path, observe?) | Navega pela barra de menus por caminho de título, ex. ["File","New"]. |
find_elements(pid, role?, titleContains?, identifier?, value?, actionable?, limit?) | Busca na árvore por referências correspondentes sem reler tudo. |
element_detail(ref) | Atributos completos / ações / atributos parametrizados para uma referência. |
focused_element() / element_at(x, y) | O elemento focado / teste de hit em um ponto da tela. |
get_changes(pid, depth?) | Diff da interface do aplicativo contra o último snapshot (adicionado/removido/alterado por referência). |
wait_for(pid, mode, …) | Consulta até idle / appears / disappears. |
kill(identity, signal?) | Encerra por pid/nome/bundle id; escalonamento padrão SIGHUP → SIGTERM → SIGKILL. |
Entrada sintética (coordenadas brutas / teclas)
click_point(x, y, …), scroll(dy, dx?), key(keys), hover(x, y), drag(fromX, fromY, toX, toY) — nível de coordenadas/teclas. Prefira os verbos baseados em referência acima; use estes apenas quando tiver uma coordenada explícita.
Captura, descoberta, simulador
screenshot(target, …), ocr(path), list_running_apps(), list_simulators(), sim(action, …), open(target, application?, background?, newInstance?).
open é o equivalente sem concessão de dar duplo clique no Finder: ele abre um arquivo, pasta, URL ou aplicativo via /usr/bin/open. O alvo é passado como um elemento literal de array de argumentos (nunca através de um shell), e a forma de opção (-u para URLs, -a/-b para aplicativos, -- antes de operandos de arquivo) impede que um alvo que comece com - seja lido como uma flag.
Instalação
Via PyPI (sem Xcode, sem Developer ID)
uvx drews-mac-control-mcp --setup
O wheel carrega o aplicativo assinado e notarizado. Ele instala em ~/Applications no primeiro uso, abre
para que o macOS exiba os prompts de permissão e imprime o comando de registro do cliente.
Aprove ambos os prompts. O primeiro permite um novo item de fundo — esse é o agente host, e dispensá-lo deixa o host incapaz de iniciar, com toda chamada de ferramenta posterior falhando e sem explicação (reative em Ajustes do Sistema ▸ Geral ▸ Itens de Login e Extensões). O segundo concede Acessibilidade, além de Gravação de Tela se você quiser capturas de tela.
Versões posteriores substituem o aplicativo instalado por conta própria: o wrapper compara a versão que
carrega contra a que está em ~/Applications a cada inicialização, então uma atualização uvx traz o aplicativo
junto. Nada neste caminho precisa de Xcode ou de uma identidade de assinatura própria.
Requer macOS 14 ou posterior. O wheel é marcado como macosx_14_0_universal2, então pip e uv recusam
instalá-lo em qualquer outro lugar.
Com Homebrew
brew install --cask drewster99/tap/maccontrol-mcp
O mesmo aplicativo, instalado em /Applications em vez de ~/Applications.
As duas compilações são produtos separados, não duas cópias de um: a compilação ~/Applications carrega
um sufixo .user nos seus identificadores de bundle, no rótulo do LaunchAgent e no serviço Mach. Instale
ambos se quiser — nenhum percebe o outro. O custo é que o macOS vê dois aplicativos, então cada um precisa
da sua própria concessão de Acessibilidade (e Gravação de Tela). Veja CLAUDE.md para o porquê.
Cada release do GitHub também carrega MacControlMCP-<version>.zip para instalação manual e um
SHA256SUMS cobrindo todos os downloads.
Atualização
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
O wrapper PyPI se auto-atualiza sozinho — a cada inicialização ele re-verifica o índice do PyPI (cache ~10 min)
e troca ~/Applications quando um wheel mais novo aparece; --refresh apenas força agora. Homebrew e
install.sh substituem a compilação /Applications no lugar. Qualquer uma dessas entra em vigor na próxima vez que seu
cliente MCP gerar o servidor, então reconecte-o depois (ex. /mcp reconnect maccontrol no Claude
Code).
Desinstalação
uvx drews-mac-control-mcp --uninstall # PyPI install
brew uninstall --cask drewster99/tap/maccontrol-mcp # Homebrew install
Ambos removem o registro do agente host antes de excluir o aplicativo, que é a única ordem que
funciona: excluir o bundle sozinho deixa o registro para trás como um item de login apontando para
nada, encontrável apenas manualmente em Ajustes do Sistema. O aplicativo expõe --unregister-and-exit para
exatamente isso, então nenhum caminho precisa da GUI.
Compilar e instalar a partir do código-fonte
Um comando
./install.sh
Isso é tudo. install.sh gera o projeto Xcode, compila o aplicativo Release, assina com seu Developer ID, instala em /Applications, inicia (o que registra o LaunchAgent do host e aciona os prompts de permissão do macOS) e registra o relay com qualquer cliente MCP (claude, codex) que encontrar no seu PATH. Quando terminar, conceda Acessibilidade (e Gravação de Tela para capturas de tela) se não foi solicitado, e você está pronto.
Pré-requisitos: macOS 14+, Xcode 16+, XcodeGen e uma identidade de assinatura Developer ID Application no seu chaveiro — o serviço XPC Mach do host é limitado à equipe, então assinatura ad-hoc não funcionará. Se o XcodeGen estiver ausente, o script oferece brew install xcodegen para você (--install-deps aceita antecipadamente, para execuções não assistidas).
Flags úteis:
./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
Compilação manual
O script orquestra os mesmos passos que você pode executar manualmente:
./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 é o loop interno rápido — ele assina e notariza o bundle que o Xcode já compilou,
em vez de recompilar do zero como install.sh e scripts/build-release.sh fazem.
Todos os três delegam a assinatura em si a scripts/sign-app.sh, que é o único lugar que
conhece a ordem de dentro para fora, o identificador explícito do relay e as verificações que ficam entre uma
compilação e uma rejeição de notarização — runtime endurecido, timestamp seguro, autoridade Developer ID Application,
sem get-task-allow, sem symlinks. Execute diretamente contra qualquer 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
Ele deriva o identificador de assinatura do relay do próprio bundle, então assina o relay de uma compilação .user sob
o identificador que o host dessa compilação realmente exige — errar isso só aparece
como um "host indisponível" inexplicável no primeiro uso.
scripts/gen-identity.sh escreve a identidade contra a qual uma compilação é feita (--variant system|user)
e imprime o IDENTITY_SUFFIX para passar a xcodebuild. É o único lugar onde os identificadores são
definidos; todo o resto deriva dele.
Para uma verificação rápida apenas dos alvos de biblioteca/binário (sem o bundle do app, não exigirá a concessão de Acessibilidade):
swift build # debug build of all targets
swift test # run the unit tests
A assinatura/notarização usa por padrão um Developer ID da Nuclear Cyborg e um perfil de chaveiro
notarytool. Substitua a identidade com--identity/CODESIGN_IDENTITYe o perfil com--profile/NOTARY_PROFILE. Forks também precisam alterar os bundle ids e o prefixo da equipe empackaging/host.launchagent.plist.
Criando um release
scripts/build-release.sh é a outra metade de install.sh: mesmas etapas de build e assinatura, mas ele
empacota o bundle notarizado no wheel do PyPI em vez de instalá-lo 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
Ele incrementa a versão em sincronia em AppVersion.swift, project.yml e
python/pyproject.toml, executa os testes e então compila duas vezes — uma por identidade de instalação, já que
os builds de /Applications e ~/Applications são produtos diferentes. Cada build é verificado
(ambas as fatias de arquitetura, o plist embutido e que ele carrega a identidade que afirma), assinado
e notarizado; o wheel recebe o build de usuário e o arquivo do cask, o de sistema. Por fim, ele
extrai o app de volta do wheel finalizado para confirmar que ainda verifica e sela. Nada chega ao PyPI,
GitHub ou origin sem --publish e um prompt de confirmação.
A publicação produz três artefatos de release — o wheel, MacControlMCP-<version>.zip e
SHA256SUMS — envia o wheel para o PyPI e atualiza o cask do Homebrew em
drewster99/homebrew-tap (--skip-tap opta por não participar).
O cask é regenerado a partir do digest do próprio release e relido do tap para confirmar que ele
aponta para a tag que acabou de ser criada.
Registrar com um cliente MCP
O que você registra depende de como você instalou, e a diferença decide se você recebe atualizações.
Instalado via PyPI — registre o wrapper, não o 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
O wrapper verifica em cada inicialização se o app em ~/Applications corresponde à versão que seu
wheel carrega, e o substitui se não for o caso. uvx fornece a outra metade: ele armazena em cache o índice do PyPI por
os dez minutos que o PyPI solicita (cache-control: max-age=600) e re-resolve quando isso expira, então um
novo release é detectado por conta própria — em minutos, não necessariamente na próxima chamada.
uvx --refresh drews-mac-control-mcp força isso imediatamente.
Apontar um cliente para o caminho do relay funciona hoje e nunca mais atualiza.
Instalado via Homebrew ou install.sh — registre o relay diretamente, já que brew upgrade e
install.sh são o que o atualizam:
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 os clientes para você, e uvx drews-mac-control-mcp --setup imprime os
comandos para seu próprio canal. Qualquer cliente MCP que inicie um servidor stdio funciona da mesma forma.
Logging
O relay e o host ambos anexam a uma única linha do tempo em:
~/Library/Logs/MacControlMCP/maccontrol.log
Ele registra inicialização / conexão / desconexão além de cada requisição e resposta por completo (cada linha marcada com [process:pid], protegida por flock para que os dois processos nunca se intercalem). Sempre ativo; defina MACCONTROL_LOG=0 para desabilitar ou MACCONTROL_LOG_PATH=/abs/file para redirecionar. Cada linha de inicialização inclui o timestamp de build do binário para que você possa confirmar qual build está ativo.
Versionamento
Há uma única versão para incrementar, declarada em três arquivos que o build mantém em sincronia:
Sources/MacControlMCPCore/AppVersion.swift— a fonte de verdade compilada que todos os componentes relatam (a linha "Versão" da GUI, oinitializeserverInfo.versiondo MCP e a identidade de build no log de inicialização).project.yml—MARKETING_VERSION/CURRENT_PROJECT_VERSION, que alimentam osCFBundleShortVersionString/CFBundleVersiondos bundles nativos.python/pyproject.toml— a versão do wheel, para queuvx drews-mac-control-mcpe o app que ele instala nunca sejam duas histórias diferentes.
install.sh e scripts/build-release.sh incrementam todos os três para você; defini-los manualmente significa definir todos os três para o mesmo valor e então re-executar xcodegen generate. Uma fase de pré-build "Verificar versão" falha o build se a constante Swift e project.yml discordarem, e o script de release relê cada arquivo que escreveu — um sed que não corresponde a nada ainda sai com código 0.
A janela de configuração do app mostra sua própria versão e, consultando o host em execução via XPC, a versão do agente ativo — sinalizando uma incompatibilidade (por exemplo, um host obsoleto deixado registrado por uma instalação mais antiga). A verificação da versão do agente precisa do build assinado para satisfazer o requisito de chamador do host; um build de desenvolvimento não assinado mostrará o agente como "não alcançável".
Limitações conhecidas
- Acessibilidade é por Espaço. AX enumera janelas no Espaço atual do Mission Control; um app cujas janelas estão em outro Espaço (ou com a tela adormecida) pode relatar zero janelas mesmo que elas existam.
- Teclas sintéticas alcançam o app que detém o foco de teclado; o macOS 14 não permite que uma ferramenta em segundo plano roube o foco de teclado, por isso
type(ref)clica no campo primeiro e recorre a uma colagem da área de transferência para visualizações de texto do AppKit. - A cobertura AX específica do app varia — Catalyst, Electron e conteúdo web expõem árvores diferentes (às vezes esparsas). Veja
docs/para as notas de design edocs/ROADMAP.mdpara o que está adiado.