
Wraps macOS system APIs in 13 tools that let Claude control your desktop directly. You get app and window management (launch, quit, focus, move, resize), audio routing via SwitchAudioSource, volume and appearance toggles, Focus mode control, and screenshot capture with optional base64 previews. It pulls system state like battery level, Wi-Fi SSID, and display count, and hooks into Finder for selection info and trash operations. Requires Accessibility for window manipulation, Screen Recording for window screenshots. Built for stdio on a local Mac, though it supports HTTP. Reach for this when you want Claude to orchestrate your desktop environment instead of just reading files.
Control macOS system settings, apps, windows, audio, displays, screenshots, and Focus mode via MCP. STDIO or Streamable HTTP.
macOS-only. This server controls the local macOS system — it requires the host machine to be running macOS. HTTP transport is supported for completeness, but the practical use case is stdio: run it locally and point your MCP client at it.
macOS system control — application lifecycle, window management, audio and display routing, screenshots, Finder integration, notifications, and Focus mode. Launch, quit, and arrange apps and windows, switch audio devices, capture screenshots, and toggle Focus mode from any MCP client. Runs as a stdio process or a local Streamable HTTP server.
| Tool | Description |
|---|---|
macos_get_info | System snapshot: battery level and charging status, power source, Wi-Fi SSID, hostname, macOS version, uptime, and display count |
macos_check_permissions | Reports Accessibility, Screen Recording, Automation > Finder, and Notification permission status for the calling process |
macos_manage_apps | List running apps, get the frontmost app, launch, quit, force-quit, hide, or show applications |
macos_manage_windows | List, focus, move, resize, move_resize, minimize, fullscreen, or close windows across all visible apps |
macos_control_volume | Get or set system output volume (0–100) and mute state |
macos_control_audio | List audio devices, get current defaults, or switch the default input/output device |
macos_control_appearance | Get or set dark/light mode |
macos_control_system | Lock the screen or put the display to sleep |
macos_take_screenshot | Capture full screen, display, named app window, or pixel region; saves PNG, optional base64 JPEG preview |
macos_manage_displays | List connected displays and apply named display layout presets |
macos_send_notification | Post a notification to macOS Notification Center |
macos_manage_focus | Get or set Do Not Disturb / Focus mode |
macos_manage_finder | Frontmost path, current selection, reveal, open with app, or move to Trash |
| Resource | Description |
|---|---|
macos://system/info | Current macOS system snapshot: battery, power source, Wi-Fi SSID, hostname, version, uptime, display count |
macos://audio/devices | All audio input and output devices, including which is the current default. Requires SwitchAudioSource CLI. |
macos://displays | Connected display inventory including persistent IDs, type, resolution, origin, rotation, scaling, and enabled state. Requires displayplacer CLI. |
Resource data is also accessible via macos_get_info, macos_control_audio (action=list), and macos_manage_displays (action=list).
macos_get_info toolAC, Battery, UPS); null on desktops with no battery"15.1.0"), uptime in secondsmacos_check_permissions tool"ghostty", "node") so you know which process to grant permissions formacos_manage_apps toollist — running user-facing apps with name, bundle ID, PID, visible, and frontmost flagsfrontmost — name, bundle ID, PID, and frontmost window title of the active applaunch — open or activate by app_name or bundle_id; hidden=true starts in the backgroundquit (graceful AppleScript quit) vs. force_quit (SIGKILL, no save prompt)hide / show — toggle visibility; requires Accessibilityquit, force_quit, hide, and show first check that the app is running — resolved by application name the way tell application does ("Visual Studio Code" finds the Code process), without launching it — and return not_running when it isn'tapp_not_found (launch of an app that isn't installed), no_frontmost_app, not_running, accessibility_requiredmacos_manage_windows toollist — all visible windows across apps, with position, size, minimized state, and 0-based display_indexfocus, move, resize, move_resize, minimize, fullscreen, close — target by app_name or exact window_title (title takes precedence when both are given)move needs x/y, resize needs width/height (both greater than 0), move_resize needs all fourlist and focus require no permissions; every other action requires Accessibilitywindow_not_found, accessibility_requiredmacos_control_volume toolget — current output volume (0–100) and mute stateset — requires level (0–100), muted, or both; level=0 does not muteaction echoedmacos_control_audio toollist — all input/output devices with an is_default flag; filter with typecurrent — default input and output device namesswitch_output / switch_input — case-insensitive partial name match ("MacBook" matches "MacBook Pro Microphone")macos_control_volume)brew install switchaudio-osx); typed errors device_not_found, switchaudio_unavailablemacos_control_appearance toolget — returns dark_mode: true/false, with the action echoedset requires mode: "dark" | "light" | "toggle" — dark/light are idempotent, toggle flips on each callaccessibility_required when Automation > System Events is deniedmacos_control_system toollock — ⌃⌘Q via Accessibility; falls back to the ScreenSaverEngine binary when Accessibility isn't grantedsleep_display — pmset displaysleepnow; no permissions requiredmacos_take_screenshot tooltarget: screen, display (0-based integer display_index, default 0), region (pixel rect, width/height greater than 0) — no Screen Recording required; window (by app_name) requires Screen Recordingpath defaults to MACOS_SCREENSHOT_DIR/<timestamp>.png, falling back to ~/Desktop; a custom path must be within ~/Desktop, /tmp, or the home directoryinclude_data=true adds a base64 JPEG preview (max 1024px wide, ~70% quality) plus preview_width/preview_heightscreen_recording_required, window_not_found, display_not_found, path_not_writablemacos_manage_displays toollist — persistent ID, connection type, resolution, refresh rate, origin, rotation, scaling, and enabled state, plus current_config (a displayplacer command that reproduces the active arrangement)apply_layout — activates a named preset from MACOS_DISPLAY_LAYOUTS; raw displayplacer args are never accepted from the callerbrew install jakehilborn/jakehilborn/displayplacer); typed errors displayplacer_not_found, layout_not_foundmacos_send_notification tooltitle required; body, subtitle, and sound=true (default notification sound) are optionalmacos_manage_focus toolget — best-effort; reads the Focus assertion database when accessible, returns status: "active" | "inactive" | "unknown"; unknown is expected on macOS 13+ where the database is SIP-protectedset — requires the built-in "Set Focus" shortcut in Shortcuts.app (present by default on macOS 12+); mode must exactly match a configured Focus profile (e.g. "Do Not Disturb", "Work"); enabled defaults to trueshortcuts_unavailable, focus_not_foundmacos_manage_finder toolfrontmost_path — POSIX path of the active Finder window, or null when none is openget_selection — POSIX paths of selected itemsreveal (open -R), open_with (open -a <App>, or the default app when app_name is omitted), trash (moves to Trash — recoverable, never a permanent delete)open_with and trash check that the path exists before calling open or Finderfrontmost_path, get_selection, and trash script Finder and require Automation > Finder permission; reveal and open_with need nonefinder_not_open, path_not_found, app_not_found (unknown open_with app), trash_refused (Finder declined an existing item — locked, in use, or no Trash on the volume), accessibility_requiredmacos://system/info resourceapplication/json — battery, power source, Wi-Fi SSID, hostname, version, uptime, display countmacos_get_infomacos://audio/devices resourceapplication/jsonmacos_control_audio (action=list)macos://displays resourcecurrent_config — as application/jsonmacos_manage_displays (action=list)Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
macOS-specific:
runJxa) and AppleScript (runAppleScript) with a configurable timeoutServiceUnavailable and an install instruction when the CLI is absentmacos_check_permissions reports exactly which process needs which grant before a tool hits ForbiddenAgent-friendly output:
accessibility_required reason and grant instructions for the permission actually denied — Privacy & Security > Accessibility, or Privacy & Security > Automation for the named appset without mode, quit without app_name) is rejected before anything runs, as -32602 with reason invalid_arguments and a hint naming what to send; each multi-action tool advertises those requirements in its inputSchemaServiceUnavailable with the exact brew install command neededmacos_manage_windows action=list reports display_index on every window so agents can reason about multi-monitor layoutsmacos_take_screenshot separates the full-resolution disk write from an optional base64 preview, keeping response size manageableThis server is local-only — it controls the macOS system it runs on. Use STDIO transport with your MCP client.
Add the following to your MCP client configuration file:
{
"mcpServers": {
"macos-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/macos-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"macos-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/macos-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
brew install switchaudio-osx).brew install jakehilborn/jakehilborn/displayplacer).Some tools require macOS permissions granted to the terminal or MCP host app:
| Permission | Required by |
|---|---|
| Accessibility | macos_manage_windows (mutating actions), macos_manage_apps (hide/show), macos_control_system (lock) |
| Screen Recording | macos_take_screenshot with target=window |
| Automation > Finder | macos_manage_finder with action=get_selection |
Use macos_check_permissions to check current status before running permission-gated operations.
git clone https://github.com/cyanheads/macos-mcp-server.git
cd macos-mcp-server
bun install
cp .env.example .env
# edit .env if you want to set MACOS_SCREENSHOT_DIR or MACOS_DISPLAY_LAYOUTS
| Variable | Description | Default |
|---|---|---|
MACOS_SCREENSHOT_DIR | Default directory for screenshot files. | ~/Desktop |
MACOS_DISPLAY_LAYOUTS | JSON object mapping layout names to displayplacer argument strings. Used by macos_manage_displays action=apply_layout. | {} |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level. | info |
MCP_SESSION_MODE | Session storage: auto, stateful, or stateless (HTTP only). This server holds no per-session state, so .env.example sets stateless explicitly. | auto (.env.example sets stateless) |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
Display layout example:
# Get the current displayplacer command for your setup:
displayplacer list
# Then configure named layouts in your env:
MACOS_DISPLAY_LAYOUTS='{"office":"id:1234 res:2560x1440 hz:60 color_depth:8 scaling:on origin:(0,0) degree:0 id:5678 res:1920x1080 hz:60 color_depth:8 scaling:on origin:(2560,0) degree:0"}'
# One-time build
bun run rebuild
# Run the built server
bun run start:stdio
# Run checks
bun run devcheck # Lint, format, typecheck, security, changelog sync
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against spec
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry — registers tools/resources and inits services |
src/config/server-config.ts | MACOS_SCREENSHOT_DIR and MACOS_DISPLAY_LAYOUTS env parsing |
src/mcp-server/tools/definitions/ | 13 tool definitions (macos-*.tool.ts) |
src/mcp-server/tools/action-requirements.ts | Per-action argument requirements — enforced by the input schema and advertised in inputSchema |
src/mcp-server/resources/definitions/ | 3 resource definitions (macos-*.resource.ts) |
src/services/osascript/ | osascript JXA + AppleScript runner with configurable timeout and permission-denial classification |
src/utils/exec-failure.ts | Caller-safe error text for a failed CLI call (never the command line) |
src/services/audio/ | SwitchAudioSource device listing and switching |
src/services/display/ | displayplacer list and apply-layout |
src/services/screencapture/ | screencapture + sips PNG capture and JPEG preview |
src/services/system-info/ | Battery, Wi-Fi, hostname, uptime via system_profiler/pmset |
tests/tools/ | Tool tests mirroring definitions |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging; no console callsnull or unknown when the OS can't answer (e.g. battery on desktops, Focus status under SIP protection) rather than guessingcreateApp() and accessed via get*Service() accessorsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
MACOS_SCREENSHOT_DIRDefault directory for screenshot files. Defaults to ~/Desktop when empty.
MACOS_DISPLAY_LAYOUTSdefault: {}JSON object mapping layout names to displayplacer argument strings for apply_layout.
MCP_LOG_LEVELdefault: infoMinimum log level for output: debug, info, notice, warning, error.
MCP_HTTP_HOSTdefault: 127.0.0.1The hostname for the HTTP server.
MCP_HTTP_PORTdefault: 3010The port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHdefault: /mcpThe endpoint path for the MCP server.
MCP_AUTH_MODEdefault: noneAuthentication mode: none, jwt, or oauth.