mac-reader
Captures PNG screenshots from a booted iOS Simulator device to verify app UI state independently of Accessibility.
Reads and acts on a Mac's screen through the macOS Accessibility API, providing tools to list windows, inspect element trees, search for controls, click elements, and type into fields.
Enables driving Xcode workflows such as archive builds and TestFlight uploads by reading and interacting with Xcode's window controls.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mac-readerread the front window and tell me what's in it"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mac-reader
An MCP server that reads and acts on a Mac's screen from another computer, over SSH, through the macOS Accessibility API. You get the window's structured content (roles, names, values, enabled state) as text instead of a screenshot for a vision model to guess at. Nothing is installed on the Mac.
Why
Driving a Mac from a Windows PC usually means screen sharing and screenshots. But macOS already maintains a structured tree of every window's controls for accessibility, and it can be read over a plain SSH login. It needs no remote desktop session and no app on the Mac, just Python's standard library.
It has been used for real work: driving Xcode through an archive build and a TestFlight upload from a Windows PC, reading the archive list, the run-destination picker and the distribution flow as text.
Related MCP server: gemini-mac-computer-use-mcp
How it works
Each tool call:
finds the Mac by its SSH host key, not by its address (
find_mac.py, below);sends
mac_agent.pyover SSH to the Mac's built-in/usr/bin/python3 -;reads the JSON it prints back.
mac_agent.py calls the Accessibility API through ctypes, so there is no pyobjc and no pip on
the Mac side.
Setup
On the Mac
Turn on Remote Login: System Settings → General → Sharing → Remote Login.
Allow key login from the computer you'll drive it from: add that computer's public key to
~/.ssh/authorized_keyson the Mac. Recommended: turn password login off, since this only ever uses a key. Put this in/etc/ssh/sshd_config.d/000-key-only.conf(needssudo):PasswordAuthentication no KbdInteractiveAuthentication noGrant Accessibility to
/usr/libexec/sshd-keygen-wrapper: System Settings → Privacy & Security → Accessibility → +, then press ⌘⇧G and type the path. macOS runs that program for every SSH login, so this is where the permission has to go for SSH commands. Without it, every read comes back empty and the tool says so.Read the Mac's host key at the Mac itself (not over the network, so you know it's the real one):
cat /etc/ssh/ssh_host_ed25519_key.pub
On your computer
pip install -r requirements.txt
cp mac.example.json mac.jsonFill in mac.json (it's gitignored):
field | what |
| your login name on the Mac |
| the whole line from step 4: |
| optional. A fixed address or hostname, if you have one. Then no network scan is needed. |
| optional. The network to scan, e.g. |
Check it can find the Mac:
python find_mac.pyRegister it with Claude Code at user scope, so every project can use it:
claude mcp add mac-reader -s user -- python "/path/to/mac-reader/server.py"Replace /path/to/mac-reader with wherever you cloned this repo.
Tools
Tool | What it does |
| Running apps and their windows. The front app is marked. Start here. |
| The window's element tree as text. |
| Elements whose role, name or value contains the query. Shows the exact names to click. |
| Presses a control once. Cursor-free unless |
| Sets a field's value (no keystrokes), then reads it back to verify. |
| A PNG from a booted iOS Simulator device, saved on your computer. |
Finding the Mac safely
A device is only "the Mac" if it proves it with the Mac's SSH host key. Something answering on port 22 is not identity: after the router reshuffles addresses, another device can hold the old one. So:
every candidate is checked by
sshitself: a connection pinned to the key inmac.jsonrunsexit 0, and any other key stops ssh before it logs in;every real command uses the same pinned connection (
HostKeyAlias, a dedicated known_hosts file,StrictHostKeyChecking=yes), so connecting to an impostor fails instead of working;BatchMode=yes: it logs in with a key or not at all, and never asks for a password;if two devices ever present the Mac's key, it refuses to pick one;
"presented a different key" (a security warning) and "couldn't connect" (a network problem) are reported separately, because they need different responses.
It tries the last verified address first, then scans the local network (about 3 seconds for a /24). It won't scan anything larger than 1024 addresses.
Safety rules
Each of these came from a real incident, on this tool or its Windows counterpart.
Press at most once.
AXPressis sent once. It falls back toAXPickorAXConfirmonly when the app reports the action was never delivered (unsupported, invalid element).kAXErrorCannotCompletedoes not fall through: it also comes back when the app got the press and answered slowly. Treating that as "didn't happen" is how one click becomes two.An action is never retried over SSH either: the connection can drop after the command ran. Reads retry once.
Confirm by what changed. After a press it watches the window for a second. If nothing changed, it says unconfirmed and warns not to press again.
The real pointer is opt-in. Accessibility actions don't move it.
allow_mouse=Trueis for controls with no action at all (Xcode's run-destination picker is one). It moves the pointer for one click and puts it back, even if the click fails.Never type a password. Password fields (
AXSecureTextField) are refused, with no override.Read-only apps. System Settings, Passwords and Keychain Access can be read but never acted in, with no override. The Claude desktop app needs
allow_other_session=True, because acting there uses another Claude session's permissions.Ambiguous matches are listed, never guessed. Pass
indexonly after reading the list.
What we learned building it
Most of this isn't written down anywhere we could find.
The permission goes to
/usr/libexec/sshd-keygen-wrapper. Not to Terminal, not to Python, not tosshd.SSH runs in a background session. From there, asking the system-wide Accessibility element "which app is focused?" fails with
kAXErrorCannotComplete. Asking each app directly (AXUIElementCreateApplication(pid)) works, with pids fromlsappinfo, which needs no permission.A locked screen doesn't fail, it lies. Reads succeed, but every window title comes back as its app's name. Every reply here ends with a LOCKED note when
CGSSessionScreenIsLockedis set.Decode
AXUIElementreferences. They have their own CoreFoundation type. A decoder that only knows strings, numbers, booleans and point/size values silently returns nothing forAXMenuBar,AXParentandAXFocusedWindow.Invisible characters break matching. Calculator's display reads U+200E followed by "0". It looks like "0" and never matches it. Names are normalised (format characters dropped, whitespace collapsed) before matching.
Xcode 26's SwiftUI panes are readable this way. AppleScript's System Events saw no text or buttons in the Organizer's archive list. This tool read all of it: rows, versions, statuses, Distribute App, Validate App. The content sits inside SwiftUI hosting views, which is our guess at why, not confirmed.
Windows' built-in
ssh-keyscancan't talk to current macOS. Windows OpenSSH 9.5 fails to negotiate a key exchange with macOS's OpenSSH 10.3 (unsupported KEX method sntrup761x25519-sha512@openssh.com), and Git for Windows'ssh-keyscansometimes hung past its own timeout. Plainsshworks with both, so identity is checked withsshitself.WebView apps expose nothing in the Simulator. A Capacitor app's content group had zero children while the screenshot showed a full page.
simulator_screenshotexists so a read can be checked against something that doesn't come from Accessibility.
Tested
With macOS 26.6 (OpenSSH 10.3) as the Mac, driven from Windows 11.
Identity: the real Mac is recognised through both Windows OpenSSH's
sshand Git for Windows'ssh. A freshly generated decoy key pinned in its place is rejected through both, andssh -vshows it stopping at the host-key check with no key offered for login. An unroutable address is reported as a connection failure, not a mismatch.Presses: on Calculator,
7was pressed once and confirmed by a separate read.All Clearpressed at 0 came back unconfirmed, correctly, so the confirmation can fail. With the press replaced by a recorder: a timeout sendsAXPressonly, "unsupported" sendsAXPressthenAXPick, and the old "any error falls through" rule is caught sending all three.Writes: a value set in a TextEdit file read back identically. Password fields: refused, zero writes.
Refusals: System Settings and the Claude app refused as described.
Linux and macOS as the driving computer should work (it only needs ssh and Python), but
haven't been tried.
Not yet tested
Presses inside a native iOS app in the Simulator. Its elements offer
AXPress; none has been sent yet.Menus and sheets that open in a separate window after a press. The confirmation only watches the pressed window, so it may say "unconfirmed" for a press that worked. Read the screen before doing anything else.
Xcode Cloud prompts. "Xcode Cloud can automate app signing and distribution" appears on Product → Archive in Xcode 26. Whether its buttons are readable here isn't known yet. Its default button is "Get Started…", so never press Enter at it blind.
Related
uia-reader does the same for native Windows windows, through UI Automation.
License
This server cannot be deployed
Maintenance
Related MCP Connectors
Securely control computers you explicitly pair through files, terminals, processes, screenshots, desktop UI/input, clipboard, browser automation, diagnostics, and document tools.
Secure access to a dedicated Otherlay Mac for files, terminals, Git, builds and UI inspection.
Securely use files, terminals, screenshots and processes on computers you pair with ReMCP.
Turns a phone into a camera+Bluetooth remote so AI assistants can see and control any PC.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceStandalone MCP server that gives AI agents full GUI control over macOS — screenshots, mouse, keyboard, apps, clipboard, and multi-display — with zero private dependencies.19MIT
- FlicenseNot gradedqualityBmaintenanceEnables AI agents to control macOS by capturing screenshots and performing mouse clicks, movement, dragging, text typing, hotkeys, and scrolling, with support for Gemini's normalized coordinate protocol and Retina display scaling.-
- FlicenseNot gradedqualityCmaintenanceEnables macOS GUI automation via screenshots, mouse/keyboard control, accessibility tree inspection, and app/window management.-
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to see and control an existing Linux X11 desktop over SSH by taking screenshots and performing mouse, scroll, typing, and keyboard-shortcut actions across browsers, terminals, file managers, and other apps.MIT