hammerspoon-mcp

hammerspoon-mcp

MCP server that lets AI agents drive macOS through Hammerspoon via typed tools, with injection-safe Lua execution and tools for window management, app control, API search, and config reload.

Category
访问服务器

README

@vukvukovich/hammerspoon-mcp

License: MIT Node Status: pre-release

Let an AI agent drive your Mac through Hammerspoon, without ever splicing its input into Lua source.

What it is

Hammerspoon is a macOS automation app you script in Lua. It exposes a command line interface through its hs.ipc module, so hs -c "<lua>" runs Lua inside the running Hammerspoon process.

This project is an MCP (Model Context Protocol) server that sits in front of that CLI. It gives an agent a set of typed tools (list windows, move a window, focus an app, search the Hammerspoon API docs, tail the console, reload your config) and translates each tool call into a Lua program that Hammerspoon runs.

It speaks MCP over stdio, so any MCP client can use it: Claude Code, Claude Desktop, or your own.

Project status

Pre-release, v0.x. The tool surface below is the planned v0.1 set. Names and argument shapes can still change between minor versions. Nothing here is API-stable yet.

Why this one is different

1. Injection-safe by construction

Most of the risk in a "run Lua for me" bridge is the moment you build the Lua. The usual approach is to interpolate the arguments into a source string and escape the dangerous characters. Escaping is a discipline, and disciplines slip.

This server never interpolates. Every tool's Lua body is a static constant in the TypeScript source. Arguments travel separately:

  1. The validated argument object is JSON-encoded.
  2. That JSON is base64-encoded.
  3. The base64 text is spliced once, into one fixed prelude line:
local ARGS = hs.json.decode(hs.base64.decode("<base64>"))

The tool body then reads ARGS.title, ARGS.windowId, and so on.

The base64 alphabet is A-Z, a-z, 0-9, +, /, and =. It contains no quote, no backslash, no newline, no square bracket, and no hyphen. So the payload cannot close the Lua string, cannot start an escape sequence, cannot open a long bracket, and cannot open a comment. Injection is impossible because of the alphabet, not because someone remembered to escape correctly.

There is no shell layer either. The server calls execFile(hsPath, ["-c", lua]) with an argv array, so no sh ever parses the command.

2. Tiered tools, safe by default

The default tier is safe: read, inspect, and arrange operations. Arbitrary Lua evaluation exists as hs_eval, but it is off unless you set HS_MCP_TOOLS=all.

See Security for the reasoning. Short version: the threat is prompt injection, not you.

3. Built for the config-development loop

Most of the value of Hammerspoon is your own init.lua. So the server helps you write it, not just drive it:

  • hs_api_search searches Hammerspoon's bundled API documentation, so the agent can look up the real signature of hs.window.moveToUnit instead of guessing.
  • hs_console_tail reads back the Hammerspoon console, so the agent can see its own errors.
  • hs_reload_config reloads init.lua after an edit.

Edit, reload, read the console, fix. The agent can run that loop itself.

Quick start

Prerequisites

  • macOS.

  • Node.js 24 or newer.

  • Hammerspoon, installed and running:

    brew install --cask hammerspoon
    
  • The hs.ipc module loaded in your Hammerspoon config. Add this line to ~/.hammerspoon/init.lua:

    require("hs.ipc")
    

    Then reload your config from the Hammerspoon menu bar icon. This is what installs and enables the hs command line tool. Without it, hs -c has nothing to talk to.

Verify the bridge by hand before wiring up any client:

hs -c "return 1 + 1"

If that prints 2, you are ready.

Add it to your MCP client

Claude Code:

claude mcp add hammerspoon -- npx -y @vukvukovich/hammerspoon-mcp

Any client that takes an mcpServers JSON block:

{
  "mcpServers": {
    "hammerspoon": {
      "command": "npx",
      "args": ["-y", "@vukvukovich/hammerspoon-mcp"]
    }
  }
}

To opt into the unsafe tier, add the environment variable. Claude Code:

claude mcp add hammerspoon -e HS_MCP_TOOLS=all -- npx -y @vukvukovich/hammerspoon-mcp

JSON:

{
  "mcpServers": {
    "hammerspoon": {
      "command": "npx",
      "args": ["-y", "@vukvukovich/hammerspoon-mcp"],
      "env": {
        "HS_MCP_TOOLS": "all"
      }
    }
  }
}

Ask the agent to call hs_health first. It reports whether the hs binary was found, whether Hammerspoon is running, and whether hs.ipc answered.

Tool reference

Fourteen tools: thirteen in the safe tier, one gated.

Tool Tier What it does
hs_health safe Report bridge status: resolved hs path, whether Hammerspoon answers, its version.
hs_api_search safe Search Hammerspoon's bundled API reference and return exact signatures.
hs_console_tail safe Return the last N lines of the Hammerspoon console.
hs_reload_config safe Reload ~/.hammerspoon/init.lua.
hs_notify safe Show a transient on-screen alert, without stealing focus.
hs_list_windows safe List windows, with id, title, owning app, screen, and frame.
hs_focus_window safe Focus a window by id, or by a substring of its title.
hs_move_window safe Move or resize a window by id, in absolute screen pixels.
hs_window_layout safe Snap a window to a named preset such as left-half or quarter-top-left.
hs_list_apps safe List running applications, with bundle id, PID, and window count.
hs_launch_app safe Launch an application by name, or focus it if it is already running.
hs_focus_app safe Bring an already-running application to the front.
hs_screens safe List screens, with id, name, frame, and which one is primary.
hs_eval unsafe Evaluate arbitrary Lua. Requires HS_MCP_TOOLS=all.

hs_window_layout presets: left-half, right-half, top-half, bottom-half, maximize, center, thirds-left, thirds-center, thirds-right, two-thirds-left, two-thirds-right, and the four quarter-* corners. Positions are computed from the screen's usable frame, so they respect the menu bar and the Dock, and they work on a second monitor whose origin is negative.

Tools in the unsafe tier are not registered at all unless you opt in. A client connected with default settings will not see hs_eval in its tool list.

Configuration

All configuration is environment variables, read once at startup.

Variable Values Default Meaning
HS_MCP_TOOLS safe | all safe Which tiers to register. all adds the unsafe tier, which today means hs_eval.
HS_MCP_HS_PATH absolute path auto-detected Path to the hs binary. Set this if your install is somewhere unusual.
HS_MCP_DOCS_PATH absolute path from the app bundle Path to Hammerspoon's bundled API documentation JSON, used by hs_api_search.
HS_MCP_LOG_LEVEL debug | info | warn | error info Verbosity of the stderr log. Logs never touch stdout, which carries the protocol.

An unrecognised value for HS_MCP_TOOLS is a startup error, not a silent fallback. Failing loudly is better than quietly running in a tier you did not expect. For the discovery order behind the hs path default, see docs/ARCHITECTURE.md.

Security

Read this section before you set HS_MCP_TOOLS=all.

Where the server runs

The server is a local process. Your MCP client spawns it, talks to it over stdio, and it runs as your user account. There is no network listener and no remote surface.

That also means it inherits your permissions. Hammerspoon holds macOS TCC (Transparency, Consent, and Control) grants such as Accessibility, and possibly Screen Recording and Automation. Anything running inside Hammerspoon acts with those grants. This server does not add permissions and it cannot take any away.

The actual threat model

The risk is not that you are untrustworthy. The risk is prompt injection.

An agent reads untrusted text constantly: web pages, README files, issue bodies, log lines, the output of other tools. Any of that text can contain instructions. Sometimes the agent follows them. This is not hypothetical and it is not solved.

So the question for every tool is: if the agent is talked into calling this, how bad is it?

  • A curated verb has a small blast radius. Worst case with hs_move_window, a window ends up in the wrong place. Annoying, reversible, visible.
  • Arbitrary Lua has no blast radius limit. Hammerspoon's Lua can run shell commands, read the clipboard, capture the screen, watch keystrokes, and make network requests. One successful injection is full control of the machine, quietly.

That gap is the whole reason for tiers.

What HS_MCP_TOOLS=all means

It registers hs_eval. From that point the agent can execute any Lua it can write, inside a process that holds your Accessibility grants. Treat it as handing over a shell that also has the screen and the keyboard.

It is a genuinely useful mode. Writing and debugging Hammerspoon config is much faster when the agent can try a snippet directly. Use it in a session you are watching, for work you asked for, and turn it back off. Do not leave it on in a long-running or unattended agent that browses the web.

Safe by default is not a claim that you cannot be trusted with the dangerous tool. It is a claim that turning it on should be a decision you made on purpose, on a specific day, for a specific reason.

What is deliberately not here

These are not oversights. They are refusals, with reasons.

  • Raw shell execution. Agents already have shell tools, sandboxed and audited by their own host. A Mac-control server does not need to be a second, worse shell.
  • Keystroke and click synthesis. Synthetic typing into whatever window happens to be focused is arbitrary code execution with extra steps. If that window is a terminal, "type this text" and "run this command" are the same operation.
  • Clipboard reads. Your clipboard holds passwords, tokens, and private messages, often within seconds of you copying them. A tool that reads it is an exfiltration primitive pointed at your most sensitive short-lived data.
  • Screenshots. Same reasoning. A screen capture is everything visible, including the windows the agent was not asked about.

Some of these may come back later, each behind its own explicit opt-in, the way hs_eval is gated now. None of them will ever be in the default tier.

Reporting a vulnerability

Open a security advisory on the repository rather than a public issue.

Troubleshooting

Start with hs_health. It is designed to tell you which of these you have.

hs not found. The server looks in a fixed list of locations and then on PATH. If your Hammerspoon lives somewhere else, set HS_MCP_HS_PATH to the absolute path of the binary. Note that GUI-launched MCP clients often have a minimal PATH that does not include Homebrew, so a path that works in your terminal may not work for the server. When in doubt, set the variable.

Hammerspoon is not running. The hs CLI is a client. It needs the Hammerspoon app running to talk to. Launch Hammerspoon and retry.

hs.ipc is not loaded. Hammerspoon is running but nothing answers, or the hs binary does not exist at all. Both usually mean require("hs.ipc") is missing from ~/.hammerspoon/init.lua. Add it, reload the config from the menu bar icon, then check hs -c "return 1 + 1" in a terminal.

Tools are missing from the client's list. If hs_eval is the missing one, that is the default tier working as intended. Set HS_MCP_TOOLS=all in the client's server config, then restart the client so the server is respawned with the new environment.

A tool times out. Hammerspoon is single-threaded. If your config is stuck in a loop or a modal dialog is blocking, calls will not return. Check the console with hs_console_tail, or reload the config.

Development

git clone https://github.com/vukvukovich/hammerspoon-mcp.git
cd hammerspoon-mcp
npm install
Script What it does
npm run build Compile TypeScript to dist/ with tsc.
npm run typecheck Type check everything, no emit.
npm run lint ESLint.
npm run lint:fix ESLint with autofix.
npm run format Prettier, write.
npm run format:check Prettier, check only.
npm test Unit tests (Vitest).
npm run test:watch Unit tests in watch mode.
npm run test:coverage Unit tests with coverage.
npm run test:integration Integration tests. Needs a real, running Hammerspoon.
npm run check Everything CI runs: typecheck, lint, format, tests.

Stack: TypeScript 5.9 in strict mode, ESM only, Node 24+, the @modelcontextprotocol/server v2 SDK, Zod v4 for schemas, Vitest 4, and typescript-eslint 8 with Prettier 3. Plain tsc for the build, no bundler.

Before contributing, read CONVENTIONS.md (binding rules) and CONTRIBUTING.md (workflow and commit format). The design is written up in docs/ARCHITECTURE.md.

License

MIT. Copyright (c) 2026 Vuk Vukovich. See LICENSE.

Hammerspoon is a separate project with its own license and is not affiliated with this one.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选