npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sitharaj88/macctl

v0.1.0

Published

macOS desktop-automation MCP server — accessibility-tree-first control of windows, input, screen capture, and the system, exposed over the Model Context Protocol.

Readme

macctl

npm version license platform

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_snapshot and ax_find let a client locate "the Save button" or "the text field named Subject" by role/title/identifier via AXUIElement, 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 backingScaleFactor reported 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. windowHandle and elementHandle values 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_MISSING error 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.jsnever to node or macctl themselves. If you grant Accessibility to the wrong app, or grant it and then keep running the old process, tools will keep failing with PERMISSION_MISSING.

Steps:

  1. 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 doctor

    This prints the responsible app (identified by walking the process tree up to the nearest .app bundle), the current status of Accessibility, Screen Recording, and Full Disk Access, and a x-apple.systempreferences: deep link for each one that isn't granted yet.

  2. 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, and list_capturable_windows.
    • Full Disk Access — only needed if you point MACCTL_ALLOWED_PATHS outside your user-visible home folders.
  3. 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, and process_kill are marked destructive. When MACCTL_CONFIRM_DESTRUCTIVE is enabled (the default), each call must include confirm: true or it's refused with CONFIRMATION_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-domain LaunchDaemon) fail honestly with PRIVILEGE_REQUIRED rather 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 with MACCTL_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; produces macctl.mcpb for GitHub Releases / Claude Desktop's drag-and-drop install).
  • MCP registry: mcp-publisher publish (using server.json).

👤 Author

Sitharaj Seenivasan

☕ Support

If this project helps you, consider buying me a coffee — it keeps the work going.

Buy Me A Coffee

📄 License

Licensed under the Apache License 2.0. © 2026 Sitharaj Seenivasan.