ucu-mcp 0.4.0 → 0.4.2

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +50 -4
  2. package/dist/bin/ucu-mcp.js +1 -1
  3. package/dist/src/index.d.ts +2 -2
  4. package/dist/src/index.js +2 -2
  5. package/dist/src/mcp/server.js +1 -1
  6. package/dist/src/mcp/tools/app-tools.d.ts +2 -0
  7. package/dist/src/mcp/tools/app-tools.js +220 -0
  8. package/dist/src/mcp/tools/element-tools.d.ts +23 -0
  9. package/dist/src/mcp/tools/element-tools.js +59 -0
  10. package/dist/src/mcp/tools/helpers.d.ts +82 -0
  11. package/dist/src/mcp/tools/helpers.js +243 -0
  12. package/dist/src/mcp/tools/index.d.ts +19 -0
  13. package/dist/src/mcp/tools/index.js +54 -0
  14. package/dist/src/mcp/tools/input-tools.d.ts +2 -0
  15. package/dist/src/mcp/tools/input-tools.js +66 -0
  16. package/dist/src/mcp/tools/keyboard-tools.d.ts +2 -0
  17. package/dist/src/mcp/tools/keyboard-tools.js +35 -0
  18. package/dist/src/mcp/tools/screen-tools.d.ts +2 -0
  19. package/dist/src/mcp/tools/screen-tools.js +69 -0
  20. package/dist/src/mcp/tools.d.ts +9 -0
  21. package/dist/src/mcp/tools.js +87 -23
  22. package/dist/src/platform/base.d.ts +3 -0
  23. package/dist/src/platform/jxa-helpers.d.ts +11 -0
  24. package/dist/src/platform/jxa-helpers.js +206 -0
  25. package/dist/src/platform/macos/ax-tree.d.ts +4 -0
  26. package/dist/src/platform/macos/ax-tree.js +462 -0
  27. package/dist/src/platform/macos/base.d.ts +57 -0
  28. package/dist/src/platform/macos/base.js +92 -0
  29. package/dist/src/platform/macos/clipboard.d.ts +3 -0
  30. package/dist/src/platform/macos/clipboard.js +20 -0
  31. package/dist/src/platform/macos/element.d.ts +4 -0
  32. package/dist/src/platform/macos/element.js +212 -0
  33. package/dist/src/platform/macos/focus.d.ts +3 -0
  34. package/dist/src/platform/macos/focus.js +33 -0
  35. package/dist/src/platform/macos/helpers.d.ts +35 -0
  36. package/dist/src/platform/macos/helpers.js +54 -0
  37. package/dist/src/platform/macos/index.d.ts +2 -0
  38. package/dist/src/platform/macos/index.js +1 -0
  39. package/dist/src/platform/macos/input.d.ts +9 -0
  40. package/dist/src/platform/macos/input.js +62 -0
  41. package/dist/src/platform/macos/screen.d.ts +7 -0
  42. package/dist/src/platform/macos/screen.js +197 -0
  43. package/dist/src/platform/macos/window.d.ts +6 -0
  44. package/dist/src/platform/macos/window.js +251 -0
  45. package/dist/src/platform/macos.js +71 -563
  46. package/dist/src/util/errors.d.ts +7 -2
  47. package/dist/src/util/errors.js +7 -3
  48. package/native/cgevent/cgevent-helper +0 -0
  49. package/native/ocr/ocr-helper +0 -0
  50. package/native/windowlist/windowlist-helper +0 -0
  51. package/package.json +1 -1
@@ -0,0 +1,251 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { existsSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { WindowNotFoundError } from "../../util/errors.js";
7
+ import { rethrowAccessibilityError, selectWindowForApp } from "./helpers.js";
8
+ const __windowDirname = dirname(fileURLToPath(import.meta.url));
9
+ export async function listApps() {
10
+ const jxaScript = `
11
+ var se = Application('System Events');
12
+ var result = [];
13
+ var procs = se.processes();
14
+ for (var i = 0; i < procs.length; i++) {
15
+ try {
16
+ var p = procs[i];
17
+ var background = false;
18
+ try { background = p.backgroundOnly(); } catch(e) {}
19
+ if (background) continue;
20
+ var wins = [];
21
+ try { wins = p.windows(); } catch(e) {}
22
+ result.push({
23
+ name: p.name() || '',
24
+ pid: p.unixId ? p.unixId() : 0,
25
+ isFrontmost: p.frontmost ? !!p.frontmost() : false,
26
+ windowCount: wins.length || 0
27
+ });
28
+ } catch(e) {}
29
+ }
30
+ JSON.stringify(result);
31
+ `;
32
+ try {
33
+ const out = execFileSync("osascript", [
34
+ "-l", "JavaScript",
35
+ "-e", jxaScript,
36
+ ], { encoding: "utf-8", timeout: 10000 }).trim();
37
+ return JSON.parse(out);
38
+ }
39
+ catch (error) {
40
+ rethrowAccessibilityError(error, "list_apps");
41
+ }
42
+ }
43
+ export async function focusApp(app) {
44
+ const appLiteral = JSON.stringify(app);
45
+ this.windowCache = undefined;
46
+ let target;
47
+ const deadline = Date.now() + 3000;
48
+ do {
49
+ const windows = await this.listWindows(true);
50
+ target = selectWindowForApp(windows, app);
51
+ if (target)
52
+ break;
53
+ await new Promise((resolve) => setTimeout(resolve, 150));
54
+ } while (Date.now() < deadline);
55
+ if (!target) {
56
+ this.activeTarget = undefined;
57
+ const err = new WindowNotFoundError(app, { hint: "list_windows returned no match for this app. If the app is running, " +
58
+ "the most likely cause is that it is an Electron app whose AX tree is " +
59
+ "not exposed to System Events (System Settings > Privacy & Security > " +
60
+ "Accessibility must be granted to the Electron process itself, not just " +
61
+ "to the host terminal). Pixel-level workaround: call screenshot to " +
62
+ "capture the screen, then ocr to locate UI text and get its bounding " +
63
+ "box coordinates, then click(x, y) at those screen coordinates. " +
64
+ "Alternatively, modify the app's config file or database directly." });
65
+ throw err;
66
+ }
67
+ this.activeTarget = {
68
+ targetId: randomUUID(),
69
+ appName: target.processName,
70
+ pid: target.pid,
71
+ windowId: target.id,
72
+ title: target.title,
73
+ capturedAt: new Date().toISOString(),
74
+ };
75
+ return this.activeTarget;
76
+ }
77
+ export async function getActiveBrowserContext(app) {
78
+ const appName = app || this.activeTarget?.appName;
79
+ if (!appName)
80
+ return undefined;
81
+ const normalized = appName.toLowerCase();
82
+ const knownBrowser = [
83
+ "safari",
84
+ "google chrome",
85
+ "chrome",
86
+ "arc",
87
+ "microsoft edge",
88
+ "edge",
89
+ "brave browser",
90
+ "brave",
91
+ ].some((name) => normalized.includes(name));
92
+ if (!knownBrowser)
93
+ return undefined;
94
+ const appLiteral = JSON.stringify(appName);
95
+ const jxaScript = `
96
+ function run() {
97
+ var appName = ${appLiteral};
98
+ try {
99
+ var app = Application(appName);
100
+ var url = "";
101
+ var title = "";
102
+ if (appName.toLowerCase().indexOf("safari") !== -1) {
103
+ try { url = app.documents[0].url(); } catch(e) {}
104
+ try { title = app.documents[0].name(); } catch(e) {}
105
+ } else {
106
+ try { url = app.windows[0].activeTab.url(); } catch(e) {}
107
+ try { title = app.windows[0].activeTab.title(); } catch(e) {}
108
+ }
109
+ return JSON.stringify({appName: appName, url: url || undefined, title: title || undefined});
110
+ } catch(e) {
111
+ return JSON.stringify({appName: appName});
112
+ }
113
+ }
114
+ run();
115
+ `;
116
+ try {
117
+ const out = execFileSync("osascript", [
118
+ "-l", "JavaScript",
119
+ "-e", jxaScript,
120
+ ], { encoding: "utf-8", timeout: 5000 }).trim();
121
+ const parsed = JSON.parse(out);
122
+ return parsed.url || parsed.title ? parsed : undefined;
123
+ }
124
+ catch {
125
+ return undefined;
126
+ }
127
+ }
128
+ export async function listWindows(_includeMinimized) {
129
+ const now = Date.now();
130
+ if (this.windowCache && now - this.windowCache.cachedAt <= this.windowCacheTtlMs) {
131
+ return this.windowCache.windows.map((window) => ({
132
+ ...window,
133
+ bounds: { ...window.bounds },
134
+ }));
135
+ }
136
+ if (this.windowCacheInFlight) {
137
+ return this.windowCache?.windows.map((w) => ({ ...w, bounds: { ...w.bounds } })) ?? [];
138
+ }
139
+ this.windowCacheInFlight = true;
140
+ try {
141
+ let windows;
142
+ const nativeResult = listWindowsNative.call(this);
143
+ if (nativeResult !== null) {
144
+ windows = nativeResult;
145
+ }
146
+ else {
147
+ windows = await listWindowsJxa.call(this);
148
+ }
149
+ this.windowCache = {
150
+ cachedAt: Date.now(),
151
+ windows: windows.map((window) => ({
152
+ ...window,
153
+ bounds: { ...window.bounds },
154
+ })),
155
+ };
156
+ return windows;
157
+ }
158
+ catch {
159
+ return [];
160
+ }
161
+ finally {
162
+ this.windowCacheInFlight = false;
163
+ }
164
+ }
165
+ function listWindowsNative() {
166
+ try {
167
+ const helperPath = resolveNativeHelper.call(this, "windowlist", "windowlist-helper");
168
+ if (!helperPath)
169
+ return null;
170
+ const out = execFileSync(helperPath, [], {
171
+ encoding: "utf-8",
172
+ timeout: 5000,
173
+ });
174
+ const parsed = JSON.parse(out.trim());
175
+ if (parsed.error)
176
+ return null;
177
+ return parsed.windows.map(w => ({
178
+ id: w.id,
179
+ title: w.title,
180
+ processName: w.processName,
181
+ pid: w.pid,
182
+ bounds: w.bounds,
183
+ isMinimized: !w.isOnScreen,
184
+ isOnScreen: w.isOnScreen,
185
+ }));
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ function resolveNativeHelper(folder, binary) {
192
+ if (this._nativeHelperPaths && folder in this._nativeHelperPaths) {
193
+ const override = this._nativeHelperPaths[folder];
194
+ return override === null ? null : override;
195
+ }
196
+ const candidates = [
197
+ join(__windowDirname, "..", "..", "..", "native", folder, binary),
198
+ join(__windowDirname, "..", "..", "native", folder, binary),
199
+ ];
200
+ for (const p of candidates) {
201
+ if (existsSync(p))
202
+ return p;
203
+ }
204
+ return null;
205
+ }
206
+ async function listWindowsJxa() {
207
+ const jxaScript = `
208
+ var se = Application('System Events');
209
+ var result = [];
210
+ var procs = se.processes();
211
+ for (var i = 0; i < procs.length; i++) {
212
+ var p = procs[i];
213
+ var pName = '';
214
+ var pPid = 0;
215
+ try { pName = p.name(); } catch(e) {}
216
+ try { pPid = p.unixId(); } catch(e) {}
217
+ try {
218
+ var wins = p.windows();
219
+ for (var j = 0; j < wins.length; j++) {
220
+ var w = wins[j];
221
+ var pos, sz;
222
+ try { pos = w.position(); } catch(e) { pos = [0, 0]; }
223
+ try { sz = w.size(); } catch(e) { sz = [0, 0]; }
224
+ if (sz[0] === 0 && sz[1] === 0) continue;
225
+ var title = '';
226
+ try { title = w.name() || ''; } catch(e) {}
227
+ result.push({
228
+ id: pName + '/win' + j,
229
+ title: title,
230
+ processName: pName,
231
+ pid: pPid,
232
+ bounds: { x: pos[0], y: pos[1], width: sz[0], height: sz[1] },
233
+ isMinimized: false,
234
+ isOnScreen: true
235
+ });
236
+ }
237
+ } catch(e) {}
238
+ }
239
+ JSON.stringify(result);
240
+ `;
241
+ try {
242
+ const jxaOut = execFileSync("osascript", [
243
+ "-l", "JavaScript",
244
+ "-e", jxaScript
245
+ ], { encoding: "utf-8", timeout: 15000 });
246
+ return JSON.parse(jxaOut.trim());
247
+ }
248
+ catch (error) {
249
+ rethrowAccessibilityError(error, "list_windows_jxa");
250
+ }
251
+ }