Control macOS system settings, apps, windows, audio, displays, screenshots, and Focus mode via MCP.
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 Accessibilityapp_not_found, 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)list 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 — accepts level (0–100), muted, or both; level=0 does not mutesetmacos_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/falseset with mode: "dark" | "light" | "toggle" — dark/light are idempotent, toggle flips on each callmacos_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 display_index), region (pixel rect) — 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 open; no permissions requiredget_selection — POSIX paths of selected items; requires Automation > Finder permissionreveal (open -R), open_with (open -a <App>), trash (moves to Trash — recoverable, not permanent delete)finder_not_open, path_not_found, 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:
System Settings > Privacy & Security > [permission type])ServiceUnavailable 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/resources/definitions/ | 3 resource definitions (macos-*.resource.ts) |
src/services/osascript/ | osascript JXA + AppleScript runner with configurable timeout |
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.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/macos-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-macos-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/macos-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/macos-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.