macos-mcp-server
Integrates with macOS to manage system settings, apps, windows, audio, displays, screenshots, and Focus mode.
Control macOS system settings, apps, windows, audio, displays, screenshots, and Focus mode.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
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.
Overview
Control your Mac from an agent: launch, quit, and arrange apps and windows, switch audio and display devices, control Music playback, capture screenshots, work with Finder, send notifications, and toggle Focus mode.
Tools
Tool | Description |
| System snapshot: battery level and charging status, power source, Wi-Fi association and network name, hostname, macOS version, uptime, and display count; a failed probe leaves its field null and named in |
| Reports Accessibility, Screen Recording, Automation > Finder, and Automation > Music status for the calling process from Apple's prompt-free checks — null with a reason when a check can't tell |
| List running apps, get the frontmost app, launch, quit, force-quit, hide, or show applications |
| List, focus, move, resize, move_resize, minimize, fullscreen, or close windows across all visible apps |
| Get or set system output volume (0–100) and mute state |
| List audio devices, get current defaults, or switch the default input/output device |
| Play, pause, stop, skip, play a playlist by name, set shuffle or repeat, or read what the Music app is playing |
| Get or set dark/light mode |
| Lock the screen or put the display to sleep |
| Capture the main display, one display by |
| List connected displays (with each one's |
| Post a notification to macOS Notification Center |
| Get or set Do Not Disturb / Focus mode |
| Frontmost path, current selection, reveal, open with app, or move to Trash |
Resources
Resource | Description |
| Current macOS system snapshot: battery, power source, Wi-Fi association and network name, hostname, version, uptime, display count; a failed probe leaves its field null and named in |
| All audio input and output devices, including which is the current default. Requires SwitchAudioSource CLI. |
| Connected display inventory including persistent IDs, index (the |
Resource data is also accessible via macos_get_info, macos_control_audio (action=list), and macos_manage_displays (action=list).
Related MCP server: mcp-server-macos-use
Capability reference
macos_get_info tool
Battery level (0–100), charging state, and power source (
AC,Battery,UPS);nullon desktops with no battery.chargingis false when the battery is held at a charge limit (AC attached; not charging) and true whilefinishing chargeWi-Fi association from the Wi-Fi device's link state; the network name is
nullwhile connected when macOS withholds it (reading it requires Location Services access for the host app). The name renders incontent[]as a code span, so markdown characters in it read back as the nameHostname and uptime (from Node's
osmodule), macOS version string (e.g."15.1")Connected display count (
0on a headless Mac)A probe that fails leaves its field
nulland names it inunavailable; nothing is filled in.battery: nullwithbatteryabsent fromunavailablemeans the Mac has no batteryNo permissions required
macos_check_permissions tool
Reports Accessibility (every
macos_manage_windowsaction,listincluded;macos_manage_appshideandshow, and thewindow_titlefrontmostreads; the ⌃⌘Q keystrokemacos_control_systemlocksends before it falls back to the screen saver), Screen Recording (window screenshots), Automation > Finder (macos_manage_finderfrontmost_path,get_selection, andtrash), and Automation > Music (macos_control_music) from Apple's prompt-free checks:AXIsProcessTrusted,CGPreflightScreenCaptureAccess, andAEDeterminePermissionToAutomateTargetwith prompting offEach field is
true,false, ornullwhen the check can't tell;unknown_reasonsnames why eachnullis unknown. Automation > Finder and Automation > Music readnulluntil macOS has asked for consent and while that app isn't running. Notifications is alwaysnull: macOS has no prompt-free check for itReturns the calling process name (e.g.
"ghostty","node") so you know which process to grant permissions forRead-only — no check sends an Apple event to another app, captures the screen, or can show a permission prompt
Run this first when debugging why another tool is failing
macos_manage_apps tool
list— 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 byapp_nameorbundle_id;hidden=truestarts in the backgroundquit(graceful AppleScript quit) vs.force_quit(SIGKILL, no save prompt)hide/show— toggle visibility; requires Accessibilityquit,force_quit,hide, andshowfirst check that the app is running — resolved by application name the waytell applicationdoes ("Visual Studio Code"finds theCodeprocess), without launching it — and returnnot_runningwhen it isn'tTyped errors:
app_not_found(launchof an app that isn't installed),no_frontmost_app(System Events reports no frontmost process),not_running,accessibility_required
macos_manage_windows tool
list— all visible windows across apps, with position, size,minimizedandfullscreenstate (each read from the window'sAXMinimized/AXFullScreenattribute,nullwhen it can't be read), anddisplay_index: the display the window overlaps most (the indexmacos_manage_displays action=listreports), ornullwhen the display frames can't be read or the window overlaps none. Two windows that share a title, and the windows of two processes that share a name, are each listedfocus,move,resize,move_resize,minimize,fullscreen,close— target bywindow_title(exact, ignoring case, in any app; when two windows of one app share the title, the frontmost of them) orapp_name(that app's frontmost window);window_titletakes precedence andapp_nameis then ignoredapp_namematches a running app by its application name ("Visual Studio Code"), localized name ("Code"), or process name ("firefox"), ignoring case; it is resolved from the running-application list, so no app is launched or sent an Apple event to find itmoveneedsx/y,resizeneedswidth/height(both greater than 0),move_resizeneeds all fourminimize—minimized: true(the default) minimizes,falserestores; it writes the window'sAXMinimizedattribute only when the window is not already in that state, and succeeds only when the window reads back in the requested statefullscreen—fullscreen: trueenters,falseexits, omitted toggles the window's current state. When the state differs it raises the target window, asfocusdoes, and sets itsAXFullScreenattribute (no keystroke, so the window that had keyboard focus is never toggled); it waits up to 4 s for the window to read back in the requested stateEvery action,
listincluded, scripts System Events and requires Accessibility; a denial returnsaccessibility_requiredand never reads as "no windows".focusraises the window and brings its app forward; no action launches an appEach write finds, changes, and reads back the same window in one script;
window.appis the process the window was found under;closereturns no windowTyped errors:
not_running(app_namenames no running app),window_not_found,accessibility_required,minimize_unsupportedandfullscreen_unsupported(the window'sAXMinimized/AXFullScreenattribute is missing or not settable, or the window did not reach the requested state within 4 s); an osascript timeout or failure keeps its own error instead of reading aswindow_not_found
macos_control_volume tool
get— current output volume (0–100) and mute state of the default output. On an output with no software volume control (many HDMI and DisplayPort displays),levelandmutedarenullandoutput_devicenames the device (nullwhen it cannot be identified)set— requireslevel(0–100),muted, or both;level=0does not mute. On an output with no software control for the requested value it fails withvolume_unsupportednaming the device and changes nothingAlways returns current state, with the
actionechoedTyped errors:
volume_unsupported; a cancelled call stops the device lookup and settles as cancelled, never asoutput_device: null
macos_control_audio tool
list— all input/output devices with anis_defaultflag; filter withtypecurrent— default input and output device namesswitch_output/switch_input— case-insensitive partial name match ("MacBook"matches"MacBook Pro Microphone")Volume level is separate (
macos_control_volume)Requires SwitchAudioSource CLI (
brew install switchaudio-osx); typed errorsdevice_not_found,switchaudio_unavailable
macos_control_music tool
playresumes or starts playback;play_playlistplays the playlist named byplaylist(exact name as the Music sidebar shows it; the first match plays). Both launch Music when it is closedpauseandstophalt playback;nextandpreviousskip tracksset_shufflerequiresshuffle(boolean);set_repeatrequiresrepeat(off,one, orall)get_currentonly readsEvery action returns the state Music reports afterward:
player_state(playing,paused,stopped,fast_forwarding,rewinding),track(name,artist,album),shuffle, andrepeat;play_playlistalso returnsplaylist, the name as Music reports itget_current,pause, andstopleave a closed Music closed and returnrunning: falsewithplayer_state,shuffle, andrepeatnull;next,previous,set_shuffle, andset_repeatfail withnot_runningtrackisnullwhen Music has nothing loaded. It is alsonull, withtrack_unavailablesaying why, when the player is not stopped but Music refuses to return a track outside the library (Apple Music radio or catalog playback on recent macOS); transport actions still workScripts Music and requires Automation > Music; the first call shows the macOS consent dialog, and a call that times out while it is open succeeds when retried after answering it
Typed errors:
not_running,playlist_not_found(nothing was played),accessibility_required(Automation > Music denied)System output level belongs to
macos_control_volume
macos_control_appearance tool
get— returnsdark_mode: true/false, with theactionechoedsetrequiresmode: "dark" | "light" | "toggle"—dark/lightare idempotent,toggleflips on each callScripts System Events; typed error
accessibility_requiredwhen Automation > System Events is denied
macos_control_system tool
lock— sends ⌃⌘Q through System Events (needs Accessibility and Automation > System Events). When either is denied, it starts the screen saver instead, which locks the session only when "Require password after screen saver begins or display is turned off" is set to Immediately in System Settings > Lock Screen;methodreportskeystrokeorscreen_saver. A timed-out or otherwise failed keystroke is reported as-is, and a cancelled call never falls backTyped error
accessibility_requiredwhen the keystroke is denied and the screen saver can't start, with the hint for the pane that was deniedsleep_display—pmset displaysleepnow; no permissions requiredBoth operations are immediate and reversible with any input (wake/unlock)
macos_take_screenshot tool
target:screen(the main display only, the one with the menu bar),display(0-based integerdisplay_index, default 0: theindexmacos_manage_displays action=listreports),region(pixel rect,width/heightgreater than 0) — no Screen Recording required;window(byapp_name, matched likemacos_manage_windows: application name, localized name, or process name, ignoring case; captures that app's frontmost on-screen window) requires Screen Recording. When the Screen Recording check can't tell, the window capture still runs andnoticesays whyAlways saves a full-resolution PNG. Omitting
path(or passing"") writes a newscreenshot-<timestamp>.pnginMACOS_SCREENSHOT_DIR(~/Desktopwhen unset), adding-2,-3, … rather than replacing a file. A custompathmust end in.png(any case);~/expands to the home directory; its directory must exist and, with symlinks resolved, lie insideMACOS_SCREENSHOT_DIR,/tmp, or the home directory. The returnedpathis the resolved one (a/tmppath reports under/private/tmp)Never replaces an existing entry: a path that already exists fails with
path_existsunlessoverwrite: true, which replaces only the regular.pngfile found atpathwhen the call checks it (never a symbolic link or directory). An entry created atpathafter that check, or put in that file's place, is never replaced and fails withpath_existsEvery call settles within 55 s of its start, the
app_namelookup included: past that it fails as a Timeout (-32004) and writes nothing. A cancelled request writes nothing eitherinclude_data=truereturns a JPEG preview, longer side at most 1024 px (a smaller capture keeps its own size), quality 70: as the firstcontent[]block (image/jpeg) and as base64previewpluspreview_width/preview_heightinstructuredContentwidth/height(and the preview's) are read from the image file headersTyped errors:
screen_recording_required,window_not_found(an app that is not running, or one with no on-screen window; an osascript timeout or failure during the window lookup keeps its own error),display_not_found,path_not_writable(also a directory that cannot be resolved or searched, an over-long one included),path_exists; apathnot ending in.png, holding U+0000, or whose file name or full resolved path is longer than the file system allows (255 bytes per name, 1024 for the whole path) is rejected as-32602with reasoninvalid_arguments
macos_manage_displays tool
list— per display:index(0-based position inNSScreen.screens; thedisplay_indexmacos_take_screenshotandmacos_manage_windowsuse;nullfor a display with no NSScreen entry, such as a disabled one), persistent ID, type ("MacBook built in screen"or"<N> inch external screen"), resolution, refresh rate ("N/A"when macOS reports none), origin, rotation, scaling, and enabled state (alsotruefor a display mirroring another); pluscurrent_config(a displayplacer command that reproduces the active arrangement) andlayouts(the names configured inMACOS_DISPLAY_LAYOUTS,[]when none). WhenMACOS_DISPLAY_LAYOUTSis invalid,liststill succeeds, omitslayouts, and itsnoticenames the variable. When the NSScreen read fails,indexis omitted andnoticesays why; that includes a read that waited 15 s for concurrent System Events enumerations to finishapply_layout— activates a named preset fromMACOS_DISPLAY_LAYOUTS(one of the nameslistreturns aslayouts); raw displayplacer args are never accepted from the caller. An invalidMACOS_DISPLAY_LAYOUTSfailsapply_layoutaslayouts_invalid(ConfigurationError), neverlayout_not_foundRequires displayplacer CLI (
brew install displayplacer); typed errorsdisplayplacer_not_found,displayplacer_output_unrecognized,layout_not_found,layouts_invalidA failed displayplacer run (non-zero exit, signal, timeout) fails the call instead of returning what it printed; a display block missing a line whose value the listing returns, or a listing missing the arrangement command, fails as
displayplacer_output_unrecognized. displayplacer 1.3.0 and 1.4.0 are both read
macos_send_notification tool
titlerequired;body,subtitle, andsound=true(default notification sound) are optionalEach call creates a new notification — not idempotent
No permission required to post; an active Focus such as Do Not Disturb silences it unless that Focus allows the posting app
macos_manage_focus tool
get— best-effort; reads~/Library/DoNotDisturb/DB/Assertions.jsonand returnsstatus: "active" | "inactive" | "unknown";unknown(with areason) when that file cannot be read or parsed, which can happen when the server process is not allowed to read it; a cancelled call settles as cancelled, never asunknownset— runs a shortcut named"Set Focus"from your Shortcuts library and passes it{"mode": …, "enabled": …}as JSON input. The shortcut is not built in: create it with Get Dictionary from Input feeding the Set Focus action.modemust exactly match a configured Focus name, including case (e.g."Do Not Disturb","Work");enableddefaults totrue. The result echoesmodeandenabled;success: truemeans the shortcut finished without error (the Focus state is not read back)Typed errors:
shortcuts_unavailable(shortcuts CLI missing, or no shortcut named "Set Focus"),shortcut_failed(the shortcut exited with an error; carries its error text). A shortcut still running after 15 s is stopped and reported as aTimeouterror; a cancelled call stops the run and settles as cancelled
macos_manage_finder tool
frontmost_path— POSIX path of the active Finder window, ornullwhen none is open; a cancelled call settles as cancelled, never asnullget_selection— POSIX paths of selected itemsreveal(open -R),open_with(open -a <App>, or the default app whenapp_nameis omitted),trash(moves to Trash — recoverable, never a permanent delete)open_withandtrashcheck that the path exists before callingopenor Finderfrontmost_path,get_selection, andtrashscript Finder and require Automation > Finder permission;revealandopen_withneed noneTyped errors:
finder_not_open,path_not_found,app_not_found(unknownopen_withapp),trash_refused(Finder declined an existing item — locked, in use, or no Trash on the volume),accessibility_required
macos://system/info resource
Current macOS system snapshot as
application/json— battery, power source, Wi-Fi association and network name, hostname, version, uptime, display count, andunavailableSame data is also reachable via
macos_get_info
macos://audio/devices resource
All audio input and output devices, including which is the current default, as
application/jsonRequires SwitchAudioSource CLI
Same data is also reachable via
macos_control_audio(action=list)
macos://displays resource
Connected display inventory —
index, persistent IDs, type, resolution, origin, rotation, scaling, enabled state, pluscurrent_config— asapplication/json; carriesnotice(and noindex) when the NSScreen read failsRequires displayplacer CLI
Same data is also reachable via
macos_manage_displays(action=list)
Features
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:
osascript service wraps both JXA (
runJxa) and AppleScript (runAppleScript) with a configurable timeoutSwitchAudioSource and displayplacer integrations are optional dependencies, found on PATH, in
/opt/homebrew/bin, or in/usr/local/binon every call — the affected tools fail withServiceUnavailableand an install instruction when no location has the CLImacOS system binaries (osascript, screencapture, sips, pmset, networksetup, system_profiler, …) run by absolute path, so the host PATH never decides which binary runs
screencapture writes into a private directory beside the destination and the PNG is placed without replacing anything the call did not check; sips makes the JPEG preview; image sizes come from the PNG/JPEG headers
pmset for battery, networksetup + ifconfig for Wi-Fi, sw_vers for version,
system_profiler -json SPDisplaysDataTypefor displays, Node'sosfor hostname and uptimePermission-first design —
macos_check_permissionsreports exactly which process needs which grant before a tool hitsForbidden
Agent-friendly output:
Permission errors carry the
accessibility_requiredreason and grant instructions for the permission actually denied —Privacy & Security > Accessibility, orPrivacy & Security > Automationfor the named appA missing per-action argument (
setwithoutmode,quitwithoutapp_name), or text no AppleScript literal can carry (U+0000, an unpaired surrogate), is rejected before anything runs, as-32602with reasoninvalid_argumentsand a hint naming what to send; each multi-action tool advertises the per-action requirements in itsinputSchemaErrors carry the failing program's own error text, never its command line or script source
Optional CLI dependencies surface
ServiceUnavailablewith the exactbrew installcommand neededmacos_manage_windowsreportsdisplay_index(the display a window overlaps most, ornullwhen unknown) on every window so agents can reason about multi-monitor layoutsmacos_take_screenshotseparates the full-resolution disk write from an optional preview, sent as an image content block and as base64 instructuredContentso both surfaces carry it
Getting started
This 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/mcpPrerequisites
macOS 13 (Ventura) or higher.
Bun v1.4.0 or higher (or Node.js v24+).
Optional: SwitchAudioSource for audio routing (
brew install switchaudio-osx).Optional: displayplacer 1.3.0 or 1.4.0 for display management (
brew install displayplacer).
Either optional CLI works from Homebrew on Apple Silicon (/opt/homebrew/bin) or Intel (/usr/local/bin), or from anywhere on PATH.
Some tools require macOS permissions granted to the terminal or MCP host app:
Permission | Required by |
Accessibility |
|
Automation > System Events |
|
Screen Recording |
|
Automation > Finder |
|
Automation > Music |
|
Use macos_check_permissions to check current status before running permission-gated operations.
Installation
Clone the repository:
git clone https://github.com/cyanheads/macos-mcp-server.gitNavigate into the directory:
cd macos-mcp-serverInstall dependencies:
bun installConfigure environment:
cp .env.example .env
# edit .env if you want to set MACOS_SCREENSHOT_DIR or MACOS_DISPLAY_LAYOUTSConfiguration
Variable | Description | Default |
| Default directory for screenshot files, and one of the directories a custom |
|
| JSON object mapping layout names to non-empty displayplacer argument strings. Used by |
|
| Transport: |
|
| Port for HTTP server. |
|
| Auth mode: |
|
| Log level. |
|
| Session storage: |
|
| Enable OpenTelemetry instrumentation. |
|
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\""}'Each screen gets its own quoted segment, the form displayplacer list prints its arrangement command in; a layout value without quoted segments reaches displayplacer as a single argument. macos_manage_displays action=list returns current_config with the same per-screen quoted segments, and returns the configured names as layouts.
Running the server
Local development
# 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 specProject structure
Directory | Purpose |
|
|
|
|
| 14 tool definitions ( |
| Per-action argument requirements — enforced by the input schema and advertised in |
| 3 resource definitions ( |
| osascript JXA + AppleScript runner with configurable timeout and permission-denial classification, AppleScript string literals for caller text, and the running-app name resolver |
| Caller-safe error text for a failed CLI call (never the command line) |
| Absolute paths of the macOS system binaries, and the per-call PATH + Homebrew lookup for displayplacer and SwitchAudioSource |
| Prompt-free Accessibility, Screen Recording, Automation > Finder, and Automation > Music checks |
| SwitchAudioSource device listing and switching |
| displayplacer list and apply-layout, plus the JXA NSScreen read that gives each display its index |
| screencapture PNG capture, path checks and no-replace placement, sips JPEG preview |
| Battery, Wi-Fi, version, and display count via pmset/networksetup/ifconfig/sw_vers/system_profiler; hostname and uptime via |
| Tool tests mirroring definitions |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor request-scoped logging; noconsolecallsRead actual system/CLI state and never fabricate it — return
nullorunknownwhen the OS can't answer (e.g. battery on desktops, Focus status when the Focus state file cannot be read) rather than guessingServices are singletons initialized in
createApp()and accessed viaget*Service()accessors
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run testLicense
Apache-2.0 — see LICENSE for details.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP connector that lets ChatGPT list, search, and run your Apple Shortcuts via a local Mac agent
Connect ChatGPT and Claude to your Mac, Windows or Linux with secure MCP remote access.
MCP connector for iMessage & Contacts via a local Mac agent + Vercel relay
Read and write Mission Control state via MCP — projects, tasks, subtasks, templates, status updates.
Related MCP Servers
- AlicenseAqualityDmaintenanceProvides native macOS computer control tools including mouse and keyboard simulation, screenshot capture, and application management for MCP-compatible agents. It enables AI assistants to directly interact with the macOS operating system and installed apps through standard tool calls.2421 npm8MIT
- AlicenseNot gradedqualityDmaintenanceEnables controlling macOS applications via accessibility APIs, supporting actions like clicking, typing, and keyboard input through MCP commands.58 npm357MIT
- AlicenseAqualityDmaintenanceA local MCP server that exposes macOS automation actions (AppleScript + CLIs) as tools, enabling MCP clients on your Mac to control apps, system settings, and more.39MIT
- AlicenseNot gradedqualityDmaintenanceEnables MCP clients to control macOS via accessibility and screen recording, providing tools to list apps, observe UI, click, type, press keys, and scroll.MIT