内容
# tingly-computer-use
用于 AI 代理的 macOS 计算机使用工具包。通过 Swift 原生 gRPC 服务器支持,提供屏幕捕获、辅助功能树内省和输入模拟作为 MCP 工具。
```
┌─────────────────┐ stdio MCP ┌──────────────┐ gRPC/UDS ┌─────────────────────┐
│ AI 代理主机 │ ────────────► │ tingly-cu │ ───────────► │ tingly-cu-native │
│ (Claude/Codex…) │ │ (Go CLI) │ │ (Swift, AppKit) │
└─────────────────┘ └──────────────┘ └─────────────────────┘
│
▼
macOS 辅助功能 · ScreenCaptureKit · CGEvent
```
## 功能
Go CLI 注册了 11 个 MCP 工具:
| 工具 | 目的 |
|---------------------------|------------------------------------------------------------------|
| `list_apps` | 列出正在运行和最近使用的应用程序(最近 14 天,过滤掉拒绝列表中的应用) |
| `get_app_state` | 聚焦应用程序的关键窗口,返回 PNG 截图和辅助功能树(必须每次调用) |
| `snapshot` | **只读** PNG 和已运行应用程序的 AX 树 — 绝不启动、激活或重新打开;如果应用程序未运行,则失败 |
| `click` | 通过 AX 树中的 `element_index` 或截图像素坐标进行点击 |
| `type_text` | 输入字面 Unicode 文本(每个字素的 keyDown/keyUp 对) |
| `press_key` | 按下 xdotool 语法中的按键组合(`Return`、`super+c`等) |
| `scroll` | 按方向和分数页面滚动元素 |
| `drag` | 从/到截图像素坐标拖动 |
| `perform_secondary_action`| 调用非默认的 AX 操作(例如 `AXShowMenu`) |
| `set_value` | 设置可设置 AX 元素的数值 |
| `turn_ended` | 清除每回合快照缓存(主机发出代理回合结束信号) |
### 为什么没有 `open_app` / 单独的 `close_app` 工具?
这些操作被刻意地合并到现有的表面,而不是作为单独的工具公开:
- **打开/启动** 被合并到 `get_app_state`:传递一个未运行的 `app` 会导致原生侧启动并激活它,然后快照 — 只需一次往返,而不是两次,并且在启动和捕获之间没有竞争。
- **快照 vs `get_app_state`**:使用 `snapshot` 时,您只想**观察**用户已经打开的应用程序(不会窃取焦点,不会启动)。使用 `get_app_state` 时,您打算**操作**应用程序并需要它聚焦。
- **关闭/退出** 被故意不公开:终止其他进程风险很高。代理可以通过 `press_key` .dismiss 窗口或正常退出(`super+w` / `super+q`),这可被主机审计,并且仍然受到拒绝列表的约束。
## 要求
- macOS **15+** (gRPC-Swift v2 生成的代码需要 macOS 15)
- Xcode / Swift 工具链 6.0+
- Go 1.23+
- 权限,授予托管代理的二进制文件(MCP 客户端进程):
- **辅助功能** — 系统设置 → 隐私和安全 → 辅助功能
- **屏幕录制** — 系统设置 → 隐私和安全 → 屏幕录制
运行 `task doctor` 同时检查两者。
## 下载(预编译二进制文件)
每个标记版本都会在 [Releases 页面](../../releases) 上发布一个 macOS arm64(Apple Silicon)tarball。如果您只需要运行 MCP 服务器,请下载而不是构建:
```bash
# 将 VERSION 替换为要使用的标签(例如 1.0.0)
VERSION=1.0.0
NAME="tingly-cu-${VERSION}-macos-arm64"
curl -LO "https://github.com/<owner>/<repo>/releases/download/v${VERSION}/${NAME}.tar.gz"
curl -LO "https://github.com/<owner>/<repo>/releases/download/v${VERSION}/${NAME}.tar.gz.sha256"
shasum -a 256 -c "${NAME}.tar.gz.sha256" # 验证完整性
tar -xzf "${NAME}.tar.gz"
# 两个二进制文件并排放置,因此 Go 包装器自动发现原生桥 — 不需要 TINGLY_CU_NATIVE 环境变量。
./${NAME}/tingly-cu doctor
```
将您的 MCP 客户端指向 `<extracted-dir>/tingly-cu`(参见下面的 [MCP 客户端配置](#mcp-客户端配置))。
> **macOS 首次运行 — Gatekeeper 通知**
>
> 发布二进制文件**未签名或 notarized**。第一次运行它们时,macOS 将拒绝并显示“Apple 无法检查恶意软件”。绕过一次:
>
> - **Finder**:右键单击 `tingly-cu`(和 `tingly-cu-native`)→ **打开** → 在对话框中确认。后续运行不受限制。
> - **或通过终端**:`xattr -d com.apple.quarantine tingly-cu tingly-cu-native`。
>
> 如果您更喜欢签名的二进制文件,请从源代码构建 — `task release` 直接生成未隔离的二进制文件。
其他架构(Intel x86_64 等)当前未发布。如果您需要它们,请通过 [Build](#build) 部分从源代码构建。
## 构建
```bash
# 在本地构建 Swift 原生服务器(调试)和 Go CLI(用于 `task mcp` 开发循环)
task build
# 发布构建两个二进制文件,在本地
task build:release
# 发布构建 + 将两个二进制文件复制到 ./dist
# (这是 MCP 客户端应该指向的内容 — 参见“MCP 客户端配置”)
task release
```
构建输出:
- `task build` / `build:release` → `swift/.build/{debug,release}/tingly-cu-native`,`./tingly-cu`
- `task release` → `dist/tingly-cu`,`dist/tingly-cu-native`
Go 二进制文件通过以下方式自动定位 `tingly-cu-native`:
1. `TINGLY_CU_NATIVE` 环境变量
2. 与 Go 二进制文件相同的目录
3. `$PATH`
`dist/` 将两个二进制文件放在同一个目录中,因此步骤 2 生效,**MCP 客户端配置中不需要环境变量**。
## 运行
```bash
# 启动 MCP stdio 服务器(自动启动 Swift 原生进程)
task mcp # 或:go/tingly-cu mcp
# 诊断/实用程序命令
task doctor # 检查 macOS 权限
task list-app # 列出正在运行的应用程序
task ax -- Safari # 转储应用程序的辅助功能树
task snap -- Safari # 将截图保存到 ./snap.png
```
## MCP 客户端配置
`tingly-cu mcp` 是一个标准的 **MCP stdio 服务器**(JSON-RPC 2.0 via stdin/stdout)。任何支持 MCP 的主机都可以通过将二进制文件作为唯一参数启动来使用它。
**设置一次:
```bash
task release # 将发布二进制文件构建到 ./dist
./dist/tingly-cu doctor # 必须报告辅助功能 + 屏幕录制的正常状态
```
`task release` 将 `tingly-cu` 和 `tingly-cu-native` 放在 `dist/` 下,因此 Go 包装器自动发现原生桥 — **MCP 配置只需要指向 `dist/tingly-cu`,不需要 `TINGLY_CU_NATIVE` 环境变量**。
授予 **将要启动 `tingly-cu` 的主机进程**(例如 Claude Desktop.app、您的终端、VS Code)macOS 权限,而不是 `tingly-cu` 本身 — macOS 将访问权限归因于父进程。
在下面的每个示例中,将 `/abs/path/to/tingly-computer-use/` 替换为您的存储库的实际绝对路径。
### Claude Desktop
编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"tingly-computer-use": {
"command": "/abs/path/to/tingly-computer-use/dist/tingly-cu",
"args": ["mcp"]
}
}
}
```
重启 Claude Desktop。工具出现在 🔨 菜单下;如果没有,请检查 `~/Library/Logs/Claude/mcp*.log`。
### Claude Code(CLI)
```bash
claude mcp add tingly-computer-use \
-- /abs/path/to/tingly-computer-use/dist/tingly-cu mcp
claude mcp list # 验证它是否显示
```
使用 `--scope user` 在全局范围内注册,而不是按项目注册。
### Codex CLI
添加到 `~/.codex/config.toml`:
```toml
[mcp_servers.tingly-computer-use]
command = "/abs/path/to/tingly-computer-use/dist/tingly-cu"
args = ["mcp"]
```
### Cursor / Cline / Continue / opencode / Gemini CLI
这些主机都接受相同的 stdio 格式 — 只有配置文件路径不同:
- Cursor:`~/.cursor/mcp.json`(或项目级 `.cursor/mcp.json`)
- Cline(VS Code):通过 Cline 面板使用 `cline_mcp_settings.json`
- Continue:`~/.continue/config.json` 下的 `experimental.modelContextProtocolServers`
- opencode:`~/.config/opencode/config.json`
- Gemini CLI:`~/.gemini/settings.json` 下的 `mcpServers`
使用上面为 Claude Desktop 显示的相同 `command` + `args` 对。
### 验证连接
从任何客户端调用 `list_apps` 应该返回正在运行的应用程序列表。如果调用挂起或返回“原生桥不可用”:
1. 重新运行 `./dist/tingly-cu doctor` — 权限撤销是静默的。
2. 确认 `dist/tingly-cu-native` 存在于 `dist/tingly-cu` 旁边(如果不存在,请重新运行 `task release`)。
3. 尾随主机的 MCP 日志,或直接运行服务器以查看结构化错误:
```bash
TINGLY_CU_LOG_FORMAT=text TINGLY_CU_LOG_LEVEL=debug ./dist/tingly-cu mcp </dev/null
```
## 配置(环境变量)
| 变量 | 目的 | 默认值 |
|-----------------------------------------|--------------------------------------------------------------------------------------------------|-----------------------|
| `TINGLY_CU_NATIVE` | `tingly-cu-native` 的绝对路径 | (自动解析) |
| `TINGLY_CU_LOG_LEVEL` | 日志阈值:`debug` \| `info` \| `warn` \| `error` | `info` |
| `TINGLY_CU_LOG_FORMAT` | `json`(每行一个 JSON 对象)或 `text` | `json` |
| `TINGLY_CU_DENYLIST` | 要添加到拒绝列表中的额外包 ID(逗号分隔) | (空) |
| `TINGLY_CU_DENYLIST_FILE` | 包含每个包 ID 的文件路径(一行一个,`#` 开头为注释) | (未设置) |
| `TINGLY_CU_ALLOWLIST` | 绕过所有拒绝规则的包 ID(逗号分隔) | (空) |
| `TINGLY_CU_ALLOWLIST_ONLY` | 如果为 `1`/`true`,则运行在**白名单模式** — 仅允许列出的应用程序 | `false` |
| `TINGLY_CU_DISABLE_DEFAULT_DENYLIST` | 如果为 `1`/`true`,则删除内置基线(仅使用自定义列表) | `false` |
**内置拒绝基线**(终端、密码管理器、系统安全代理、Chrome)位于 [`DenyList.defaultDeniedBundleIDs`](swift/Sources/TinglyComputerUseKit/DenyList.swift)。通过 `TINGLY_CU_DENYLIST_FILE` 添加您自己的:
```text
# ~/.config/tingly/denylist.txt
com.apple.Safari # 不让代理驱动我的浏览器
com.tinyspeck.slackmacgap
```
```bash
export TINGLY_CU_DENYLIST_FILE=~/.config/tingly/denylist.txt
```
## 日志记录
Go 和 Swift 端都将 JSON 行日志输出到 stderr:
```json
{"ts":"2026-04-26T10:11:22.345Z","level":"info","msg":"原生服务器监听中","socket":"/tmp/tingly-cu-501.sock"}
{"ts":"2026-04-26T10:11:23.012Z","level":"warn","msg":"应用程序无窗口,重新打开中","app":"Safari"}
```
在开发过程中进行人类可读的输出:
```bash
TINGLY_CU_LOG_FORMAT=text TINGLY_CU_LOG_LEVEL=debug task mcp
```
## 布局
```
proto/computeruse/v1/computeruse.proto Swift + Go 共享的合约
swift/Sources/TinglyComputerUse/ CLI 入口(serve | doctor | version)
swift/Sources/TinglyComputerUseKit/ gRPC 服务,AX 遍历,输入模拟
go/cmd/tingly-cu/ CLI 入口(mcp | doctor | ls-apps | ax | snap | version)
go/internal/bridge/ 启动原生,gRPC 客户端包装器
go/internal/mcpserver/ MCP 工具注册
go/internal/tools/ 每个工具的参数强制转换 + 分派
build/ 构建/gen-proto 脚本
Taskfile.yml 任务运行器目标
```
## 重新生成 proto 代码
```bash
# 先决条件:
# brew install protobuf swift-protobuf
# go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# (来自 grpc/grpc-swift 的 Swift gRPC v2 插件)
task gen
```
生成的工件(`go/pkg/proto/...`,`swift/Sources/TinglyComputerUseKit/Generated/...`)被检查。
## 状态和限制
- **仅限 macOS。** Linux/Windows 目标未实现。
- **无测试** 在此分支中 — Swift `Tests/` 为空,无 `*_test.go`。计划对固定应用程序进行集成测试。
- 截图被 base64 编码到 MCP `CallToolResult` 中;非常大的 Retina 屏幕可能会给代理的令牌预算带来压力。基于资源的流式传输正在路线图中。
- `tingly-cu-native` 在前台运行,无自动重启功能;Go 端在 `SIGINT/SIGTERM` 上清理,但尚未监督子进程。
## 许可证
参见 [LICENSE](LICENSE)。