Hands

Hands

A macOS computer-use MCP server that grants AI agents mouse, keyboard, and screen control with a robust security model including permission profiles, app deny-lists, and audit logging.

Category
访问服务器

README

<div align="center">

🖐️ Hands

A macOS computer-use MCP server for autonomous AI agents.

License: MIT Python 3.12+ Tests MCP CI

Give an LLM eyes, a mouse, and a keyboard — with a real security model in between.

Quick StartToolsSecurity ModelPluginsDocs

</div>


Most "computer use" demos give a model raw pixel coordinates and hope for the best. Hands is built differently: every action goes through a typed 7-phase dispatch pipeline — validate → rate-limit → authorize → lock → execute-with-retry → observe → audit — so that giving an agent control of your desktop doesn't mean giving up any of your say in what it's allowed to do.

import anyio
from hands import Container, HandsConfig

config = HandsConfig()
config.driver = "fake"          # no macOS needed — great for a first look
container = Container.build(config)

async def main():
    shot = await container.dispatcher.dispatch("screenshot", {})
    print(shot["ok"], shot["bounds_pt"])

anyio.run(main)

Why Hands

  • A real policy engine, not a rubber stamp. Rule-based profiles (strict / default / trusted), an app deny-list (Passwords, 1Password, System Settings — blocked out of the box), secret-pattern detection on typed text, per-tool confirmation, and a sliding-window rate limiter. See Security Model.
  • Tamper-evident audit log. Every action is appended to a SHA-256 hash-chained JSONL log; deleting or editing any line downstream breaks the chain, and hands audit verify proves it.
  • Redaction by construction. Clipboard content and typed text never enter application state, logs, audit records, or metrics — only their length and a SHA-256 hash ever do.
  • A fake driver, not just mocks. FakeDriver is a full in-memory virtual desktop (windows, apps, clipboard, AX tree, OCR boxes) that every tool runs against identically to the real macOS driver. The entire test suite — 215 tests — runs on Linux CI with zero macOS hardware, and it's the fastest way to try Hands out or build a plugin without touching your real screen.
  • Perception, not just action. OCR-grounded text search (find_text), an accessibility-tree fallback (get_ui_tree), and a verify/wait condition language so an agent can confirm an action worked instead of flying blind.
  • A kill switch that actually works. touch ~/.hands/KILL halts the server immediately — checked before every single dispatch, including mid-way through a batched execute_sequence.
  • Extensible without forking. Third-party plugins register tools through the exact same ToolSpec machinery as the 22 built-ins, loaded via Python entry points; a plugin that raises during setup is logged and skipped, not a server crash. (Plugins run in-process with access to the same services — this is crash isolation, not a security sandbox; don't load untrusted plugin code any more than you would an untrusted import.)

Requirements

  • macOS 13+ (Ventura or later) for real desktop control
  • Python 3.12+
  • Screen Recording and Accessibility permissions (System Settings → Privacy & Security) — hands permissions tells you exactly what's missing

You can install and explore the library on any OS using the fake driver (HANDS_DRIVER=fake) — only real desktop control requires macOS.

Installation

pip install hands-mcp

With the macOS driver and Vision OCR (needed for real desktop control):

pip install "hands-mcp[macos]"

The import name is still hands regardless of the install name above:

import hands
print(hands.__version__)

For development:

git clone https://github.com/yuvitbatra/hands-mcp.git
cd hands-mcp
uv sync --group dev
uv run pytest -q

Quick Start

As an MCP server (for AI agents)

Add to your MCP client configuration (e.g. Claude Desktop's claude_desktop_config.json):

{
  "mcpServers": {
    "hands": {
      "command": "hands",
      "args": ["serve"]
    }
  }
}

Or run it directly over stdio:

hands serve

As a Python library

import anyio
from hands import Container, HandsConfig

# The fake driver is a full in-memory virtual desktop — no macOS needed.
config = HandsConfig()
config.driver = "fake"
container = Container.build(config)

async def main():
    res = await container.dispatcher.dispatch("screenshot", {})
    print(res["ok"])  # True

anyio.run(main)
# Or run the full MCP server over stdio from your own process:
import anyio
from hands import run_server, load_config

anyio.run(run_server, load_config())

Diagnostics

hands doctor              # resolved config, driver, displays, registered tools
hands doctor --metrics    # ...plus a metrics snapshot (counters + latency histograms)
hands permissions         # TCC grant status (Screen Recording, Accessibility) + fix links
hands audit verify        # verify the audit log's hash chain end to end

Tools (22)

Full argument/return reference: docs/TOOLS.md.

Tool Policy Description
screenshot read Capture the screen (or a region); cached by default
get_state read Session state: cursor, displays, action history, dirty flag
find_text read OCR search returning bounding boxes for matching text
get_ui_tree read Accessibility tree for an app (roles, labels, frames)
wait read Poll until a condition is met: text present, window state, duration
verify read Check an expected outcome after acting; returns evidence
mouse_move act Move the cursor
mouse_click act Click at a point (single, double, or triple)
mouse_drag act Click-drag along a path
mouse_scroll act Scroll at a point
keyboard_type act Type text into the focused element (refused during secure input)
key_press act Press a key or chord (e.g. cmd+s, Return)
clipboard_get sensitive Read the current clipboard (text or image)
clipboard_set act Set the clipboard to text or a PNG image
clipboard_paste act Paste via clipboard + Cmd+V, restoring the clipboard after
window_list read List windows, optionally filtered by app
window_focus act Bring a window to front (stale refs re-resolve by fuzzy title)
window_manage act → sensitive on close Move, resize, minimize, maximize, or close a window
app_open act Launch an app by bundle ID or name, waiting for its window
app_close act → sensitive on force Quit an app gracefully, or force-terminate
app_list read List running apps and the frontmost one
execute_sequence act Batch up to 20 pre-decided acting steps in one round trip, gated by guard conditions

Security Model

An agent that can move your mouse and type on your behalf is a genuinely different risk profile from a chatbot, so the policy layer isn't an afterthought — it's a fixed phase of every dispatch, before the tool ever runs:

validate args → rate limit → authorize (PermissionEngine) → acquire action lock
    → execute with retry → record state/screen-dirty → audit + metrics

Profiles (HANDS_SECURITY__PROFILE, default default):

Profile act tools sensitive tools
strict requires confirmation requires confirmation
default allowed requires confirmation
trusted allowed allowed

Evaluation order for every action: (1) is the frontmost app on the deny-list? → denied outright, even under trusted. (2) does a configured rule match? → first match wins. (3) does typed/pasted text match a secret_patterns regex? → confirmation required regardless of profile. (4) fall through to the profile's class default above.

App deny-list (HANDS_SECURITY__DENY_APPS, on by default): blocks acting tools — reads still work — against com.apple.systempreferences*, com.apple.Passwords*, com.apple.keychainaccess, 1Password (both bundle id families). This applies uniformly whether the tool is called directly or from inside execute_sequence.

Rate limiting (HANDS_SECURITY__MAX_ACTIONS_PER_S, default 10.0): a 1-second sliding window over acting tools; bursts past the limit are denied, not queued.

Secure input refusal: keyboard_type and clipboard_get are refused outright while macOS reports secure text entry active (a password field has focus) — independent of profile.

Confirmation hooks: under dialog mode (default, macOS only) a denied-by-default sensitive action pops a real confirmation dialog via osascript; under deny mode (or off-macOS) it's auto-denied. Plug in your own hook by subclassing the PermissionEngine's confirm callback if you need a different UX (e.g. routing through your agent framework's own approval flow).

Redaction invariant: clipboard content and typed text are used only in-memory for policy matching (e.g. secret-pattern checks) — they never reach state, the audit log, or metrics in raw form. Only length and a SHA-256 hash ever leave those boundaries.

Kill switch: touch ~/.hands/KILL halts the server immediately — checked at the top of every dispatch, including mid-sequence inside execute_sequence. Remove the file to resume.

Audit log: every action is appended to ~/.hands/audit.jsonl as {"event", "prev_hash", "hash"}, where hash = sha256(prev_hash + canonical_json(event)). Tampering with or deleting any line downstream breaks the chain — verify with hands audit verify.

Configuration

All settings can be overridden via HANDS_* environment variables (nested fields use __, e.g. HANDS_SECURITY__PROFILE). Full schema: src/hands/config.py.

Variable Default Description
HANDS_DRIVER auto auto (macOS on darwin, else fake), fake, or macos
HANDS_SECURITY__PROFILE default strict, default, or trusted
HANDS_SECURITY__DENY_APPS see above JSON list of bundle-id globs blocked for acting tools
HANDS_SECURITY__SECRET_PATTERNS [] JSON list of regexes that force confirmation on typed/pasted text
HANDS_SECURITY__MAX_ACTIONS_PER_S 10.0 Sliding-window rate limit for acting tools
HANDS_SECURITY__CONFIRMATION dialog dialog (real macOS confirm dialog) or deny (auto-deny, safe for CI)
HANDS_SECURITY__PLUGIN_ALLOWLIST (all allowed) JSON list of allowed plugin entry-point names
HANDS_SCREENSHOT__MAX_DIM 1568 Max screenshot dimension
HANDS_SCREENSHOT__JPEG_QUALITY 80 JPEG quality (0–100)
HANDS_MOUSE__CLICK_DELAY_MS 8 Delay between mouse down and up
HANDS_KEYBOARD__CHUNK_SIZE 32 Characters per typing chunk
HANDS_AX__MAX_NODES 500 Depth-first-traversal node cap for get_ui_tree

Plugin System

Hands supports external plugins that register custom tools through the exact same ToolSpec machinery as the built-ins — no side door around the dispatcher, so plugin tools get the same policy, retry, and audit treatment.

from hands.plugins.api import HandsPlugin, PluginContext

class MyPlugin:
    name = "my-plugin"
    version = "1.0.0"

    def setup(self, ctx: PluginContext) -> None:
        from pydantic import BaseModel
        from hands.registry import ToolSpec
        from hands.retry import RetryPolicy

        class Args(BaseModel, extra="forbid"):
            query: str

        async def my_tool(args: Args, ctx_) -> dict:
            return {"result": f"searched for {args.query}"}

        ctx.registry.register(ToolSpec(
            "my_search", "Custom search tool",
            Args, my_tool, "read", RetryPolicy.read(), idempotent=True))

    def teardown(self) -> None:
        pass

Register it via the hands.plugins entry-point group in your own package's pyproject.toml:

[project.entry-points."hands.plugins"]
my-plugin = "my_package.plugin:MyPlugin"

Plugins load at server startup. A plugin that raises in setup is logged and skipped — it never takes the server down. Restrict which plugins may load with an allowlist:

HANDS_SECURITY__PLUGIN_ALLOWLIST='["my-plugin"]' hands serve

See docs/plugins.md for the full plugin author's guide, and src/hands/plugins/api.py for the stable import surface (semver-guarded — additive-only within a major version).

Testing

uv run pytest -q                                          # 215 tests, any OS, ~2s
HANDS_CONTRACT_MACOS=1 uv run pytest tests/contract -q    # real macOS driver (needs TCC grants)
HANDS_E2E_MACOS=1 uv run pytest tests/e2e -q              # full stack vs. a real Tk fixture app
uv run pytest tests/perf -m perf --benchmark-only -q      # latency budgets
uv run pytest tests/stress -m stress -q                   # 10k-action soak + concurrency

The contract/e2e suites are opt-in because they genuinely move your mouse and open/close real apps — don't run them unattended. See CONTRIBUTING.md for the full development workflow.

Architecture

types, errors, config, retry
    ↓
driver/base (Protocol) ← fake.py (tests/CI) + macos.py (real hardware)
    ↓
services/  (screenshot, ocr, mouse, keyboard, clipboard, windows, apps,
            waiter, verification)
    ↓
state, permissions (PermissionEngine), audit (hash-chained), metrics, registry
    ↓
dispatcher  (7-phase pipeline: validate → rate-limit → authorize → lock →
             execute-with-retry → observe → audit)
    ↓
tools/      (22 MCP tools)
    ↓
container → server (MCP stdio transport) → cli

Full design rationale: docs/DESIGN.md.

Contributing

Contributions welcome — see CONTRIBUTING.md for the development setup, test-driven workflow, and what a good PR looks like.

License

MIT

推荐服务器

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 模型以安全和受控的方式获取实时的网络信息。

官方
精选