macctl
macctl is an accessibility-first macOS desktop automation MCP server that lets AI clients see the screen, inspect app accessibility trees, control mouse/keyboard, manage windows, and run commands, all behind tiered permissions and audit logging.
README
macctl
Accessibility-first macOS desktop automation for Claude and other MCP clients.
macctl gives an MCP client first-class access to a Mac: it can see the screen, read the accessibility tree of any application to identify controls by role/title/identifier (not just pixels), drive the mouse and keyboard, manage windows, inspect processes and launchd services, work with files and the clipboard, and run shell commands — all gated behind explicit permission tiers, with a local audit log of every call. It is the macOS counterpart to winctl, Sitharaj's Windows equivalent.
Why macctl
- Accessibility-tree-first, not screenshot-and-click.
ax_snapshotandax_findlet a client locate "the Save button" or "the text field named Subject" by role/title/identifier viaAXUIElement, instead of guessing pixel coordinates from a screenshot and hoping the layout doesn't shift. - Correct global-point coordinates. Every coordinate macctl reports or
accepts is a global screen point in the CG coordinate space (top-left
origin of the main display), with each display's
backingScaleFactorreported alongside — no silent pixel/point confusion across Retina and external displays. - Reliable, paced input. Keystrokes are synthesized layout-independently; text longer than ~200 characters is delivered via a clipboard paste (with the prior clipboard contents restored afterward) instead of hundreds of individually racy synthetic keypresses.
- Stable, opaque handles.
windowHandleandelementHandlevalues are fingerprinted (pid + window/element identity) rather than raw pointers, so a handle from one call either still resolves correctly on the next call or fails cleanly — it never silently targets the wrong window. - Honest, verified failures. Window moves are read back and the verified
final frame is returned (apps clamp geometry); TCC failures come back as a
structured
PERMISSION_MISSINGerror naming the exact permission and the exact app to grant it to, not a generic timeout. - Tiered permissions + audit log. Every tool belongs to one of five permission tiers. Tools in a disabled tier are never registered with the MCP client — it cannot see or attempt them, not just be told no. Every call is appended to a local, redacted JSONL audit log.
Requirements
- macOS 14 (Sonoma) or later recommended; built and tested on macOS 26.
- Apple Silicon or Intel — macctl ships a universal (arm64 + x86_64) helper binary, no compilation required on your machine.
- Node.js 20+.
Grant permissions
macctl's native tools (window management, input, accessibility, screen capture) need macOS permissions granted through System Settings → Privacy & Security, not through macctl itself. This is the single most common source of confusion, so read this before anything else:
The permission grant attaches to whichever app launches macctl — Claude Desktop, Terminal, iTerm2, VS Code, or whatever process ultimately spawns
node dist/index.js— never tonodeormacctlthemselves. If you grant Accessibility to the wrong app, or grant it and then keep running the old process, tools will keep failing withPERMISSION_MISSING.
Steps:
-
Run the doctor tool to find out exactly which app to grant, and get a direct System Settings link for each permission:
npx @sitharaj88/macctl doctorThis prints the responsible app (identified by walking the process tree up to the nearest
.appbundle), the current status of Accessibility, Screen Recording, and Full Disk Access, and ax-apple.systempreferences:deep link for each one that isn't granted yet. -
Open System Settings → Privacy & Security and grant:
- Accessibility — required for window management, mouse/keyboard
input, and the
ax_*accessibility tools. - Screen Recording — required for
capture,capture_region,capture_window, andlist_capturable_windows. - Full Disk Access — only needed if you point
MACCTL_ALLOWED_PATHSoutside your user-visible home folders.
- Accessibility — required for window management, mouse/keyboard
input, and the
-
Restart the host app (Claude Desktop, your terminal, etc.) after granting. macOS does not apply a fresh TCC grant to an already-running process.
You can also trigger the native grant dialogs directly instead of only getting deep links:
npx @sitharaj88/macctl doctor --prompt
Tools that hit a missing permission at runtime return a structured
PERMISSION_MISSING error with the same "which app, which permission, which
link" detail — call system_doctor again after granting to confirm.
Installation
Claude Desktop (recommended)
Download the latest macctl.mcpb from
GitHub Releases and
double-click it. The bundled helper binary is Developer ID signed and
notarized, so Gatekeeper accepts it without extra steps on a fresh download.
Claude Code
claude mcp add macctl -- npx -y @sitharaj88/macctl
Manual MCP client configuration
Add an entry to your client's MCP server config (Claude Desktop's
claude_desktop_config.json, or the equivalent for your client):
{
"mcpServers": {
"macctl": {
"command": "npx",
"args": ["-y", "@sitharaj88/macctl"]
}
}
}
Then follow Grant permissions above and restart the client.
Permission tiers & profiles
Every tool belongs to exactly one tier. A tool in a disabled tier is never
registered with the MCP client — this is an allowlist enforced at
tools/list time, not a runtime check the model could talk its way around.
| Tier | Covers |
|---|---|
observe |
Read-only: screenshots, window/AX inspection, system/process info, file reads. |
interact |
Mouse/keyboard input, window focus/move/close, AX actions, clipboard. |
filesystem |
Writing, deleting, moving, and creating files/directories. |
manage |
Process start/kill, launchd service control, notifications. |
shell |
Arbitrary shell commands via shell_run — unsandboxed, runs with the host process's own OS permissions. |
Profiles bundle tiers together:
| Profile | Tiers enabled |
|---|---|
readonly |
observe |
standard (default) |
observe, interact, filesystem |
full |
observe, interact, filesystem, manage, shell |
system_doctor is always registered regardless of profile — it's how you
diagnose everything else.
Set the profile via MACCTL_PROFILE, or bypass profiles entirely with an
explicit tier list via MACCTL_TIERS (e.g. MACCTL_TIERS=observe,interact),
which overrides the profile's tier set completely rather than adding to it.
MACCTL_PROFILE=readonly npx @sitharaj88/macctl # observation only
MACCTL_PROFILE=full npx @sitharaj88/macctl # everything, incl. shell
MACCTL_TIERS=observe,interact npx @sitharaj88/macctl # explicit override
Configuration
All configuration is via environment variables, read once at startup:
| Variable | Default | Description |
|---|---|---|
MACCTL_PROFILE |
standard |
readonly | standard | full. See profiles above. |
MACCTL_TIERS |
(unset) | Comma-separated tier list (observe,interact,filesystem,manage,shell) that, if set, replaces the profile's tier set entirely. |
MACCTL_ALLOWED_PATHS |
(unset → home dir, /tmp, /private/tmp, /Volumes) |
Colon-separated (:) list of directories file/exec-path tools are confined to. |
MACCTL_DENIED_PATHS |
(unset) | Colon-separated (:) list of additional directories to deny, layered on top of the built-in denylist (~/Library/Keychains, ~/.ssh, ~/Library/Application Support/com.apple.TCC, ~/Library/Containers, ~/Library/Group Containers, ~/Library/Cookies, ~/Library/Safari, ~/Library/Mail, /private/var/db). Denied always wins over allowed. |
MACCTL_CONFIRM_DESTRUCTIVE |
true |
If true, destructive tools require an explicit confirm: true argument. Set to 0/false/no/off to disable. |
MACCTL_AUDIT_LOG |
~/Library/Logs/macctl/audit.jsonl |
Path to the audit log file. |
MACCTL_AUDIT_DISABLED |
false |
Disable audit logging entirely. |
MACCTL_MAX_IMAGE_WIDTH |
1600 |
Screenshots wider than this (in pixels) are downscaled by the helper before being returned. |
MACCTL_COMMAND_TIMEOUT_MS |
60000 |
Default timeout for shell_run, overridable per call. |
MACCTL_HELPER_PATH |
(unset → auto-resolved next to the installed package, native/bin/macctl-helper) |
Explicit path to the native helper binary — mainly for local development (scripts/build-helper.sh --dev). |
Complete tool reference
41 tools total. Tier is shown per tool; destructive tools additionally
require confirm: true when MACCTL_CONFIRM_DESTRUCTIVE is enabled
(the default).
Doctor (always registered, any profile)
| Tool | Tier | Description |
|---|---|---|
system_doctor |
observe | TCC permission status, responsible app, native helper reachability, active profile/tiers, audit log location, version info. |
Screen (5)
| Tool | Tier | Description |
|---|---|---|
list_monitors |
observe | List connected displays with frame, visible frame, backing scale factor, and which is main. |
capture |
observe | Screenshot a full display (main by default). |
capture_region |
observe | Screenshot a rectangular region in global screen points. |
capture_window |
observe | Screenshot a single window by windowHandle. |
list_capturable_windows |
observe | List on-screen windows available for capture_window, via ScreenCaptureKit. |
Windows (7)
| Tool | Tier | Description |
|---|---|---|
window_list |
observe | List all on-screen windows with title, owning app, pid, bounds, layer, minimized state, and a windowHandle. |
window_get_active |
observe | Get the frontmost app and its focused window. |
window_get_desktop_info |
observe | Get screen layout, light/dark appearance, and cursor position. |
window_focus |
interact | Activate a window's owning app and raise the window. |
window_set_state |
interact | Minimize, restore, maximize, fullscreen, hide, or show a window. |
window_move |
interact | Move and/or resize a window; returns the verified final frame. |
window_close |
interact, destructive | Close a window by pressing its AXCloseButton. |
Input (7)
| Tool | Tier | Description |
|---|---|---|
input_move_mouse |
interact | Move the mouse cursor to a global screen point. |
input_click |
interact | Click at a global screen point (left/right/middle, single/double/triple). |
input_drag |
interact | Press, drag through interpolated points, and release. |
input_scroll |
interact | Post a scroll-wheel event, optionally moving the cursor first. |
input_type |
interact | Type Unicode text at the current keyboard focus (layout-independent; paste for long text). |
input_press_keys |
interact | Press a keyboard chord, e.g. "cmd+shift+4". |
input_key_hold |
interact | Hold a single key down or release it (auto-releases after 30s). |
Accessibility (4)
| Tool | Tier | Description |
|---|---|---|
ax_snapshot |
observe | Walk the AXUIElement tree of an app or window (role/title/value/description/identifier/enabled/frame per node). |
ax_find |
observe | Bounded search over an app's/window's tree by role, title/value substring, and/or identifier. |
ax_invoke |
interact | Perform an accessibility action on an element (default AXPress). |
ax_set_value |
interact | Set an element's value directly, or via focus + Cmd+A + type fallback, with verified read-back. |
System & Processes (8)
| Tool | Tier | Description |
|---|---|---|
system_info |
observe | CPU, memory, disk, network, battery, graphics, macOS version, hardware model. |
list_services |
observe | List launchd services: running (launchctl list) joined with installed LaunchAgents/LaunchDaemons. |
list_installed_apps |
observe | List installed applications: name, bundle id, version, path. |
process_list |
observe | List running processes (pid, ppid, cpu%, mem%, rss, elapsed, command). |
control_service |
manage, destructive | Restart/stop/start/enable/disable a launchd LaunchAgent. LaunchDaemons (system domain) refuse with PRIVILEGE_REQUIRED. |
notify |
manage | Show a macOS notification banner. |
process_start |
manage | Launch an app (open -a/open -b), open a document/URL, or spawn a bare executable. |
process_kill |
manage, destructive | Terminate a process by pid (SIGTERM, optionally escalating to SIGKILL). |
Files, Clipboard & Shell (9)
| Tool | Tier | Description |
|---|---|---|
file_known_folders |
observe | List well-known macOS folders (home, Desktop, Documents, Downloads, iCloud Drive, etc.) with existence/allow-list status. |
file_list |
observe | List a directory's entries, optionally recursive with a depth cap. |
file_read |
observe | Read a file as utf8 text or base64, capped at maxBytes. |
file_search |
observe | Search a directory tree by name glob and/or content regex, or accelerated via mdfind. |
file_write |
filesystem, destructive | Write, append to, or create a text/base64 file. |
file_manage |
filesystem, destructive | Copy, move, delete a file/directory, or mkdir -p. |
clipboard_read |
interact | Read clipboard text, file references, and image presence. |
clipboard_write |
interact | Replace clipboard contents with text or file references. |
shell_run |
shell | Run a command via /bin/zsh -c, capturing stdout/stderr/exit code. |
Example workflow
A realistic multi-tool sequence — opening TextEdit, finding its text area via the accessibility tree (not coordinates), typing, and verifying visually:
1. process_start { app: "TextEdit" }
2. window_get_active → windowHandle for the new TextEdit window
3. ax_find { windowHandle, role: "AXTextArea" }
→ elementHandle for the document's text area
4. ax_invoke { elementHandle } # focus/click it
5. input_type { text: "Meeting notes...\n\n- Discuss Q3 roadmap" }
6. ax_snapshot { windowHandle, maxDepth: 5 } # read the value back, verify it landed
7. capture_window { windowHandle } # visual confirmation
8. window_move { windowHandle, x: 100, y: 100 }
9. input_press_keys { keys: "cmd+s" } # save
10. window_close { windowHandle, confirm: true } # destructive — needs confirm
Each step uses a real tool name and an opaque handle produced by an earlier step — never a guessed coordinate or a raw pointer.
Safety & audit
- Destructive-action confirmation.
window_close,file_write,file_manage,control_service, andprocess_killare marked destructive. WhenMACCTL_CONFIRM_DESTRUCTIVEis enabled (the default), each call must includeconfirm: trueor it's refused withCONFIRMATION_REQUIRED— no first-try accidental deletes. - Path containment. File and executable-path tools resolve the real
(symlink-followed) path and check it against
MACCTL_ALLOWED_PATHS/MACCTL_DENIED_PATHS, with a built-in denylist (Keychains,~/.ssh, TCC store, app containers, Safari/Mail data,/private/var/db) that always wins over anything allowed. - No silent privilege escalation. macctl never shells out to
sudo. Actions that would need elevated privileges (e.g. controlling a system-domainLaunchDaemon) fail honestly withPRIVILEGE_REQUIREDrather than prompting for or assuming root. - Audit log. Every tool call is appended as one JSON line to
~/Library/Logs/macctl/audit.jsonl(configurable, or disable withMACCTL_AUDIT_DISABLED=1): timestamp, tool name, outcome, duration, error code, and redacted arguments (long strings and base64-looking blobs are replaced with[redacted N chars]before being written).
Privacy
See PRIVACY.md for the full policy. In short: macctl runs entirely locally and has no telemetry. Screenshots, accessibility-tree contents, clipboard data, and file contents are returned only to the MCP client that requested them — nothing is sent anywhere by macctl itself.
Development
git clone https://github.com/sitharaj88/macctl.git
cd macctl
npm install
npm run build:all # tsc + universal Swift helper build (native/bin/macctl-helper)
npm run build:helper (via scripts/build-helper.sh) builds the Swift
helper as a universal (arm64 + x86_64) binary using Swift Package Manager;
run it with --dev for a fast, current-arch-only build during local
iteration.
npm run smoke # spawns the server, exercises TCC-free tools across all 3 profiles
npm run smoke:full # also exercises TCC-dependent tools (skips gracefully if ungranted)
node scripts/verify-interactive.mjs # full TextEdit round-trip — needs a real GUI session + Accessibility/Screen Recording grants
verify-interactive.mjs drives an actual TextEdit window end-to-end (open →
find the text area → type → read the value back → screenshot → move →
close), so it must run in a real logged-in GUI session with permissions
already granted to your terminal — it will not work over SSH or in CI.
Publishing
- npm:
npm publish --access public - .mcpb bundle:
npx @anthropic-ai/mcpb pack(respects.mcpbignore; producesmacctl.mcpbfor GitHub Releases / Claude Desktop's drag-and-drop install). - MCP registry:
mcp-publisher publish(usingserver.json).
👤 Author
Sitharaj Seenivasan
- 🌐 Website: sitharaj.in
- 💼 LinkedIn: sitharaj08
- 💻 GitHub: sitharaj88
☕ Support
If this project helps you, consider buying me a coffee — it keeps the work going.
📄 License
Licensed under the Apache License 2.0. © 2026 Sitharaj Seenivasan.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。