About hammerspoon
hammerspoon-mcp is an MCP server for AI agents that need programmatic control of macOS through Hammerspoon. It combines OS automation tools with event watchers and subscribable resources.
Key features
- 01Manage windows and virtual desktops
- 02Control apps, input, and screens
- 03Switch audio and Bluetooth devices
- 04Change Focus and system settings
- 05Watch OS events and subscribe to resources
- 06Run Lua in Hammerspoon
Use cases
- 01Arrange windows and move apps between Spaces
- 02Switch audio output or connect Bluetooth devices
- 03Automate input and adjust system settings
- 04React to app, network, battery, or clipboard events
MCP support
Exposes macOS automation tools and subscribable resources to MCP clients. The server communicates with Hammerspoon and streams watched OS events.
Configuration
Local server config is the same for every client. Copy it into your client’s MCP config file.
Tools
Local servers expose tools only once running on your machine, and the maintainer has not provided a tool list.
Details
From the project READMEhammerspoon-mcp
Give AI agents deep control over macOS — windows, audio, Bluetooth, Spaces, Focus mode, and 200+ OS APIs — through one MCP server.
What is this?
An MCP server that bridges AI agents to Hammerspoon — the most powerful macOS automation tool available. Unlike screenshot-and-click tools, this gives agents programmatic OS-level access: manage windows by ID, switch audio devices by name, move apps between Spaces, toggle Focus mode, and subscribe to real-time OS events.
75 tools. 12 subscribable resources. Zero native dependencies.
Quick Start
# 1. Install Hammerspoon (if you haven't)
brew install --cask hammerspoon
# 2. Enable the IPC module (one-time setup)
# Add this line to ~/.hammerspoon/init.lua:
# require("hs.ipc")
# Then reload your Hammerspoon config (Cmd+Alt+Ctrl+R or restart the app)
# 3. (Optional) Install the hs CLI symlink for convenience:
# Run in Hammerspoon console: hs.ipc.cliInstall()
# Or manually: ln -sf /Applications/Hammerspoon.app/Contents/Frameworks/hs/hs ~/.local/bin/hs
# 4. Run the MCP server
npx hammerspoon-mcp
Note: The server auto-discovers the
hsbinary — it checks~/.local/bin/hs,/usr/local/bin/hs,/opt/homebrew/bin/hs, and the app bundle path. You don't needhson your PATH if Hammerspoon is installed to/Applications.
Architecture
┌─────────────────────────────────────────────────────────┐
│ MCP Client (Claude, Cursor, MeshClaw, etc.) │
└──────────────────────────┬──────────────────────────────┘
│ stdio (JSON-RPC)
┌──────────────────────────▼──────────────────────────────┐
│ hammerspoon-mcp (Node.js) │
│ ├─ tools → execSync("hs -c '...'") (request/response)│
│ └─ watchers → named pipe reader (push events) │
└──────────────────────────┬──────────────────────────────┘
│ hs CLI / named pipe
┌──────────────────────────▼──────────────────────────────┐
│ Hammerspoon.app (Lua runtime + ObjC bridge) │
│ 200+ modules: window, screen, spaces, audio, wifi, │
│ bluetooth, battery, USB, filesystem, accessibility... │
└──────────────────────────┬──────────────────────────────┘
│
macOS APIs (CoreGraphics, IOKit, etc.)
MCP Client Configuration
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"hammerspoon": {
"command": "npx",
"args": ["hammerspoon-mcp"]
}
}
}
Claude Code
claude mcp add hammerspoon -- npx hammerspoon-mcp
Cursor / VS Code / Copilot
Add to .vscode/mcp.json:
{
"servers": {
"hammerspoon": {
"command": "npx",
"args": ["hammerspoon-mcp"]
}
}
}
Kiro
Add to ~/.aws/amazonq/mcp.json:
{
"mcpServers": {
"hammerspoon": {
"command": "npx",
"args": ["hammerspoon-mcp"]
}
}
}
Tools
🪟 Window Management (7)
| Tool | Description |
|---|---|
hs_list_windows | All visible windows with app, title, frame, id |
hs_focus_window | Bring window to front by id or title match |
hs_move_window | Move/resize window by id |
hs_window_layout | 16 presets (halves, thirds, quarters, grid, cascade, center…) + custom fractional layouts |
hs_save_layout | Snapshot all window positions as a named layout |
hs_restore_layout | Restore a saved layout (matches by app + title) |
hs_list_layouts | List saved layout names |
🖥️ Spaces / Virtual Desktops (9)
| Tool | Description |
|---|---|
hs_spaces_list | All spaces per screen with active markers |
hs_spaces_active | Currently focused space + screen |
hs_spaces_goto | Switch by ID or index |
hs_spaces_move_window | Move window to another space (optionally follow) |
hs_spaces_add | Create a new space |
hs_spaces_remove | Remove a space (safety checks) |
hs_spaces_windows | Windows on a specific space |
hs_spaces_window_spaces | Which space(s) a window is on |
hs_spaces_mission_control | Toggle Mission Control / Exposé / Launchpad / Show Desktop |
📱 Application Control (3)
| Tool | Description |
|---|---|
hs_list_apps | Running applications with bundle ID, PID, hidden state |
hs_launch_app | Launch or activate an app by name |
hs_kill_app | Quit an app |
⌨️ Input (3)
| Tool | Description |
|---|---|
hs_click | Click at coordinates with button + modifier options |
hs_type | Type text (Unicode, CJK, emoji) |
hs_hotkey | Press key combo (e.g. "cmd+shift+4") |
🖼️ Screen (3)
| Tool | Description |
|---|---|
hs_screenshot | Capture full screen or region (base64 PNG) |
hs_screens | Display info (resolution, name, rotation) |
hs_mouse_position | Current cursor coordinates |
🔊 Audio Devices (6)
| Tool | Description |
|---|---|
hs_audio_devices | List all input/output with volume, mute, transport type |
hs_audio_default | Get/set default devices (fuzzy name match) |
hs_audio_volume | Volume on any specific device (0–100) |
hs_audio_mute | Mute/unmute/toggle any device |
hs_audio_datasources | List/switch data sources (speakers vs headphones) |
hs_audio_watch | Watch for device changes |
🔵 Bluetooth (5)
| Tool | Description |
|---|---|
hs_bluetooth_status | Power state and discoverability |
hs_bluetooth_power | On / off / toggle |
hs_bluetooth_devices | List paired or connected devices |
hs_bluetooth_connect | Connect by name (fuzzy) or MAC address |
hs_bluetooth_disconnect | Disconnect a device |
Requires blueutil:
brew install blueutil
🌙 Focus / Do Not Disturb (6)
| Tool | Description |
|---|---|
hs_focus_status | Check if DND/Focus is active + profile name |
hs_focus_toggle | Toggle via Control Center (no setup needed) |
hs_focus_shortcut | Enable/disable via macOS Shortcuts (most reliable) |
hs_focus_install_shortcut | One-time setup guide |
hs_focus_schedule | DND for N minutes with auto-disable |
hs_focus_cancel_timer | Cancel scheduled disable |
🔋 System State (6)
| Tool | Description |
|---|---|
hs_battery | Charge %, power source, time remaining |
hs_wifi | Current SSID and interface details |
hs_volume | System volume get/set |
hs_dark_mode | Get/toggle dark mode |
hs_notify | Post a macOS notification |
hs_clipboard | Read/write system pasteboard |
🔆 Display (1)
| Tool | Description |
|---|---|
hs_brightness | Get/set screen brightness (0–100) |
⏱️ Timers (3)
| Tool | Description |
|---|---|
hs_timer_start | Named countdown with notification on fire |
hs_timer_cancel | Cancel by name |
hs_timer_list | List active timers |
📁 Finder / Files (4)
| Tool | Description |
|---|---|
hs_finder_selection | Get currently selected Finder items |
hs_finder_open | Open/reveal path in Finder |
hs_trash | Move to Trash (reversible) |
hs_quicklook | Quick Look preview |
🛠️ Utilities (5)
| Tool | Description |
|---|---|
hs_system_info | CPU, memory, thermal state, uptime |
hs_caffeinate | Prevent sleep for N minutes |
hs_execute | Run shell command (user environment) |
hs_open_url | Open URL in browser or specific app |
hs_alert | HUD-style on-screen overlay message |
👁️ Watchers — Live OS Event Streams (13)
| Tool | Events |
|---|---|
hs_watch_apps | App activated, launched, terminated, hidden |
hs_watch_wifi | Network changes |
hs_watch_usb | Device connect/disconnect |
hs_watch_battery | Charge level, power source |
hs_watch_screens | Display configuration |
hs_watch_paths | Filesystem changes |
hs_watch_sleep | Sleep/wake/lock/unlock |
hs_watch_clipboard | Every pasteboard change |
hs_watch_clipboard_history | Ring buffer with source app tracking |
hs_get_clipboard_history | Read the full buffer |
hs_watch_urls | Incoming hammerspoon://mcp/... URLs |
hs_unwatch | Stop a watcher |
hs_list_watchers | List active watchers |
🧪 Escape Hatch (1)
| Tool | Description |
|---|---|
hs_eval | Execute arbitrary Lua in the Hammerspoon runtime |
This gives access to all 200+ Hammerspoon modules not covered by dedicated tools — USB details, serial ports, network interfaces, Canvas drawing, menubar items, etc.
Resources (Subscriptions)
MCP clients can subscribe to resource URIs and receive push notifications when OS events fire:
| URI | Triggers |
|---|---|
watcher://apps | App lifecycle events |
watcher://wifi | Network changes |
watcher://usb | USB device events |
watcher://battery | Battery/power changes |
watcher://screens | Display reconfiguration |
watcher://paths | Filesystem changes |
watcher://sleep | Sleep/wake/lock events |
watcher://clipboard | Pasteboard changes |
watcher://clipboard_history | Clipboard ring buffer updates |
watcher://urls | Incoming URL events |
watcher://audio | Audio device changes |
hammerspoon://system | System info snapshot |
How event streaming works
Agent calls: hs_watch_wifi
→ Server injects Lua watcher into Hammerspoon runtime
→ macOS WiFi change fires
→ Watcher writes JSON to named pipe (/tmp/hs-mcp-events.pipe)
→ Node reader emits event
→ Server calls sendResourceUpdated("watcher://wifi")
→ Client reads resource → gets { ssid: "NewNetwork", ... }
Examples
"Tile my editor and terminal side by side"
hs_window_layout({ preset: "side-by-side" })
"Switch my audio to AirPods"
hs_audio_default({ output: "airpods" })
→ "set default output: AirPods Pro"
"Move Slack to Space 3"
hs_list_windows({ app: "Slack" }) → { id: 4523, ... }
hs_spaces_move_window({ windowId: 4523, spaceIndex: 3 })
"Start a 25-minute focus session"
hs_focus_schedule({ minutes: 25 })
hs_timer_start({ name: "pomodoro", minutes: 25, message: "Break time!" })
"Notify me when I switch away from my IDE"
hs_watch_apps()
→ Events stream to watcher://apps whenever focus changes
"Connect my headphones and set volume to 40%"
hs_bluetooth_connect({ device: "WH-1000XM5" })
hs_audio_volume({ device: "WH-1000XM5", volume: 40 })
Compared to Alternatives
| Capability | hammerspoon-mcp | mac-use-mcp | macos-automator-mcp |
|---|---|---|---|
| Window management by ID | ✅ | ❌ | ❌ |
| Virtual desktop (Spaces) control | ✅ | ❌ | ❌ |
| Audio device switching | ✅ | ❌ | ❌ |
| Bluetooth control | ✅ | ❌ | ❌ |
| Focus / DND mode | ✅ | ❌ | ❌ |
| Real-time OS event subscriptions | ✅ | ❌ | ❌ |
| Clipboard history | ✅ | ✅ | ❌ |
| Arbitrary OS scripting | ✅ (Lua) | ❌ | ✅ (AppleScript) |
| Screenshot | ✅ | ✅ | ❌ |
| Click / type / hotkeys | ✅ | ✅ | ❌ |
| Accessibility tree queries | via hs_eval | ✅ | ❌ |
| Zero-install (no prerequisites) | ❌ | ✅ | ✅ |
| Total tools | 75 | 18 | ~10 |
Prerequisites
| Requirement | Install |
|---|---|
| macOS 13+ | — |
| Node.js 22+ | brew install node |
| Hammerspoon | brew install --cask hammerspoon |
hs.ipc module | Add require("hs.ipc") to ~/.hammerspoon/init.lua |
| blueutil (optional) | brew install blueutil (for Bluetooth tools) |
The
hsCLI symlink (hs.ipc.cliInstall()) is optional — the server auto-discovers the binary inside the Hammerspoon app bundle.
Verify setup
# If hs is on your PATH:
hs -c 'return "ok"'
# Or directly via the app bundle:
/Applications/Hammerspoon.app/Contents/Frameworks/hs/hs -c 'return "ok"'
# Should print: ok
If this works, npx hammerspoon-mcp will connect successfully.
Permissions
| Permission | Required for | How to grant |
|---|---|---|
| Accessibility | Window, input, app control | System Settings → Privacy & Security → Accessibility → add your terminal |
| Screen Recording | hs_screenshot only | System Settings → Privacy & Security → Screen Recording |
Project Structure
hammerspoon-mcp/
├── src/
│ ├── index.ts MCP server entry point + resource registration
│ ├── bridge.ts hs CLI bridge (auto-discovers binary, eval, evalJson)
│ ├── tools.ts Core tools: window, app, input, screen, system, eval (23)
│ ├── watcher-tools.ts Watcher management tools (13)
│ ├── watchers.ts Named pipe event bridge
│ ├── spaces-tools.ts Spaces / virtual desktop tools (9)
│ ├── audio-tools.ts Audio device management (6)
│ ├── focus-tools.ts DND / Focus mode (6)
│ ├── bluetooth-tools.ts Bluetooth via blueutil (5)
│ └── extra-tools.ts Brightness, timers, system info, Finder, utils (13)
├── package.json
├── tsconfig.json
├── LICENSE MIT
└── README.md
Development
git clone <repo>
cd hammerspoon-mcp
npm install
npm run dev # Run with tsx (hot reload)
npm run build # Compile to dist/
npm start # Run compiled version
How It Works
- Startup — the bridge auto-discovers the
hsbinary (checks~/.local/bin,/usr/local/bin,/opt/homebrew/bin, app bundle) and validates connectivity - Tools execute synchronously via
hs -c '<lua>'— typically 30–80ms per call. Stdout noise (-- Loading extension: ...) is stripped automatically - Watchers inject Lua code that writes JSON lines to a FIFO pipe; a Node reader process emits them as MCP resource updates
- Resources buffer the last 50 events per watcher; clients read on-demand after receiving update notifications
- Graceful shutdown stops all watchers and cleans up the pipe on SIGINT/SIGTERM
Known Limitations
- Node.js 22+ is specified in
enginesbut the server builds and runs fine on Node 20 (tested) hs_spaces_goto,addSpaceToScreen,removeSpacebriefly flash Mission Control (macOS limitation — enable "Reduce motion" to minimize)- Bluetooth tools require blueutil (
brew install blueutil) - Focus/DND reading uses heuristics (no public Apple API exists)
hs_screenshotreturns base64 — large for full-screen captures- Saved layouts persist only in Hammerspoon's runtime (lost on HS reload)
- Named pipe path is
/tmp/hs-mcp-events.pipe— only one server instance at a time
License
MIT © 2026 Mohammed Babur
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.