Tool reference
44 tools, generated from the compiled schemas of macos-mcp v0.2.0. Every tool returns structured JSON in structuredContent; every failure carries a stable code and what an agent needs to recover — never prose alone. Tool names and argument shapes are public API: removals need a deprecation notice one minor version ahead (the ledger).
24 tools are on by default (ui, screen, system, files). The rest are gated to keep the default surface lean; switch them on with enable_toolset at runtime or --toolsets at launch. Grants attach to the app hosting the agent (your terminal or MCP client), and macos-mcp doctor names what is missing with the exact System Settings deep-link.
| Toolset | Tools | Default | macOS permission |
ui Accessibility-first UI automation | ui_snapshot, ui_click, ui_type, ui_scroll, ui_drag, ui_shortcut, app_launch, app_quit, window_manage, wait | on | Accessibility |
screen Screenshots, OCR and find-on-screen | screenshot, ocr, screen_find | on | Screen Recording |
system Shell, AppleScript recipes, clipboard, open, Spotlight | shell, applescript_recipes, applescript_run_recipe, applescript_eval, clipboard_read, clipboard_write, open, spotlight_search | on | none (Automation per app for recipes) |
files Trash and Finder tags — never a hard delete | trash, tags | on | none |
apps Calendar, Reminders, Contacts | calendar_list, calendar_search, reminders_list, reminders_search, contacts_search | gated | Calendars / Reminders / Contacts (prompted on first use) |
intents Shortcuts and App Intents | intents_list, intents_run | gated | none (Shortcuts may prompt per shortcut) |
web Browser control: Apple's Safari MCP where available, AppleScript fallback elsewhere | web_navigate, web_current_tab, web_page_text, web_tabs, web_select_tab | gated | Automation for Safari (fallback) or Safari remote automation (Apple MCP) |
ios Control a mirrored iPhone in device points — read, tap, swipe, type, navigate | iphone_status, iphone_screenshot, iphone_find, iphone_tap, iphone_swipe, iphone_type, iphone_key, iphone_resume | gated | Screen Recording + Accessibility |
server Meta | enable_toolset | on | none |
The web toolset is either/or: where Apple's Safari MCP server mounts (Safari 27 / Safari Technology Preview with remote automation on) its 17 web_* tools are re-exported verbatim; otherwise the five AppleScript-backed tools below serve the same slot and every result says method: "fallback".
ui_snapshot maps an app's accessibility tree and stamps every element with an id of the form <pid>.<hash>. The other ui_* tools take that id — the pid rides in the handle, so they need no separate app argument, and an id from a relaunched app resolves to nothing rather than to the wrong element.
// 1. map the app
{"name": "ui_snapshot", "arguments": {"app": "TextEdit"}}
// → {"app": {...}, "node_count": 14, "root": {"children": [
// {"id": "51728.965cab4f8d4d", "role": "AXButton",
// "title": "Save", "actions": ["AXPress"], "frame": {...}}, ...]}}
// 2. act on what you found
{"name": "ui_click", "arguments": {"id": "51728.965cab4f8d4d"}}
// → {"ok": true, "method": "ax", "verified": false,
// "element": {"id": "51728.965cab4f8d4d", "role": "AXButton", "title": "Save"}}
method says which path ran: ax used the element's own accessibility action — no window raised, no pointer moved, no focus stolen — and cgevent fell back to synthetic input. verified is true when the engine re-read the UI and saw the change it intended; a click has no general "did it land" signal, typing does.
Accessibility snapshot of an app's UI: a tree of {id, role, title, value, frame, actions} with stable element ids the other ui_* tools take. Secure text fields are marked `secure` and their value is never read. Caps keep the payload small and every clip is reported in `truncated`. An app with no accessibility support returns a root with no children — use `screenshot` for those.
| Argument | Type | Required | Default | Description |
app | string | no | — | Bundle identifier or app name. Defaults to the frontmost app. |
include_menu_bar | boolean | no | false | Include the app's menu bar, which is large and rarely needed. |
max_children | integer | no | 100 | Per-element sibling cap. |
max_depth | integer | no | 12 | Deepest level walked below the app element. |
max_nodes | integer | no | 2000 | Total node budget for the snapshot. |
max_string | integer | no | 512 | Character cap for each title/value/help string. |
pid | integer | no | — | Target by process id instead of name. |
Click an element by id. Uses the element's own accessibility action where it has one — which works without raising the window, moving the pointer, or stealing focus — and falls back to a synthetic click at the element's centre. The result reports which path ran as `method`. The synthetic fallback clicks whatever is topmost on screen, so a target covered by another app's window is refused with the covering app's name — raise the target window first (window_manage raise).
| Argument | Type | Required | Default | Description |
id | string | yes | — | Element id from a previous ui_snapshot (it carries the app's pid, so no separate app argument is needed). Re-snapshot if the UI has changed since. |
button | left | right | no | "left" | Right-click prefers the element's AXShowMenu action. |
count | integer | no | 1 | 2 for a double-click. Anything above 1 uses synthetic events. |
Type text into an element by id. Sets the value through accessibility where the app accepts it, otherwise sends keystrokes, and in both cases reads the field back to confirm — a write that reported success but changed nothing comes back as an `action_unverified` error, never as success. Refuses secure text fields.
| Argument | Type | Required | Default | Description |
id | string | yes | — | Element id from a previous ui_snapshot (it carries the app's pid, so no separate app argument is needed). Re-snapshot if the UI has changed since. |
text | string | yes | — | Text to enter. |
mode | replace | append | no | "replace" | replace swaps the whole value; append inserts at the caret. |
Scroll an element by id. `page` uses the container's own accessibility scroll actions, which work without raising the window or moving the pointer; `reveal` brings an element into view; `pixels` sends wheel events. A request that would move nothing comes back as an error rather than a no-op reported as success.
| Argument | Type | Required | Default | Description |
id | string | yes | — | Element id from a previous ui_snapshot (it carries the app's pid, so no separate app argument is needed). Re-snapshot if the UI has changed since. |
dx | integer | no | 0 | Horizontal pixels; positive scrolls right. |
dy | integer | no | 0 | Vertical pixels; positive scrolls up, as on a trackpad. |
mode | pixels | page | reveal | no | "pixels" | pixels sends wheel events; page performs the element's own scroll-by-page actions in the direction of dx/dy; reveal scrolls this element into view and ignores dx/dy. |
Drag from one place to another with the pointer held down — reorder rows, move sliders, drop a file on a target. Each end is an element id (its centre) or a point in global screen coordinates (screen_find centers paste straight in). Drag effects are app-specific, so this is one of the few verbs whose result is honestly `verified: false`; re-snapshot to see what changed. A zero-length drag is refused.
| Argument | Type | Required | Default | Description |
button | left | right | no | "left" | |
duration_ms | integer | no | 300 | Gesture duration; apps ignore teleporting drags. |
from | object | no | — | Start point in global screen coordinates (top-left origin). |
from_id | string | no | — | Element id from a previous ui_snapshot (it carries the app's pid, so no separate app argument is needed). Re-snapshot if the UI has changed since. |
to | object | no | — | End point in global screen coordinates. |
to_id | string | no | — | Element id from a previous ui_snapshot (it carries the app's pid, so no separate app argument is needed). Re-snapshot if the UI has changed since. |
Send a keyboard shortcut such as "cmd+s", "cmd+shift+p" or "f5". Key positions are resolved against the active keyboard layout, so this is correct on non-US layouts. Goes to the frontmost app unless an app or pid is given.
| Argument | Type | Required | Default | Description |
keys | string | yes | — | Modifiers plus one key, '+'-separated: cmd, shift, alt/opt, ctrl, fn. The key is a single character or a name like return, tab, escape, space, delete, left, f1. |
app | string | no | — | Bundle identifier or app name to receive the keys. |
pid | integer | no | — | Target by process id instead of name. |
Launch an app by name, bundle id, or absolute path, and wait until it reports ready. Launching an app that is already running re-opens or activates it rather than starting a second copy — the result says which happened. Does not steal focus unless `activate` is set: most automation here works without the app being frontmost.
| Argument | Type | Required | Default | Description |
app | string | yes | — | App name ('TextEdit'), bundle id ('com.apple.TextEdit'), or absolute path to a .app bundle. |
activate | boolean | no | false | Bring the app to the front after launch. |
wait_ms | integer | no | 15000 | How long to wait for the app to finish launching. |
app_quit destructive — needs confirmation
Quit an app and confirm it actually exited. Graceful by default, which lets the app show its own save dialogs — if it is still running when the wait runs out, that comes back as an error naming the likely cause, never as success. `force` skips the dialogs and discards unsaved work.
| Argument | Type | Required | Default | Description |
app | string | no | — | Bundle identifier or app name. |
confirm_token | string | no | — | One-shot token from a prior confirmation_required response; authorizes this destructive call under the standard profile. |
force | boolean | no | false | Force-quit: no save dialogs, unsaved work is lost. |
pid | integer | no | — | Target by process id instead of name. |
wait_ms | integer | no | 10000 | How long to wait for the process to exit. |
Move, resize, minimize, unminimize, raise, or close a window — each verified by reading the window state back. A write the app accepted but ignored is an error, and a resize the app clamped reports the frame the window actually took. Target a window by element id (a node with role AXWindow in ui_snapshot), or by app plus an optional title substring; the default is the frontmost app's focused window. The result carries window.id — the handle for the window actually acted on. For several actions on ONE window, pass that id back rather than re-targeting by app: resolution follows focus, and minimize moves it, so an app with more than one window can otherwise hand the next call a different window. A window id stays valid for as long as the window exists — minimizing it or opening other windows does not repoint it — and once the window is gone the id resolves to a typed error, never to another window.
| Argument | Type | Required | Default | Description |
action | move | resize | minimize | unminimize | raise | close | yes | — | close presses the window's close button, so an app with unsaved changes may answer with a save sheet — the result reports whether the window is still present. |
app | string | no | — | Bundle identifier or app name. Defaults to the frontmost app. |
height | number | no | — | resize: new height, in points. |
id | string | no | — | Element id from a previous ui_snapshot (it carries the app's pid, so no separate app argument is needed). Re-snapshot if the UI has changed since. |
pid | integer | no | — | Target by process id instead of name. |
title | string | no | — | Pick the window whose title contains this, case-insensitively. |
width | number | no | — | resize: new width, in points. |
x | number | no | — | move: new left edge, in screen points. |
y | number | no | — | move: new top edge, in screen points. |
wait read-only
Wait a fixed time, or poll until a condition holds: an app running or gone, an element id resolving or not. Prefer a condition over a blind sleep — it returns the moment the condition is true, and a condition that never comes true is a `timeout` error rather than a guess.
| Argument | Type | Required | Default | Description |
app | string | no | — | App name or bundle id, for the app_* conditions. |
id | string | no | — | Element id, for the element_* conditions. |
ms | integer | no | — | Fixed sleep in milliseconds, when no condition is given. |
pid | integer | no | — | Target by process id instead of name. |
timeout_ms | integer | no | 10000 | Give up on the condition after this long. |
until | app_running | app_gone | element_exists | element_gone | no | — | Condition to poll for instead of sleeping blind. |
Capture a display, a window, or a rect as PNG (returned as image content; structuredContent carries dimensions and the captured region in screen points). Downscaled so the longest edge fits max_dimension.
| Argument | Type | Required | Default | Description |
app | string | no | — | Capture this app's window instead of a display (bundle identifier or name). With several windows open, the frontmost on-screen match wins — narrow with window_title, or re-target exactly with the window_id every result reports. |
display | integer | no | — | Display index, 0 = main. The default when no window or rect is given. |
max_dimension | integer | no | 1568 | Longest-edge pixel cap; never upscales. |
rect | object | no | — | Crop in global screen points, top-left origin — an element frame from ui_snapshot pastes straight in. Ignored when a window is targeted. |
window_id | integer | no | — | Exact window from a previous result — the precise handle. |
window_title | string | no | — | Case-insensitive title substring to pick a window; combines with app. |
ocr read-only
Read the text on a display, in a window, or inside a rect (Vision, accurate mode). Returns lines with confidence and bounding boxes in global screen points — the same space ui_snapshot frames use, so a box's center is clickable as-is. For finding one known string, screen_find is cheaper to read.
| Argument | Type | Required | Default | Description |
app | string | no | — | Capture this app's window instead of a display (bundle identifier or name). With several windows open, the frontmost on-screen match wins — narrow with window_title, or re-target exactly with the window_id every result reports. |
display | integer | no | — | Display index, 0 = main. The default when no window or rect is given. |
max_dimension | integer | no | — | Longest-edge pixel cap. Default is native resolution — downscaling costs recognition accuracy. |
rect | object | no | — | Crop in global screen points, top-left origin — an element frame from ui_snapshot pastes straight in. Ignored when a window is targeted. |
window_id | integer | no | — | Exact window from a previous result — the precise handle. |
window_title | string | no | — | Case-insensitive title substring to pick a window; combines with app. |
Find text on screen and get clickable coordinates: OCR plus fuzzy matching, tolerant of small misreads. The bridge for apps without accessibility support — matches come back best-first with center points in global screen coordinates. No match above the threshold is an element_not_found error carrying the nearest text actually seen.
| Argument | Type | Required | Default | Description |
text | string | yes | — | The text to look for — a word, label, or short phrase. |
app | string | no | — | Capture this app's window instead of a display (bundle identifier or name). With several windows open, the frontmost on-screen match wins — narrow with window_title, or re-target exactly with the window_id every result reports. |
display | integer | no | — | Display index, 0 = main. The default when no window or rect is given. |
max_dimension | integer | no | — | Longest-edge pixel cap. Default is native resolution — downscaling costs recognition accuracy. |
rect | object | no | — | Crop in global screen points, top-left origin — an element frame from ui_snapshot pastes straight in. Ignored when a window is targeted. |
threshold | number | no | 0.8 | Minimum match score; 1.0 demands an exact (case-insensitive) hit. |
window_id | integer | no | — | Exact window from a previous result — the precise handle. |
window_title | string | no | — | Case-insensitive title substring to pick a window; combines with app. |
Run a shell command with /bin/zsh -c. Returns exit_code and combined stdout+stderr (capped, `truncated` flags the cut). Times out with a typed error; long jobs should raise timeout_seconds (max 600).
| Argument | Type | Required | Default | Description |
command | string | yes | — | Command line passed to /bin/zsh -c. |
cwd | string | no | — | Working directory (default: server's cwd). |
env | object | no | — | Extra environment variables, merged over the server's. |
timeout_seconds | number | no | 30 | |
The catalog of vetted AppleScript recipes: id, description, and the parameters each one takes. Recipes are the standard way to script apps — parameterized, injection-safe, and allowed where raw applescript_eval is not.
Takes no arguments.
Run a recipe from the catalog by id with typed params. Script failures come back as ok:false with the script's own error message (and a fix hint for missing Automation permission) — like a non-zero shell exit, not a protocol error. Scripting another app prompts for Automation permission on first use.
| Argument | Type | Required | Default | Description |
recipe | string | yes | — | Recipe id from applescript_recipes. |
params | object | no | — | Recipe parameters by name; types per the recipe's schema. |
timeout_seconds | number | no | 30 | |
applescript_eval destructive — needs confirmation
Evaluate raw AppleScript or JXA source. Gated to the `full` permission profile — under `standard`, use applescript_run_recipe. Same result contract as recipes: script failures are ok:false with the script's message, not protocol errors.
| Argument | Type | Required | Default | Description |
source | string | yes | — | The script source to evaluate. |
confirm_token | string | no | — | One-shot token from a prior confirmation_required response; authorizes this destructive call under the standard profile. |
language | applescript | jxa | no | "applescript" | |
timeout_seconds | number | no | 30 | |
Read the clipboard with type awareness. Default picks the richest of string → png → file_url; pass `type` to demand one (an absent type is an element_not_found listing what the clipboard does hold). Images come back as PNG image content regardless of how the source app put them there.
| Argument | Type | Required | Default | Description |
type | string | png | file_url | no | — | Demand a specific representation instead of the default order. |
Replace the clipboard with exactly one of: text, a base64 PNG, or file paths. The write is read back from the pasteboard before reporting success.
| Argument | Type | Required | Default | Description |
file_paths | array | no | — | Absolute paths to place as file references. |
png_base64 | string | no | — | Base64-encoded image data, placed as PNG. |
text | string | no | — | Plain text to place. |
Open a file or URL with its default app, or a specific app by bundle id / .app path. Reports the app that actually took it (name + pid). To reveal a file in Finder instead of opening it, use the finder_reveal recipe.
| Argument | Type | Required | Default | Description |
target | string | yes | — | Absolute file path (or ~/…), or a URL with a scheme. |
app | string | no | — | Bundle id or absolute .app path to open the target with. |
Spotlight (mdfind) search. `query` is plain text or Spotlight query syntax (e.g. kMDItemFSName = "*.pdf"c); `name_only` matches file names. Results depend on Spotlight indexing being enabled for the searched volume.
| Argument | Type | Required | Default | Description |
query | string | yes | — | Search text or Spotlight query. |
limit | integer | no | 50 | |
name_only | boolean | no | false | Match against file names only (mdfind -name). |
only_in | string | no | — | Restrict to this directory tree. |
trash destructive — needs confirmation
Move a file or folder to the Trash — recoverable by design; this server never hard-deletes. Verified: the item is gone from its place and present in the Trash, and the result carries the trash path for undo.
| Argument | Type | Required | Default | Description |
path | string | yes | — | Absolute (or ~/…) path of the file or folder. |
confirm_token | string | no | — | One-shot token from a prior confirmation_required response; authorizes this destructive call under the standard profile. |
Read a file's Finder tags; pass `set` to replace them (empty array clears). Writes are read back before reporting success.
| Argument | Type | Required | Default | Description |
path | string | yes | — | Absolute (or ~/…) path of the file or folder. |
set | array | no | — | Replace all tags with these; omit to only read. |
The user's event calendars: id and title.
Takes no arguments.
Events in a date window (default: the coming 7 days), optionally filtered by text across title, calendar and location. Dates are ISO-8601; date-only forms like 2026-08-01 work too. First use may prompt for Calendar access, attributed to the app hosting macos-mcp.
| Argument | Type | Required | Default | Description |
from | string | no | — | Window start (ISO-8601 or YYYY-MM-DD). Default: start of today. |
limit | integer | no | 50 | |
text | string | no | — | Case-insensitive filter over title, calendar and location. |
to | string | no | — | Window end. Default: 7 days after `from`; capped at 366 days. |
Reminders, incomplete by default, optionally narrowed to one list. First use may prompt for Reminders access.
| Argument | Type | Required | Default | Description |
include_completed | boolean | no | false | |
limit | integer | no | 100 | |
list | string | no | — | Exact reminder-list name (case-insensitive). |
Reminders whose title or notes contain the text (case-insensitive), incomplete by default.
| Argument | Type | Required | Default | Description |
text | string | yes | — | Search text. |
include_completed | boolean | no | false | |
limit | integer | no | 50 | |
Contacts matching a name, with phones, emails and organization. First use may prompt for Contacts access.
| Argument | Type | Required | Default | Description |
name | string | yes | — | Name or name fragment. |
limit | integer | no | 25 | |
List the shortcuts installed on this Mac (name + identifier). These are the App Intents apps have donated; run one with intents_run. Needs a logged-in GUI session — the Shortcuts helper is unavailable from a bare SSH shell.
Takes no arguments.
Run a shortcut by name or identifier and return its output. Optional `input` is passed to the shortcut as text. Effects are the shortcut's own; this reports what it returned, empty when it produces nothing.
| Argument | Type | Required | Default | Description |
name | string | yes | — | Shortcut name or identifier from intents_list. |
input | string | no | — | Optional text input handed to the shortcut. |
Open an http(s) URL in Safari (AppleScript fallback — used when Apple's Safari MCP isn't available). Launches Safari if needed, then polls until the page settles and VERIFIES the front tab actually shows it; a redirect is reported, not hidden. Fails typed if the tab never moved.
| Argument | Type | Required | Default | Description |
url | string | yes | — | Absolute http(s) URL. javascript: and file: are refused. |
timeout_seconds | number | no | 10 | How long to wait for the page to settle. |
URL and title of Safari's frontmost tab (AppleScript fallback). Refuses rather than launching Safari when it isn't running.
Takes no arguments.
Rendered text of Safari's frontmost tab (AppleScript fallback) — the page as read, not its HTML. Capped by max_chars with `truncated` flagging the cut. Needs no developer settings; scripts and markup are not returned.
| Argument | Type | Required | Default | Description |
max_chars | integer | no | 20000 | |
Every tab of Safari's front window with its 1-based index, URL, title and whether it is current (AppleScript fallback). The indices are what web_select_tab takes.
Takes no arguments.
Make a tab of Safari's front window current, by the 1-based index from web_tabs (AppleScript fallback). Reads the current tab back and fails typed if the window ignored the switch.
| Argument | Type | Required | Default | Description |
index | integer | yes | — | 1-based tab index from web_tabs. |
Is a mirrored iPhone readable right now, and if not, what to do about it. Never fails: it reports one of ready, screen_recording_missing, app_not_installed, app_not_running, window_offscreen, no_phone_content, blocked, not_connected, needs_resume or capture_timed_out, each with the observation behind it and the single next action. Call this first, and call it after any other iphone_ tool returns refused. **The three states that come from the window's own accessibility tree** — blocked (a modal sheet is over the phone; its message and buttons are in `overlay`), not_connected (the window offers Sign In / Connect) and needs_resume (the session paused: an AXButton 'Resume' beside 'Connection Paused'; `overlay.control` names the control as observed and `overlay.signals` says which markers fired, and iphone_resume presses it) — are the ones pixels cannot see: an alert still renders something phone-shaped, so a screenshot would call it ready. **Device identification is reported with its quality.** `phone.device_match.quality` is `matched` when at least one profile fits the window's shape, `nearest` when **none** does and the reported profile is only the closest one, and `pinned` when you passed `device`. `device_candidates` is empty in the `nearest` case, which is the honest answer rather than a guess dressed as a match; taps still land correctly because the mapping is proportional, but the reported point size is approximate until you pin one. Note that a locked-and-far-away phone, an unfinished setup screen, a phone being used in your hand, and a region where iPhone Mirroring is unavailable all look identical from outside the app — they share the no_phone_content state, whose fix lists all four. This tool is focus-free, so it never reports focus_unavailable; only the tools that act on the phone can see that one.
| Argument | Type | Required | Default | Description |
device | string | no | — | Pin the device profile (e.g. "iphone-16-pro") instead of inferring it from the mirrored window's shape. iphone_status lists the ids that fit. Inference is proportional and lands taps correctly either way; pin this when you need device points to be exact. |
Capture the mirrored iPhone's screen as PNG — cropped to the phone itself, never the surrounding window or its letterbox bars. structuredContent carries the device's logical size in points, the scale it is being shown at, and the device profiles that fit its shape. Read-only and focus-free: it takes nothing from the app you are in.
| Argument | Type | Required | Default | Description |
device | string | no | — | Pin the device profile (e.g. "iphone-16-pro") instead of inferring it from the mirrored window's shape. iphone_status lists the ids that fit. Inference is proportional and lands taps correctly either way; pin this when you need device points to be exact. |
max_dimension | integer | no | 1568 | Longest-edge pixel cap; never upscales. |
Find text on the mirrored iPhone and get coordinates in the phone's own space: OCR plus fuzzy matching, tolerant of small misreads. Matches come back best-first with a center in **device points** — the frame of reference that survives a window resize, a different Mac, and a transcript replayed tomorrow — alongside the global screen point that currently corresponds to it. Text outside the phone (window chrome, a notification lying over it) is dropped rather than returned. No match above the threshold is an element_not_found error carrying the nearest text actually seen. There is no accessibility tree behind iPhone Mirroring — the content is an opaque video surface — so this is the primary way to locate anything on the phone.
| Argument | Type | Required | Default | Description |
text | string | yes | — | The string to look for. Case- and diacritic-insensitive. |
device | string | no | — | Pin the device profile (e.g. "iphone-16-pro") instead of inferring it from the mirrored window's shape. iphone_status lists the ids that fit. Inference is proportional and lands taps correctly either way; pin this when you need device points to be exact. |
threshold | number | no | 0.8 | Minimum fuzzy score, 0–1. Lower it for noisy or stylised text. |
iphone_tap destructive — needs confirmation
Tap the mirrored iPhone at a point in **device points** — the space iphone_find returns, which survives a window resize. Captures the phone before and after and reports whether the screen actually changed: a tap that changed nothing is an action_unverified error, never a false success. A change proves something happened, not that this tap caused it — a notification arriving would also move pixels — so treat it as evidence, and read the screen back with iphone_screenshot when it matters. Takes keyboard and mouse focus for the length of the call: macOS sends synthetic input to the frontmost app only, so this cannot run alongside other ui_ automation on this Mac and will interrupt whoever is at the keyboard.
| Argument | Type | Required | Default | Description |
x | number | yes | — | Device-point x, from iphone_find's device_center. |
y | number | yes | — | Device-point y. |
confirm_token | string | no | — | One-shot token from a prior confirmation_required response; authorizes this destructive call under the standard profile. |
count | integer | no | 1 | 2 for a double-tap (zoom, text selection). |
device | string | no | — | Pin the device profile (e.g. "iphone-16-pro") instead of inferring it from the mirrored window's shape. iphone_status lists the ids that fit. Inference is proportional and lands taps correctly either way; pin this when you need device points to be exact. |
min_change | number | no | — | Fraction of the phone's screen that must look different for the action to count as verified, 0–1. Compared on a coarse grid so video compression noise does not register. Raise it on a screen with live content (a video, an animated wallpaper) that changes on its own. |
settle_ms | integer | no | — | How long to keep watching for the screen to change, in milliseconds. Polled, not slept: a landed action returns as soon as it shows. Raise it for a slow network or a long animation; the phone arrives as a video stream, so nothing here is instant. |
iphone_swipe destructive — needs confirmation
Swipe or scroll the mirrored iPhone between two points in **device points**. `drag` mode presses, moves and releases — a finger on the glass, which is what dismisses a sheet or pulls to refresh. `scroll` mode sends wheel events instead, which is what Apple's own iPhone Mirroring documentation points at for moving a list, and is often the one that works when a drag does not. Verified the same way as iphone_tap: no change on screen is an action_unverified error rather than a reported success. Takes focus for the length of the call.
| Argument | Type | Required | Default | Description |
from | object | yes | — | Where the finger goes down, in device points. |
to | object | yes | — | Where it lifts. |
confirm_token | string | no | — | One-shot token from a prior confirmation_required response; authorizes this destructive call under the standard profile. |
device | string | no | — | Pin the device profile (e.g. "iphone-16-pro") instead of inferring it from the mirrored window's shape. iphone_status lists the ids that fit. Inference is proportional and lands taps correctly either way; pin this when you need device points to be exact. |
duration_ms | integer | no | 300 | How long the drag takes. Short is a flick, long is a deliberate drag; iOS reads the difference. Ignored in scroll mode. |
min_change | number | no | — | Fraction of the phone's screen that must look different for the action to count as verified, 0–1. Compared on a coarse grid so video compression noise does not register. Raise it on a screen with live content (a video, an animated wallpaper) that changes on its own. |
mode | drag | scroll | no | "drag" | drag = a finger on the glass; scroll = wheel events. |
settle_ms | integer | no | — | How long to keep watching for the screen to change, in milliseconds. Polled, not slept: a landed action returns as soon as it shows. Raise it for a slow network or a long animation; the phone arrives as a video stream, so nothing here is instant. |
iphone_type destructive — needs confirmation
Type text into whatever has keyboard focus on the mirrored iPhone. **The result is honestly unverified and says so**: iOS hides the on-screen keyboard while Mirroring is active and the mirrored content has no accessibility tree, so there is nothing on this Mac that can read the field back. The body carries verification.method = "none" — branch on that, not on ok — plus whether the screen changed at all, which is weak evidence the keystrokes arrived somewhere. To confirm the text landed, call iphone_screenshot and look. Tap the field first: this types wherever focus already is, and if nothing has focus the keystrokes are discarded silently. Known Apple defect, mitigated but not fixed here: iPhone Mirroring intermittently latches a modifier and returns text in alternating case — this tool releases every modifier first and never sends Shift, and reports the risk rather than claiming immunity.
| Argument | Type | Required | Default | Description |
text | string | yes | — | The text to send. Sent as Unicode, so emoji and non-Latin scripts work. |
clear_modifiers | boolean | no | true | Release every modifier before typing, the mitigation for the alternating-case defect. Turn it off only if it interferes. |
confirm_token | string | no | — | One-shot token from a prior confirmation_required response; authorizes this destructive call under the standard profile. |
device | string | no | — | Pin the device profile (e.g. "iphone-16-pro") instead of inferring it from the mirrored window's shape. iphone_status lists the ids that fit. Inference is proportional and lands taps correctly either way; pin this when you need device points to be exact. |
settle_ms | integer | no | — | How long to keep watching for the screen to change, in milliseconds. Polled, not slept: a landed action returns as soon as it shows. Raise it for a slow network or a long animation; the phone arrives as a video stream, so nothing here is instant. |
iphone_key destructive — needs confirmation
Press one of iPhone Mirroring's navigation keys. **home, app_switcher and spotlight send Apple's documented Command-1 / Command-2 / Command-3**, each measured on a real iPhone. home and app_switcher additionally have named buttons in the window's own toolbar (AXButton 'Home Screen' and AXButton 'App Switcher', both taking AXPress) and those are pressed when they are there — but the toolbar auto-hides and a hidden toolbar exposes no children at all, so in a normal session they are not, and the shortcut is the ordinary path rather than a degraded one. The result says which ran: `method` is "ax_press" or "shortcut", and `ax_press` reports whether the control was available and whether it was used. The other two are not shortcuts either — back sends iOS's left-edge swipe gesture, because iPhone Mirroring documents no back key; lock **ends the mirroring session** (it closes the window), because Mirroring only ever runs against an iPhone that is already locked and nearby, so there is no phone to lock from here. Verified like every other mutating verb — the screen has to change, and for lock the session has to actually end — so a shortcut that Apple changes in a future macOS surfaces as action_unverified rather than as a silent no-op. Takes focus for the length of the call.
| Argument | Type | Required | Default | Description |
key | home | app_switcher | spotlight | back | lock | yes | — | Which navigation key to press. |
confirm_token | string | no | — | One-shot token from a prior confirmation_required response; authorizes this destructive call under the standard profile. |
device | string | no | — | Pin the device profile (e.g. "iphone-16-pro") instead of inferring it from the mirrored window's shape. iphone_status lists the ids that fit. Inference is proportional and lands taps correctly either way; pin this when you need device points to be exact. |
min_change | number | no | — | Fraction of the phone's screen that must look different for the action to count as verified, 0–1. Compared on a coarse grid so video compression noise does not register. Raise it on a screen with live content (a video, an animated wallpaper) that changes on its own. |
settle_ms | integer | no | — | How long to keep watching for the screen to change, in milliseconds. Polled, not slept: a landed action returns as soon as it shows. Raise it for a slow network or a long animation; the phone arrives as a video stream, so nothing here is instant. |
iphone_resume destructive — needs confirmation
Press the `Resume` control iPhone Mirroring shows when it has paused itself, then report what the window did about it. Mirroring suspends the session after inactivity and shows an AXButton 'Resume' beside 'Connection Paused'; iphone_status reports that as `needs_resume`. **Branch on `outcome`, not on ok** — ok only means the control was pressed. `outcome` is "resumed" when the paused markers are gone, or "authentication_required" when the window is still paused or a macOS authentication prompt has taken the screen. Both really happen: a measured resume needed no password at all, and the same Mac asks for one on other occasions, so neither is assumed. **This tool never answers that prompt.** Passwords and Touch ID are a hard line here, and SecurityAgent cannot be scripted in any case; when authentication is needed a person does it, and the result says so. Verification is the window's own accessibility tree changing state, never the press's return value — the AX click that resumed a real session reported verified:false and was right to. A tree that cannot be read back at all is action_unverified. If no pause control can be identified, this refuses and lists every control it did see rather than pressing a guess.
| Argument | Type | Required | Default | Description |
confirm_token | string | no | — | One-shot token from a prior confirmation_required response; authorizes this destructive call under the standard profile. |
settle_ms | integer | no | — | How long to keep re-reading the window's accessibility tree for the paused markers to clear, in milliseconds. Polled, not slept. When the budget runs out with the window still paused, that is the authentication_required answer, not a failure. |
Enable a gated toolset — its tools join the tool list at once and a list_changed notification follows. Toolsets: ui (enabled, 10 tools, needs accessibility); screen (enabled, 3 tools, needs screen_recording); system (enabled, 8 tools, no TCC grants); apps (enabled, 5 tools, needs calendars, reminders, contacts); files (enabled, 2 tools, no TCC grants); intents (enabled, 2 tools, no TCC grants); ios (enabled, 8 tools, needs screen_recording, accessibility); web (enabled, 5 tools, needs automation).
| Argument | Type | Required | Default | Description |
toolset | string | yes | — | Toolset name to enable. |
Error codes
Every failure carries one of these codes. They are wire-stable: renames need a deprecation entry.
| Code | Means | What to do |
permission_missing | A required TCC grant is missing. | fix holds the exact System Settings deep-link; grant it and restart the client. |
element_not_found | An id, app or key did not resolve. | nearest_matches lists what is there — often ids visible right now. Re-snapshot and retry. |
invalid_argument | A value failed semantic validation the schema can't express — unknown recipe param, enum violation, type mismatch. | param names the offender; expected says what would have been accepted. Fix the argument, don't retry blind. |
refused | A hard line. The server will not do this. | reason says why, alternative points at the legitimate route. Do not retry. |
action_unverified | It ran, reported success, and nothing changed. | expected vs actual show the gap. Re-snapshot: the UI is not where you thought. |
timeout | The operation outlived its budget. | Retry with a longer timeout, or check the app is responding. |
confirmation_required | A destructive action needs a confirm token. | Re-issue with the token once the human has approved. |
upstream_unavailable | A mounted upstream (Safari/Xcode MCP) is down. | enable_hint names the setting to switch on. |
machine_unreachable | Fleet: the target machine is not answering. | Check last_seen; the agent may be offline. |
host_at_capacity | Fleet: the host is at its macOS-VM ceiling (2 per Apple Silicon host — a kernel quota). | Schedule the clone on another host; more RAM will not help. |
lease_held | Fleet: another client holds the machine. | expires says when it frees. |
Safety properties worth knowing
- Secure text fields are never read and never typed into — a typed
refused carrying an alternative, not a best-effort attempt. - Requests that could only be no-ops are refused up front rather than performed: a zero-delta scroll, a click on a disabled control, a reveal on an element that cannot scroll.
- Every call appends to
~/.macos-mcp/audit.jsonl. Arguments are stored as a SHA-256 digest, never raw — typed text can be a secret. - No telemetry. Nothing is sent anywhere. The full safety model →