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.
@@ -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
- /** Normalize a shortcut string to lowercase, trimmed, sorted modifiers. */
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) => s.trim())
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) {
@@ -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
- // Lowercase letters
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
- // Digits
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 keyCode = MAC_KEY_CODES[key.toLowerCase()];
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.4.3",
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,4 @@
1
+ interface:
2
+ display_name: "UCU-MCP"
3
+ short_description: "Guide agents using UCU-MCP macOS computer-use"
4
+ default_prompt: "Use $ucu-mcp to automate macOS desktop apps through UCU-MCP."
@@ -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.