A macOS computer-use toolkit for AI agents. Exposes screen capture, accessibility-tree introspection, and input simulation as MCP tools, backed by a Swift native gRPC server.
┌─────────────────┐ stdio MCP ┌──────────────┐ gRPC/UDS ┌─────────────────────┐
│ AI agent host │ ────────────► │ tingly-cu │ ───────────► │ tingly-cu-native │
│ (Claude/Codex…) │ │ (Go CLI) │ │ (Swift, AppKit) │
└─────────────────┘ └──────────────┘ └─────────────────────┘
│
▼
macOS Accessibility · ScreenCaptureKit · CGEvent
The Go CLI registers eleven MCP tools:
| Tool | Purpose |
|---|---|
list_apps |
List running + recently used apps (last 14 days, deny-listed apps filtered) |
get_app_state |
Focus the app's key window, return PNG screenshot + accessibility tree (must be called once per turn) |
snapshot |
Read-only PNG + AX tree of an already-running app — never launches, activates, or reopens; fails if the app is not running |
click |
Click by element_index from the AX tree, or by screenshot pixel coords |
type_text |
Type literal Unicode text (per-grapheme keyDown/keyUp pairs) |
press_key |
Press a key combo in xdotool syntax (Return, super+c, …) |
scroll |
Scroll an element by direction + fractional pages |
drag |
Drag from/to screenshot pixel coords |
perform_secondary_action |
Invoke a non-default AX action (e.g. AXShowMenu) |
set_value |
Set the value of a settable AX element |
turn_ended |
Clear per-turn snapshot cache (host signals end of agent turn) |
These actions are deliberately collapsed into the existing surface rather than exposed as separate tools:
- Open / launch is folded into
get_app_state: passing anappthat is not running causes the native side to launch and activate it, then snapshot — one round-trip instead of two, and no race between launch and capture. - Snapshot vs
get_app_state: usesnapshotwhen you only want to observe an app the user already has open (no focus stealing, no launching). Useget_app_statewhen you intend to act on the app and need it focused. - Close / quit is intentionally not exposed: terminating other processes is high-risk. The agent can dismiss windows or quit gracefully via
press_key(super+w/super+q), which is auditable by the host and still subject to the deny list.
- macOS 15+ (gRPC-Swift v2 generated code requires macOS 15)
- Xcode / Swift toolchain 6.0+
- Go 1.23+
- Permissions, granted to the binary that hosts the agent (the MCP client process):
- Accessibility — System Settings → Privacy & Security → Accessibility
- Screen Recording — System Settings → Privacy & Security → Screen Recording
Run task doctor to check both at once.
Each tagged release publishes a macOS arm64 (Apple Silicon) tarball on the Releases page. If you just want to run the MCP server, download instead of building:
# Replace VERSION with the tag you want (e.g. 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" # verify integrity
tar -xzf "${NAME}.tar.gz"
# The two binaries sit side-by-side, so the Go wrapper auto-discovers the
# native bridge — no TINGLY_CU_NATIVE env required.
./${NAME}/tingly-cu doctorPoint your MCP client at <extracted-dir>/tingly-cu (see MCP client configuration below).
First run on macOS — Gatekeeper notice
The release binaries are not codesigned or notarized. The first time you run them, macOS will refuse with "Apple cannot check it for malicious software". Bypass once:
- Finder: right-click
tingly-cu(andtingly-cu-native) → Open → confirm in the dialog. Subsequent runs are unrestricted.- Or via terminal:
xattr -d com.apple.quarantine tingly-cu tingly-cu-native.If you prefer signed binaries, build from source —
task releaseproduces unquarantined binaries directly.
Architectures other than arm64 (Intel x86_64, etc.) are not currently published. Build from source via the Build section if you need them.
# Build Swift native server (debug) + Go CLI in place (for `task mcp` dev loop)
task build
# Release build of both binaries, in place
task build:release
# Release build + copy both binaries side-by-side into ./dist
# (this is what MCP clients should point at — see "MCP client configuration")
task releaseBuild outputs:
task build/build:release→swift/.build/{debug,release}/tingly-cu-native,./tingly-cutask release→dist/tingly-cu,dist/tingly-cu-native
The Go binary auto-locates tingly-cu-native via, in order:
TINGLY_CU_NATIVEenv var- Same directory as the Go binary
$PATH
dist/ puts the two binaries in the same directory, so step 2 fires and no env var is needed in MCP client configs.
# Start the MCP stdio server (auto-spawns the Swift native process)
task mcp # or: go/tingly-cu mcp
# Diagnostic / utility commands
task doctor # Check macOS permissions
task list-app # List running apps
task ax -- Safari # Dump accessibility tree of an app
task snap -- Safari # Save screenshot to ./snap.pngtingly-cu mcp is a standard MCP stdio server (JSON-RPC 2.0 over stdin/stdout). Any MCP-capable host can use it by spawning the binary with mcp as its only argument.
Set up once:
task release # builds release binaries into ./dist
./dist/tingly-cu doctor # must report OK on Accessibility + Screen Recordingtask release co-locates tingly-cu and tingly-cu-native under dist/, so the Go wrapper auto-discovers the native bridge — MCP configs only need to point at dist/tingly-cu, no TINGLY_CU_NATIVE env required.
Grant the two macOS permissions to the host process that will spawn tingly-cu (e.g. Claude Desktop.app, your terminal, VS Code), not to tingly-cu itself — macOS attributes the access to the parent.
In every example below, replace /abs/path/to/tingly-computer-use/ with your actual absolute path to the repo.
Edit ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"tingly-computer-use": {
"command": "/abs/path/to/tingly-computer-use/dist/tingly-cu",
"args": ["mcp"]
}
}
}Restart Claude Desktop. The tools appear under the 🔨 menu; if not, check ~/Library/Logs/Claude/mcp*.log.
claude mcp add tingly-computer-use \
-- /abs/path/to/tingly-computer-use/dist/tingly-cu mcp
claude mcp list # verify it shows upUse --scope user to register globally instead of per-project.
Add to ~/.codex/config.toml:
[mcp_servers.tingly-computer-use]
command = "/abs/path/to/tingly-computer-use/dist/tingly-cu"
args = ["mcp"]These hosts all accept the same stdio shape — only the config file path differs:
- Cursor:
~/.cursor/mcp.json(or project-level.cursor/mcp.json) - Cline (VS Code):
cline_mcp_settings.jsonvia the Cline panel - Continue:
~/.continue/config.jsonunderexperimental.modelContextProtocolServers - opencode:
~/.config/opencode/config.json - Gemini CLI:
~/.gemini/settings.jsonundermcpServers
Use the same command + args pair shown for Claude Desktop above.
From any client, calling list_apps should return the running app list. If a call hangs or returns "native bridge not available":
- Re-run
./dist/tingly-cu doctor— permission revocations are silent. - Confirm
dist/tingly-cu-nativeexists next todist/tingly-cu(re-runtask releaseif not). - Tail the host's MCP log, or run the server directly to see structured errors:
TINGLY_CU_LOG_FORMAT=text TINGLY_CU_LOG_LEVEL=debug ./dist/tingly-cu mcp </dev/null
| Variable | Purpose | Default |
|---|---|---|
TINGLY_CU_NATIVE |
Absolute path to tingly-cu-native |
(resolves automatically) |
TINGLY_CU_LOG_LEVEL |
Log threshold: debug | info | warn | error |
info |
TINGLY_CU_LOG_FORMAT |
json (one JSON object per line) or text |
json |
TINGLY_CU_DENYLIST |
Extra bundle IDs (comma-separated) to add to the deny list | (empty) |
TINGLY_CU_DENYLIST_FILE |
Path to a file containing one bundle ID per line (# starts a comment) |
(unset) |
TINGLY_CU_ALLOWLIST |
Bundle IDs (comma-separated) that bypass all deny rules | (empty) |
TINGLY_CU_ALLOWLIST_ONLY |
If 1/true, run in whitelist mode — only allowlisted apps are permitted |
false |
TINGLY_CU_DISABLE_DEFAULT_DENYLIST |
If 1/true, drop the built-in baseline (use only your custom list) |
false |
Built-in deny baseline (terminals, password managers, system security agents, Chrome) lives in DenyList.defaultDeniedBundleIDs. Add your own via TINGLY_CU_DENYLIST_FILE:
# ~/.config/tingly/denylist.txt
com.apple.Safari # don't let agents drive my browser
com.tinyspeck.slackmacgap
export TINGLY_CU_DENYLIST_FILE=~/.config/tingly/denylist.txtBoth Go and Swift sides emit JSON-line logs to stderr:
{"ts":"2026-04-26T10:11:22.345Z","level":"info","msg":"native server listening","socket":"/tmp/tingly-cu-501.sock"}
{"ts":"2026-04-26T10:11:23.012Z","level":"warn","msg":"no window for app, reopening","app":"Safari"}For human-readable output during development:
TINGLY_CU_LOG_FORMAT=text TINGLY_CU_LOG_LEVEL=debug task mcpproto/computeruse/v1/computeruse.proto contract shared by Swift + Go
swift/Sources/TinglyComputerUse/ CLI entry (serve | doctor | version)
swift/Sources/TinglyComputerUseKit/ gRPC service, AX traversal, input simulation
go/cmd/tingly-cu/ CLI entry (mcp | doctor | ls-apps | ax | snap | version)
go/internal/bridge/ spawns native, gRPC client wrapper
go/internal/mcpserver/ MCP tool registration
go/internal/tools/ per-tool argument coercion + dispatch
build/ build/gen-proto scripts
Taskfile.yml task runner targets
# Prerequisites:
# 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
# (Swift gRPC v2 plugin from grpc/grpc-swift)
task genGenerated artifacts (go/pkg/proto/..., swift/Sources/TinglyComputerUseKit/Generated/...) are checked in.
- macOS-only. Linux/Windows targets are not implemented.
- No tests in this branch yet — Swift
Tests/is empty, no*_test.go. Integration tests against a fixture app are planned. - The screenshot is base64-encoded into the MCP
CallToolResult; very large Retina screens can stress the agent's token budget. Streaming-via-resource is on the roadmap. tingly-cu-nativeruns in the foreground with no auto-restart-on-crash; the Go side cleans up onSIGINT/SIGTERMbut does not yet supervise the child.
See LICENSE.