ucu-mcp 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +28 -3
- package/dist/src/mcp/server.js +1 -0
- package/dist/src/mcp/tools/element-tools.js +23 -2
- package/dist/src/mcp/tools/index.d.ts +1 -1
- package/dist/src/mcp/tools/index.js +1 -1
- package/dist/src/mcp/tools/keyboard-tools.js +2 -2
- package/dist/src/mcp/tools/screen-tools.js +139 -3
- package/dist/src/platform/base.d.ts +43 -1
- package/dist/src/platform/linux.d.ts +2 -2
- package/dist/src/platform/macos/base.d.ts +3 -1
- package/dist/src/platform/macos/base.js +7 -1
- package/dist/src/platform/macos/element.d.ts +22 -1
- package/dist/src/platform/macos/element.js +377 -13
- package/dist/src/platform/macos/focus.d.ts +10 -0
- package/dist/src/platform/macos/focus.js +10 -0
- package/dist/src/platform/macos/screen.js +21 -18
- package/dist/src/platform/macos/window.js +22 -0
- package/dist/src/platform/windows.d.ts +2 -2
- package/dist/src/safety/guard.js +17 -2
- package/dist/src/utils/input.js +24 -16
- package/package.json +2 -1
- package/skills/ucu-mcp/SKILL.md +100 -0
- package/skills/ucu-mcp/agents/openai.yaml +4 -0
- package/skills/ucu-mcp/references/tool-reference.md +255 -0
- package/skills/ucu-mcp/references/troubleshooting.md +142 -0
- package/skills/ucu-mcp/references/workflows.md +146 -0
package/dist/src/safety/guard.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
const DEFAULT_BLOCKED_KEYS = [
|
|
13
13
|
// macOS – app-level
|
|
14
14
|
"cmd+q",
|
|
15
|
+
"cmd+shift+q", // log out(方向2 后字母 q 可解析,须显式拦截)
|
|
16
|
+
"cmd+option+q", // log out variant
|
|
15
17
|
"cmd+w",
|
|
16
18
|
"cmd+l", // lock screen
|
|
17
19
|
// macOS – system-level
|
|
@@ -74,12 +76,22 @@ const DEFAULT_TEXT_INJECTION_PATTERNS = [
|
|
|
74
76
|
// ---------------------------------------------------------------------------
|
|
75
77
|
// Helpers
|
|
76
78
|
// ---------------------------------------------------------------------------
|
|
77
|
-
/**
|
|
79
|
+
/** Modifier alias → canonical name. MAC_MODIFIER_FLAGS accepts both forms
|
|
80
|
+
* (cmd/command, option/alt, control/ctrl); normalize them so a blocklist
|
|
81
|
+
* entry like "cmd+option+esc" also catches "cmd+alt+esc". */
|
|
82
|
+
const MODIFIER_CANONICAL = {
|
|
83
|
+
alt: "option", ctrl: "control", cmd: "command",
|
|
84
|
+
};
|
|
85
|
+
/** Normalize a shortcut string to lowercase, trimmed, sorted modifiers with
|
|
86
|
+
* modifier aliases canonicalized (alt→option, ctrl→control, cmd→command). */
|
|
78
87
|
function normalizeShortcut(raw) {
|
|
79
88
|
return raw
|
|
80
89
|
.toLowerCase()
|
|
81
90
|
.split("+")
|
|
82
|
-
.map((s) =>
|
|
91
|
+
.map((s) => {
|
|
92
|
+
const t = s.trim();
|
|
93
|
+
return MODIFIER_CANONICAL[t] ?? t;
|
|
94
|
+
})
|
|
83
95
|
.sort()
|
|
84
96
|
.join("+");
|
|
85
97
|
}
|
|
@@ -104,6 +116,8 @@ export const OBSERVE_ACTIONS = new Set([
|
|
|
104
116
|
// and an AX window lookup — it does not synthesize mouse or keyboard input,
|
|
105
117
|
// so the user-activity pause must not block it. (OpenCode 0.3.7 follow-up)
|
|
106
118
|
"focus_app",
|
|
119
|
+
// describe_screen reads screen state (OCR + AX), no input synthesis.
|
|
120
|
+
"describe_screen",
|
|
107
121
|
]);
|
|
108
122
|
/** Actions that synthesize user input — need full user-activity protection. */
|
|
109
123
|
export const INPUT_ACTIONS = new Set([
|
|
@@ -117,6 +131,7 @@ export const INPUT_ACTIONS = new Set([
|
|
|
117
131
|
"click_element",
|
|
118
132
|
"type_in_element",
|
|
119
133
|
"set_value",
|
|
134
|
+
"click_menu_bar_extra",
|
|
120
135
|
"clipboard_write",
|
|
121
136
|
]);
|
|
122
137
|
export function classifyAction(action) {
|
package/dist/src/utils/input.js
CHANGED
|
@@ -80,6 +80,20 @@ const MAC_MODIFIER_FLAGS = {
|
|
|
80
80
|
option: 0x00080000, alt: 0x00080000,
|
|
81
81
|
control: 0x00040000, ctrl: 0x00040000,
|
|
82
82
|
};
|
|
83
|
+
// 字母/数字 keyCode —— typeText 与 pressKey 共享的唯一数据源。
|
|
84
|
+
// pressKey 在 MAC_KEY_CODES(特殊键)未命中时回退查这两个 map,让 Cmd+M / Cmd+W 等
|
|
85
|
+
// 含字母的快捷键可用。注意 'a' 的 keyCode 是 0,查找时必须用 `in` 判定存在性,
|
|
86
|
+
// 不能用 truthy(否则 0 会被当成未命中而穿透到 digit map)。
|
|
87
|
+
const MAC_LETTER_KEY_CODES = {
|
|
88
|
+
a: 0, s: 1, d: 2, f: 3, h: 4, g: 5, z: 6, x: 7, c: 8, v: 9,
|
|
89
|
+
b: 11, q: 12, w: 13, e: 14, r: 15, y: 16, t: 17,
|
|
90
|
+
o: 31, u: 32, i: 33, p: 34, l: 37, j: 38, k: 40,
|
|
91
|
+
n: 45, m: 46,
|
|
92
|
+
};
|
|
93
|
+
const MAC_DIGIT_KEY_CODES = {
|
|
94
|
+
"1": 18, "2": 19, "3": 20, "4": 21, "5": 23,
|
|
95
|
+
"6": 22, "7": 26, "8": 28, "9": 25, "0": 29,
|
|
96
|
+
};
|
|
83
97
|
// ── AppleScript string escaping ───────────────────────────────────────────
|
|
84
98
|
function escapeAppleScriptString(str) {
|
|
85
99
|
return str
|
|
@@ -288,23 +302,11 @@ export async function typeText(text, delay = 20, _platform = process.platform) {
|
|
|
288
302
|
if (_platform === "darwin") {
|
|
289
303
|
// Character -> { keyCode, shift? } map for CGEvent injection
|
|
290
304
|
const CHAR_TO_KEY = {};
|
|
291
|
-
|
|
292
|
-
const letterMap = {
|
|
293
|
-
a: 0, s: 1, d: 2, f: 3, h: 4, g: 5, z: 6, x: 7, c: 8, v: 9,
|
|
294
|
-
b: 11, q: 12, w: 13, e: 14, r: 15, y: 16, t: 17,
|
|
295
|
-
o: 31, u: 32, i: 33, p: 34, l: 37, j: 38, k: 40,
|
|
296
|
-
n: 45, m: 46,
|
|
297
|
-
};
|
|
298
|
-
for (const [ch, code] of Object.entries(letterMap)) {
|
|
305
|
+
for (const [ch, code] of Object.entries(MAC_LETTER_KEY_CODES)) {
|
|
299
306
|
CHAR_TO_KEY[ch] = { code };
|
|
300
307
|
CHAR_TO_KEY[ch.toUpperCase()] = { code, shift: true };
|
|
301
308
|
}
|
|
302
|
-
|
|
303
|
-
const digitMap = {
|
|
304
|
-
"1": 18, "2": 19, "3": 20, "4": 21, "5": 23,
|
|
305
|
-
"6": 22, "7": 26, "8": 28, "9": 25, "0": 29,
|
|
306
|
-
};
|
|
307
|
-
for (const [ch, code] of Object.entries(digitMap)) {
|
|
309
|
+
for (const [ch, code] of Object.entries(MAC_DIGIT_KEY_CODES)) {
|
|
308
310
|
CHAR_TO_KEY[ch] = { code };
|
|
309
311
|
}
|
|
310
312
|
// Unshifted symbols
|
|
@@ -422,9 +424,15 @@ export async function pressKey(key, modifiers = [], _platform = process.platform
|
|
|
422
424
|
return;
|
|
423
425
|
}
|
|
424
426
|
if (_platform === "darwin") {
|
|
425
|
-
const
|
|
427
|
+
const lookup = key.toLowerCase();
|
|
428
|
+
// 先查特殊键,未命中再回退查字母/数字(让 Cmd+M / Cmd+W 等含字母的快捷键可用)。
|
|
429
|
+
// 用 `in` 判定存在性——'a' 的 keyCode 是 0,truthy 判断会误穿透到 digit map。
|
|
430
|
+
const keyCode = lookup in MAC_KEY_CODES ? MAC_KEY_CODES[lookup] :
|
|
431
|
+
(key.length === 1 && lookup in MAC_LETTER_KEY_CODES) ? MAC_LETTER_KEY_CODES[lookup] :
|
|
432
|
+
(key.length === 1 && key in MAC_DIGIT_KEY_CODES) ? MAC_DIGIT_KEY_CODES[key] :
|
|
433
|
+
undefined;
|
|
426
434
|
if (keyCode === undefined) {
|
|
427
|
-
throw new Error(`Unknown key: ${key}. Supported keys: ${Object.keys(MAC_KEY_CODES).join(", ")}`);
|
|
435
|
+
throw new Error(`Unknown key: ${key}. Supported keys: special keys (${Object.keys(MAC_KEY_CODES).join(", ")}), single letters a-z, single digits 0-9`);
|
|
428
436
|
}
|
|
429
437
|
// Build modifier flags
|
|
430
438
|
let flags = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucu-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "MCP server for Universal Computer Use — desktop automation for AI agents via Model Context Protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dist/src/",
|
|
13
13
|
"dist/index.js",
|
|
14
14
|
"dist/index.d.ts",
|
|
15
|
+
"skills/",
|
|
15
16
|
"README.md",
|
|
16
17
|
"CHANGELOG.md"
|
|
17
18
|
],
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ucu-mcp
|
|
3
|
+
description: >-
|
|
4
|
+
Guidance for using UCU-MCP, the macOS computer-use MCP server (screenshot,
|
|
5
|
+
click, type, OCR, AX element tools, menu-bar tray support). Use when an
|
|
6
|
+
agent needs to automate macOS desktop apps over MCP — establishing target
|
|
7
|
+
context, reading screen state, interacting with UI elements, operating
|
|
8
|
+
menu-bar/tray apps, or recovering from AX/permission errors. Covers Claude
|
|
9
|
+
Code CLI/Desktop, Codex, OpenCode, and other MCP clients.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# UCU-MCP
|
|
13
|
+
|
|
14
|
+
UCU-MCP is a cross-client computer-use MCP server for macOS (Windows/Linux are
|
|
15
|
+
explicit stubs). It exposes 26 tools that let an agent see the screen and drive
|
|
16
|
+
native apps through a combination of Accessibility (AX) APIs, CGEvent input
|
|
17
|
+
synthesis, Vision OCR, and ScreenCaptureKit screenshots.
|
|
18
|
+
|
|
19
|
+
- npm package: `ucu-mcp`
|
|
20
|
+
- Run: `npx -y ucu-mcp` (stdio MCP server) or install globally via `npm i -g ucu-mcp`
|
|
21
|
+
|
|
22
|
+
## Core Workflow
|
|
23
|
+
|
|
24
|
+
1. **Check readiness** → `doctor` verifies Accessibility + Screen Recording
|
|
25
|
+
permissions and native helpers. If anything is missing, follow its guidance
|
|
26
|
+
(see [troubleshooting](references/troubleshooting.md)).
|
|
27
|
+
2. **Establish target context** → `list_apps` then `focus_app(name)` sets the
|
|
28
|
+
active window target. Subsequent AX tools operate against that target.
|
|
29
|
+
3. **Prefer AX over coordinates** → `find_element(text/role/value)` →
|
|
30
|
+
`click_element` / `type_in_element` / `set_value`. AX is precise and survives
|
|
31
|
+
layout shifts; coordinates are a last resort.
|
|
32
|
+
4. **When AX is opaque (Electron/Tauri/WebView)** → `screenshot` + `ocr` to
|
|
33
|
+
locate text by bounding box, then `click(x, y)` at the returned coordinates.
|
|
34
|
+
5. **When image content is not visible to you** (relayed/downgraded to a URL) →
|
|
35
|
+
`screenshot(describe: true)` or the standalone `describe_screen` tool to get a
|
|
36
|
+
structured text view (OCR blocks + AX tree + foreground window).
|
|
37
|
+
6. **Menu-bar/tray apps** (e.g. cc-switch) → `click_menu_bar_extra(app,
|
|
38
|
+
description/name/index)` opens the tray menu, then `find_element` inside it.
|
|
39
|
+
7. **Verify actions** → pass `captureAfter: true` on action tools, or call
|
|
40
|
+
`screenshot` / `get_window_state` afterwards.
|
|
41
|
+
8. **Recover from errors** → every error response carries a `hint` with the
|
|
42
|
+
next step. See the [error code table](references/troubleshooting.md).
|
|
43
|
+
|
|
44
|
+
Full tool inventory with parameters: [tool-reference](references/tool-reference.md).
|
|
45
|
+
Common task playbooks: [workflows](references/workflows.md).
|
|
46
|
+
|
|
47
|
+
## Operating Rules
|
|
48
|
+
|
|
49
|
+
- **AX-first.** Use `find_element` → `click_element` / `type_in_element` /
|
|
50
|
+
`set_value` whenever the AX tree exposes the target. Fall back to coordinates
|
|
51
|
+
only when AX returns nothing (Electron/WebView) or the control silently
|
|
52
|
+
swallows AX actions.
|
|
53
|
+
- **Observe before acting.** Call `screenshot` / `get_window_state` /
|
|
54
|
+
`describe_screen` before destructive or hard-to-reverse actions so you act on
|
|
55
|
+
current state, not assumptions.
|
|
56
|
+
- **TARGET_STALE is recoverable.** Re-run `focus_app` for the target app, then
|
|
57
|
+
retry — the element cache refetches equivalent AX nodes.
|
|
58
|
+
- **Tray apps need `click_menu_bar_extra`.** `focus_app` alone cannot reach
|
|
59
|
+
pure menu-bar (LSUIElement) apps; their status item is hosted by
|
|
60
|
+
`SystemUIServer` and is not in any app window's AX tree.
|
|
61
|
+
- **Dangerous actions are blocked.** Quit/logout/lock shortcuts (`cmd+q`,
|
|
62
|
+
`cmd+shift+q`, `cmd+l`, …), sensitive-window URLs, and suspicious injected
|
|
63
|
+
text are rejected by the safety guard. Choose a safer action or ask the user.
|
|
64
|
+
- **Sensitive fields are masked in `describe_screen`.** Password fields
|
|
65
|
+
(`AXSecureTextField`, or names matching `/password|secret|token/i`) appear as
|
|
66
|
+
`[REDACTED]` — never try to read or exfiltrate them.
|
|
67
|
+
- **macOS is locked → actions blocked.** The server refuses to synthesize input
|
|
68
|
+
while the screen is locked; wait for unlock or ask the user.
|
|
69
|
+
|
|
70
|
+
## MCP Config
|
|
71
|
+
|
|
72
|
+
Add UCU-MCP to your MCP client. Stdio transport, no arguments needed.
|
|
73
|
+
|
|
74
|
+
**Codex / generic TOML:**
|
|
75
|
+
|
|
76
|
+
```toml
|
|
77
|
+
[mcp_servers.ucu-mcp]
|
|
78
|
+
command = "npx"
|
|
79
|
+
args = ["-y", "ucu-mcp"]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Claude Code CLI / Desktop** — add via `claude mcp add`:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
claude mcp add ucu-mcp -- npx -y ucu-mcp
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Run `ucu-mcp doctor` once after first connect to verify macOS permissions
|
|
89
|
+
(System Settings → Privacy & Security → Accessibility **and** Screen Recording
|
|
90
|
+
must be granted to the launching terminal/client).
|
|
91
|
+
|
|
92
|
+
## References
|
|
93
|
+
|
|
94
|
+
- [tool-reference.md](references/tool-reference.md) — all 26 tools, parameters,
|
|
95
|
+
return shapes, and when to use each.
|
|
96
|
+
- [workflows.md](references/workflows.md) — playbooks for common tasks: form
|
|
97
|
+
filling, tray apps, opaque Electron UIs, vision-degraded environments, stale
|
|
98
|
+
targets.
|
|
99
|
+
- [troubleshooting.md](references/troubleshooting.md) — error code table with
|
|
100
|
+
recovery steps, permission issues, AX-opacity workarounds, OCR failures.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Tool Reference
|
|
2
|
+
|
|
3
|
+
UCU-MCP exposes 26 tools across five categories. All action tools accept
|
|
4
|
+
optional `captureAfter` / `captureMaxWidth` / `captureFormat` parameters that
|
|
5
|
+
screenshot the result and append it to the response.
|
|
6
|
+
|
|
7
|
+
Coordinate inputs are **screen-absolute** unless noted (window-relative only
|
|
8
|
+
when a `windowId` is explicitly passed).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Screen & Window
|
|
13
|
+
|
|
14
|
+
### `screenshot`
|
|
15
|
+
Capture the full screen, a region, or a specific window.
|
|
16
|
+
|
|
17
|
+
| Param | Type | Default | Notes |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| `display` | number | 0 | Display index |
|
|
20
|
+
| `windowId` | string | — | From `list_windows`; captures that window |
|
|
21
|
+
| `region` | `{x,y,width,height}` | — | Mutually exclusive with `windowId` |
|
|
22
|
+
| `format` | `"png"` \| `"jpeg"` | `"png"` | |
|
|
23
|
+
| `maxWidth` | number | 1280 | Resize preserving aspect ratio |
|
|
24
|
+
| `describe` | boolean | false | Append a text `ScreenDescription` block (OCR + AX) after the image |
|
|
25
|
+
| `describeOptions` | object | — | `{axDepth=3, ocrBlocks=50, includeAx=true}` when `describe=true` |
|
|
26
|
+
|
|
27
|
+
Returns one image content block (+ one text block if `describe=true`). Use
|
|
28
|
+
`describe=true` when image content may not reach the model (relay/URL downgrade).
|
|
29
|
+
|
|
30
|
+
### `list_windows`
|
|
31
|
+
List visible windows. Returns `WindowInfo[]` (`{id, title, processName, pid,
|
|
32
|
+
bounds, isMinimized, isOnScreen}`). When empty, includes a `diagnostics` hint
|
|
33
|
+
distinguishing permission-denied vs Electron-opacity.
|
|
34
|
+
|
|
35
|
+
| Param | Type | Default |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `includeMinimized` | boolean | false |
|
|
38
|
+
|
|
39
|
+
### `get_window_state`
|
|
40
|
+
AX tree of a window. Returns `WindowState` = `{window, focusedElement?, tree?}`.
|
|
41
|
+
The `tree` is a depth-limited `ElementInfo` (`{role, name, value, states,
|
|
42
|
+
bounds?, children?}`).
|
|
43
|
+
|
|
44
|
+
| Param | Type | Default |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `windowId` | string | active target |
|
|
47
|
+
| `depth` | number | 3 (capped at 10) |
|
|
48
|
+
| `includeBounds` | boolean | false |
|
|
49
|
+
|
|
50
|
+
### `get_screen_size`
|
|
51
|
+
Returns `{width, height, scaleFactor, estimated?}`. Synchronous, low-cost.
|
|
52
|
+
|
|
53
|
+
| Param | Type | Default |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `display` | number | 0 |
|
|
56
|
+
|
|
57
|
+
### `ocr`
|
|
58
|
+
Run Vision OCR on the full screen or a region. Returns `{elements:
|
|
59
|
+
OcrElement[], fullText}`. Each `OcrElement` = `{text, x, y, width, height,
|
|
60
|
+
confidence}` in screen-absolute coordinates.
|
|
61
|
+
|
|
62
|
+
| Param | Type | Default |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `display` | number | 0 |
|
|
65
|
+
| `region` | `{x,y,width,height}` | full screen |
|
|
66
|
+
|
|
67
|
+
### `describe_screen`
|
|
68
|
+
Structured text description of the screen — the **vision-degraded fallback**.
|
|
69
|
+
Returns `ScreenDescription` = `{capturedAt, screen, foregroundWindow,
|
|
70
|
+
ocr:{blocks, fullText, status}, ax:{elements?, status, windowId?}, errors[]}`.
|
|
71
|
+
Each source is collected independently; failures land in `errors` (never thrown).
|
|
72
|
+
Password fields are masked to `[REDACTED]`.
|
|
73
|
+
|
|
74
|
+
| Param | Type | Default | Notes |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| `display` | number | 0 | |
|
|
77
|
+
| `ocr` | boolean | true | Requires Screen Recording when true |
|
|
78
|
+
| `includeAx` | boolean | true | Requires Accessibility when true |
|
|
79
|
+
| `axDepth` | number | 3 | Capped at 10 |
|
|
80
|
+
| `ocrBlocks` | number | 50 | Max OCR elements returned |
|
|
81
|
+
| `windowId` | string | active target | AX traversal target |
|
|
82
|
+
|
|
83
|
+
Use when: image content blocks are not visible to you; you need
|
|
84
|
+
machine-readable layout; you want OCR + AX in one call with graceful failure.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Mouse & Input
|
|
89
|
+
|
|
90
|
+
All accept `captureAfter` / `captureMaxWidth` / `captureFormat`.
|
|
91
|
+
|
|
92
|
+
### `click` / `double_click`
|
|
93
|
+
Click at screen coordinates. `button` ∈ `left|right|middle`.
|
|
94
|
+
|
|
95
|
+
| Param | Type |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `x`, `y` | number |
|
|
98
|
+
| `button` | `"left"` \| `"right"` \| `"middle"` |
|
|
99
|
+
| `windowId` | string (optional, makes x/y window-relative) |
|
|
100
|
+
|
|
101
|
+
### `scroll`
|
|
102
|
+
| Param | Type | Notes |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `x`, `y` | number | position |
|
|
105
|
+
| `deltaX`, `deltaY` | number | negative deltaY = scroll up |
|
|
106
|
+
|
|
107
|
+
### `drag`
|
|
108
|
+
| Param | Type |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `startX`, `startY`, `endX`, `endY` | number |
|
|
111
|
+
| `button` | `"left"` \| `"right"` \| `"middle"` |
|
|
112
|
+
| `duration` | number (ms) |
|
|
113
|
+
|
|
114
|
+
### `move`
|
|
115
|
+
Move cursor without clicking. Params: `x`, `y`.
|
|
116
|
+
|
|
117
|
+
### `get_cursor_position`
|
|
118
|
+
Returns `{x, y}`.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Keyboard
|
|
123
|
+
|
|
124
|
+
### `type_text`
|
|
125
|
+
Type a string at the current cursor position (CGEvent background injection).
|
|
126
|
+
|
|
127
|
+
| Param | Type |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `text` | string |
|
|
130
|
+
| `delay` | number (ms, optional) |
|
|
131
|
+
|
|
132
|
+
### `press_key`
|
|
133
|
+
Press a key combo. Supports special keys, single letters a–z, single digits 0–9.
|
|
134
|
+
|
|
135
|
+
| Param | Type | Notes |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `key` | string | e.g. `"enter"`, `"m"`, `"5"` |
|
|
138
|
+
| `keys` | string[] | alternative to `key` for multi-tap |
|
|
139
|
+
| `modifiers` | string[] | `cmd`, `shift`, `alt`/`option`, `ctrl`/`control`, `capslock` |
|
|
140
|
+
|
|
141
|
+
Blocked combos: `cmd+q`, `cmd+shift+q`, `cmd+option+q`, `cmd+l`, `alt+f4`,
|
|
142
|
+
`ctrl+alt+del` (logout/lock).
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## AX Element Interaction
|
|
147
|
+
|
|
148
|
+
These operate on the active target's window (set via `focus_app`). Prefer these
|
|
149
|
+
over coordinate clicks.
|
|
150
|
+
|
|
151
|
+
### `find_element`
|
|
152
|
+
Find AX elements by text/role/value. Returns `{results: FindElementResult[],
|
|
153
|
+
metrics}`. Each result has an `id` for use in the element tools below. When 0
|
|
154
|
+
results, includes a hint guiding to `screenshot`+`ocr`+`click(x,y)` (Electron
|
|
155
|
+
opacity).
|
|
156
|
+
|
|
157
|
+
| Param | Type | Default | Notes |
|
|
158
|
+
|---|---|---|---|
|
|
159
|
+
| `text` | string | — | Match element name/description |
|
|
160
|
+
| `role` | string | — | e.g. `AXButton`, `AXTextField` |
|
|
161
|
+
| `value` | string | — | Match current value |
|
|
162
|
+
| `textMode` | `"contains"` \| `"exact"` \| `"regex"` | `"contains"` | |
|
|
163
|
+
| `app` | string | active target | |
|
|
164
|
+
| `depth` | number | 5 | |
|
|
165
|
+
| `index` | number | — | Return only the Nth match (0-based) |
|
|
166
|
+
| `near` | `{x,y}` | — | Sort by ascending distance, closest first |
|
|
167
|
+
| `visibleOnly` | boolean | false | |
|
|
168
|
+
| `includeBounds` | boolean | false | |
|
|
169
|
+
|
|
170
|
+
### `click_element`
|
|
171
|
+
Click by element `id`. AXPress first; on AX failure falls back to coordinate
|
|
172
|
+
click at the element's bounds center (handles Tauri/Electron silent swallows).
|
|
173
|
+
|
|
174
|
+
| Param | Type |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `elementId` | string |
|
|
177
|
+
| `app` | string (optional) |
|
|
178
|
+
|
|
179
|
+
### `set_value`
|
|
180
|
+
Set an AX element's value directly (no key synthesis). Best for text fields,
|
|
181
|
+
checkboxes, sliders.
|
|
182
|
+
|
|
183
|
+
| Param | Type |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `elementId` | string |
|
|
186
|
+
| `value` | string |
|
|
187
|
+
| `app` | string (optional) |
|
|
188
|
+
|
|
189
|
+
### `type_in_element`
|
|
190
|
+
Focus an element and type into it. Refetches an equivalent AX node if the
|
|
191
|
+
original `elementId` is stale (UI tree changed).
|
|
192
|
+
|
|
193
|
+
| Param | Type | Default |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `elementId` | string | |
|
|
196
|
+
| `text` | string | |
|
|
197
|
+
| `clearFirst` | boolean | false |
|
|
198
|
+
| `app` | string | active target |
|
|
199
|
+
|
|
200
|
+
### `click_menu_bar_extra`
|
|
201
|
+
Click a menu-bar status item (tray icon) — for menu-bar-only apps (e.g.
|
|
202
|
+
cc-switch) that `focus_app` cannot target. After clicking, the menu opens; use
|
|
203
|
+
`find_element` to locate menu items, or `screenshot` + `ocr` if the menu's AX
|
|
204
|
+
tree is opaque.
|
|
205
|
+
|
|
206
|
+
| Param | Type | Notes |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| `app` | string | Target app name |
|
|
209
|
+
| `description` | string | Match by description/name substring |
|
|
210
|
+
| `name` | string | Match by name/description substring |
|
|
211
|
+
| `index` | number | 0-based among matched items |
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Runtime & Synchronization
|
|
216
|
+
|
|
217
|
+
### `list_apps`
|
|
218
|
+
Returns `AppInfo[]` = `{name, pid, isFrontmost, windowCount}`. Background-only
|
|
219
|
+
processes are filtered.
|
|
220
|
+
|
|
221
|
+
### `focus_app`
|
|
222
|
+
Set the active target context. Establishes a window for AX tools; falls back to
|
|
223
|
+
a tray target (`windowId: "tray"`) for menu-bar-only apps if
|
|
224
|
+
`click_menu_bar_extra` status items are found.
|
|
225
|
+
|
|
226
|
+
| Param | Type |
|
|
227
|
+
|---|---|
|
|
228
|
+
| `app` | string |
|
|
229
|
+
|
|
230
|
+
### `wait`
|
|
231
|
+
Pause execution.
|
|
232
|
+
|
|
233
|
+
| Param | Type |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `ms` | number (1–60000) |
|
|
236
|
+
|
|
237
|
+
### `wait_for_element`
|
|
238
|
+
Poll until an AX element matches. Returns the match or times out.
|
|
239
|
+
|
|
240
|
+
| Param | Type | Default |
|
|
241
|
+
|---|---|---|
|
|
242
|
+
| `text` / `role` / `value` | string | — |
|
|
243
|
+
| `app` | string | active target |
|
|
244
|
+
| `until` | `"appear"` \| `"disappear"` \| `"value_change"` | `"appear"` |
|
|
245
|
+
| `timeout` / `timeoutMs` | number (ms) | 5000 |
|
|
246
|
+
| `interval` / `intervalMs` | number (ms) | 500 |
|
|
247
|
+
|
|
248
|
+
### `doctor`
|
|
249
|
+
Verify permissions, native helpers, and client readiness. Returns a JSON report
|
|
250
|
+
with `platform`, `safety`, `nativeHelpers`, `clients`, and `recommendations`.
|
|
251
|
+
Run this first when something is misbehaving.
|
|
252
|
+
|
|
253
|
+
### `clipboard_read` / `clipboard_write`
|
|
254
|
+
Read/write the system clipboard. `clipboard_write` text-injection patterns
|
|
255
|
+
(e.g. shell-escape sequences) are blocked by the safety guard.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## First Checks
|
|
4
|
+
|
|
5
|
+
1. Run `doctor` — verifies Accessibility + Screen Recording permissions and
|
|
6
|
+
native helpers. Most failures are permission issues.
|
|
7
|
+
2. Confirm the target app is running and not minimized to the point of having no
|
|
8
|
+
on-screen window: `list_apps` + `list_windows`.
|
|
9
|
+
3. Check `errors[]` in `describe_screen` responses — it names which source
|
|
10
|
+
(ocr/ax/foreground/screen) failed and why.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Error Code Table
|
|
15
|
+
|
|
16
|
+
Every error response carries a `code` and a `hint`. The table below maps codes
|
|
17
|
+
to recovery steps (mirrors the runtime `recoveryHint`).
|
|
18
|
+
|
|
19
|
+
| Code | Meaning | Recovery |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `WINDOW_NOT_FOUND` | The target window does not exist or is not on screen. | `list_windows` again, retry with a fresh `windowId`, or omit `windowId` for screen coordinates. |
|
|
22
|
+
| `TARGET_STALE` | The active target window changed pid or closed. | `focus_app` for the target app again, then retry. `type_in_element` auto-refetches equivalent nodes. |
|
|
23
|
+
| `ELEMENT_NOT_FOUND` | No AX element matched the selector. | `find_element` again with broader selectors (different `text`, `textMode:"contains"`, drop `role`). If still empty, the app may be Electron-opaque — see below. |
|
|
24
|
+
| `PERMISSION_DENIED` | Accessibility or Screen Recording not granted. | Run `doctor`, then grant the missing permission in System Settings → Privacy & Security, and **restart the launching client** (changes do not apply to already-running processes). |
|
|
25
|
+
| `SAFETY_BLOCKED` | Action rejected by the safety guard (dangerous shortcut, sensitive window, suspicious text). | Choose a less risky action, or ask the user to perform it manually. Blocked shortcuts include `cmd+q`, `cmd+shift+q`, `cmd+l`, `alt+f4`. |
|
|
26
|
+
| `INPUT_FAILED` | Input synthesis (click/type/keypress) failed at the CGEvent layer. | Observe current state with `screenshot` or `get_window_state`, then retry only if safe. |
|
|
27
|
+
| `CAPTURE_FAILED` | Screenshot/OCR failed (usually Screen Recording permission). | `doctor` → grant Screen Recording → restart client. |
|
|
28
|
+
| `COORDINATE_OUT_OF_BOUNDS` | Click/drag coordinates are outside the active display/window. | `get_screen_size` or `list_windows`, retry with coordinates inside bounds. |
|
|
29
|
+
| `UNSUPPORTED_PARAMETER` | A parameter combination is invalid (e.g. `screenshot` with both `windowId` and `region`). | Remove or replace the unsupported parameter; inspect `tools/list` for the schema. |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Permission Issues
|
|
34
|
+
|
|
35
|
+
macOS requires two permissions for full functionality:
|
|
36
|
+
|
|
37
|
+
- **Accessibility** — needed for all AX tools (`find_element`,
|
|
38
|
+
`click_element`, `get_window_state`, `list_windows`, `click_menu_bar_extra`).
|
|
39
|
+
- **Screen Recording** — needed for `screenshot`, `ocr`, and `describe_screen`
|
|
40
|
+
(with `ocr: true`).
|
|
41
|
+
|
|
42
|
+
Grant via **System Settings → Privacy & Security → Accessibility / Screen
|
|
43
|
+
Recording**, enabling the entry for the launching terminal/client app (Terminal,
|
|
44
|
+
iTerm, Claude Code, Codex, etc.).
|
|
45
|
+
|
|
46
|
+
**Critical:** permission changes do not apply to already-running processes.
|
|
47
|
+
After granting, **quit and restart the client** that launches `ucu-mcp`.
|
|
48
|
+
|
|
49
|
+
Run `ucu-mcp doctor` (or the `doctor` tool) to verify — it reports per-permission
|
|
50
|
+
status and which process to authorize.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Electron / Tauri / WebView AX Opacity
|
|
55
|
+
|
|
56
|
+
**Symptom:** `find_element` returns 0 results, `get_window_state` returns a near-
|
|
57
|
+
empty tree (just an `AXGroup`), `list_windows` shows the window but AX tools
|
|
58
|
+
can't see into it.
|
|
59
|
+
|
|
60
|
+
**Cause:** Electron/Tauri/WebView apps render their UI in a composited layer
|
|
61
|
+
that macOS AX cannot introspect. The AX tree exposes only the window frame and
|
|
62
|
+
traffic-light buttons.
|
|
63
|
+
|
|
64
|
+
**Workaround — pixel path:**
|
|
65
|
+
```
|
|
66
|
+
screenshot({})
|
|
67
|
+
ocr({})
|
|
68
|
+
→ blocks[].text locates the target UI text with bounding box {x,y,width,height}
|
|
69
|
+
click({ x: block.x + block.width/2, y: block.y + block.height/2 })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`find_element` and `list_windows` emit a `hint` describing this fallback when
|
|
73
|
+
they detect the pattern. For one-shot planning, `describe_screen` gives OCR + AX
|
|
74
|
+
together.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Menu-Bar / Tray App Not Reachable
|
|
79
|
+
|
|
80
|
+
**Symptom:** `focus_app("tray-app")` throws `WINDOW_NOT_FOUND`; the app has no
|
|
81
|
+
window in `list_windows`.
|
|
82
|
+
|
|
83
|
+
**Cause:** Pure menu-bar (LSUIElement) apps have no window; their status item is
|
|
84
|
+
hosted by the `SystemUIServer` system process.
|
|
85
|
+
|
|
86
|
+
**Workaround:**
|
|
87
|
+
```
|
|
88
|
+
click_menu_bar_extra({ app: "tray-app", name: "TrayApp" }) # opens tray menu
|
|
89
|
+
find_element({ text: "Settings", app: "tray-app" }) # menu items are AX-visible
|
|
90
|
+
click_element({ elementId })
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`focus_app` automatically falls back to a tray target when `click_menu_bar_extra`
|
|
94
|
+
finds a matching status item, so subsequent AX tools work against the menu.
|
|
95
|
+
|
|
96
|
+
If the tray menu itself is Electron-opaque:
|
|
97
|
+
```
|
|
98
|
+
click_menu_bar_extra({ app: "tray-app" })
|
|
99
|
+
screenshot({})
|
|
100
|
+
ocr({}) → locate menu item by text → click(x, y)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## OCR Failures
|
|
106
|
+
|
|
107
|
+
**Symptom:** `ocr` or `describe_screen` reports OCR failure (`ocr.status:
|
|
108
|
+
"failed"`), or native OCR helper not found in `doctor`.
|
|
109
|
+
|
|
110
|
+
**Checks:**
|
|
111
|
+
1. Screen Recording permission granted and client restarted.
|
|
112
|
+
2. Native helper present — `doctor` reports `ocr` helper status. If missing, the
|
|
113
|
+
npm package may be corrupted; reinstall.
|
|
114
|
+
3. Screen is not locked (OCR captures a black frame when locked).
|
|
115
|
+
|
|
116
|
+
`describe_screen` degrades gracefully — OCR failure still returns AX state, so
|
|
117
|
+
you can fall back to `get_window_state` / `find_element`.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## describe_screen Returns Empty / All-skipped
|
|
122
|
+
|
|
123
|
+
**Symptom:** `describe_screen` returns `errors: []` but `ocr.status:
|
|
124
|
+
"skipped"` and `ax.status: "skipped"`.
|
|
125
|
+
|
|
126
|
+
**Cause:** you passed `ocr: false, includeAx: false`, or the params defaulted to
|
|
127
|
+
that (note: in live MCP use the SDK applies `ocr: true, includeAx: true`
|
|
128
|
+
defaults; if you see both skipped, the client stripped defaults).
|
|
129
|
+
|
|
130
|
+
**Fix:** explicitly pass `ocr: true, includeAx: true`.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Actions Blocked While macOS Is Locked
|
|
135
|
+
|
|
136
|
+
**Symptom:** input actions (`click`, `type_text`, `press_key`, …) fail; observe
|
|
137
|
+
actions (`screenshot`, `ocr`) return black/empty frames.
|
|
138
|
+
|
|
139
|
+
**Cause:** the safety guard refuses to synthesize input while the screen is
|
|
140
|
+
locked (the user is not present to supervise).
|
|
141
|
+
|
|
142
|
+
**Fix:** wait for the user to unlock, or ask them to unlock. There is no bypass.
|