@ebowwa/osascript 1.1.0

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.
@@ -0,0 +1,866 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/index.ts
5
+ import { spawn } from "child_process";
6
+ async function executeOsascript(script, options = {}) {
7
+ const { language = "applescript", timeout = 30000 } = options;
8
+ const args = [];
9
+ if (language === "javascript") {
10
+ args.push("-l", "JavaScript");
11
+ }
12
+ args.push("-e", script);
13
+ return new Promise((resolve) => {
14
+ const proc = spawn("osascript", args, { timeout });
15
+ let stdout = "";
16
+ let stderr = "";
17
+ proc.stdout.on("data", (data) => {
18
+ stdout += data.toString();
19
+ });
20
+ proc.stderr.on("data", (data) => {
21
+ stderr += data.toString();
22
+ });
23
+ const timer = setTimeout(() => {
24
+ proc.kill();
25
+ resolve({
26
+ success: false,
27
+ stdout: stdout.trim(),
28
+ stderr: "Timeout exceeded",
29
+ exitCode: 1
30
+ });
31
+ }, timeout);
32
+ proc.on("close", (code) => {
33
+ clearTimeout(timer);
34
+ resolve({
35
+ success: code === 0,
36
+ stdout: stdout.trim(),
37
+ stderr: stderr.trim(),
38
+ exitCode: code ?? 1
39
+ });
40
+ });
41
+ proc.on("error", (error) => {
42
+ clearTimeout(timer);
43
+ resolve({
44
+ success: false,
45
+ stdout: "",
46
+ stderr: error.message,
47
+ exitCode: 1
48
+ });
49
+ });
50
+ });
51
+ }
52
+ async function executeAppleScript(script, timeout = 30000) {
53
+ return executeOsascript(script, { language: "applescript", timeout });
54
+ }
55
+ async function executeJXA(script, timeout = 30000) {
56
+ return executeOsascript(script, { language: "javascript", timeout });
57
+ }
58
+ async function displayNotification(options) {
59
+ const { title = "Notification", subtitle, message, soundName } = options;
60
+ let script = `display notification "${escapeString(message)}"`;
61
+ if (title)
62
+ script += ` with title "${escapeString(title)}"`;
63
+ if (subtitle)
64
+ script += ` subtitle "${escapeString(subtitle)}"`;
65
+ if (soundName)
66
+ script += ` sound name "${escapeString(soundName)}"`;
67
+ return executeAppleScript(script);
68
+ }
69
+ async function displayDialog(options) {
70
+ const { title, message, buttons = ["OK"], defaultButton = 1, icon, givingUpAfter } = options;
71
+ let script = `display dialog "${escapeString(message)}"`;
72
+ script += ` buttons ${JSON.stringify(buttons)}`;
73
+ script += ` default button ${defaultButton}`;
74
+ if (icon)
75
+ script += ` with icon ${icon}`;
76
+ if (givingUpAfter)
77
+ script += ` giving up after ${givingUpAfter}`;
78
+ const result = await executeAppleScript(script);
79
+ if (!result.success) {
80
+ throw new Error(`Dialog failed: ${result.stderr}`);
81
+ }
82
+ const buttonMatch = result.stdout.match(/button returned:(\w+)/);
83
+ const gaveUpMatch = result.stdout.match(/gave up:(\w+)/);
84
+ return {
85
+ buttonReturned: buttonMatch ? buttonMatch[1] : buttons[0],
86
+ gaveUp: gaveUpMatch ? gaveUpMatch[1] === "true" : false
87
+ };
88
+ }
89
+ async function getVolume() {
90
+ const script = `
91
+ output volume of (get volume settings) as integer
92
+ `;
93
+ const volumeResult = await executeAppleScript(script);
94
+ const mutedScript = `
95
+ output muted of (get volume settings) as boolean
96
+ `;
97
+ const mutedResult = await executeAppleScript(mutedScript);
98
+ return {
99
+ volume: parseInt(volumeResult.stdout) || 0,
100
+ muted: mutedResult.stdout.toLowerCase() === "true"
101
+ };
102
+ }
103
+ async function setVolume(volume, muted) {
104
+ const safeVolume = Math.max(0, Math.min(100, Math.round(volume)));
105
+ let script = `set volume output volume ${safeVolume}`;
106
+ if (muted !== undefined) {
107
+ script += muted ? " with output muted" : " without output muted";
108
+ }
109
+ return executeAppleScript(script);
110
+ }
111
+ async function getClipboard() {
112
+ const result = await executeAppleScript("the clipboard");
113
+ if (!result.success) {
114
+ throw new Error(`Failed to get clipboard: ${result.stderr}`);
115
+ }
116
+ return result.stdout;
117
+ }
118
+ async function setClipboard(text) {
119
+ const script = `set the clipboard to "${escapeString(text)}"`;
120
+ return executeAppleScript(script);
121
+ }
122
+ async function clearClipboard() {
123
+ return executeAppleScript('set the clipboard to ""');
124
+ }
125
+ async function listRunningApplications() {
126
+ const script = `
127
+ tell application "System Events"
128
+ set appList to name of every process whose background only is false
129
+ set output to ""
130
+ repeat with appName in appList
131
+ set output to output & appName & linefeed
132
+ end repeat
133
+ return output
134
+ end tell
135
+ `;
136
+ const result = await executeAppleScript(script);
137
+ if (!result.success) {
138
+ throw new Error(`Failed to list applications: ${result.stderr}`);
139
+ }
140
+ const apps = result.stdout.split(`
141
+ `).filter((name) => name.trim()).map((name) => ({
142
+ name: name.trim(),
143
+ running: true
144
+ }));
145
+ return apps;
146
+ }
147
+ async function getApplicationInfo(appName) {
148
+ const script = `
149
+ tell application "System Events"
150
+ if exists process "${escapeString(appName)}" then
151
+ set theProcess to process "${escapeString(appName)}"
152
+ set theName to name of theProcess
153
+ set thePath to POSIX path of (application file of theProcess as alias)
154
+ set thePID to unix id of theProcess
155
+ set windowCount to count of windows of theProcess
156
+ return theName & "|" & thePath & "|" & thePID & "|" & windowCount
157
+ else
158
+ return "NOT_RUNNING"
159
+ end if
160
+ end tell
161
+ `;
162
+ const result = await executeAppleScript(script);
163
+ if (!result.success || result.stdout === "NOT_RUNNING") {
164
+ return {
165
+ name: appName,
166
+ running: false
167
+ };
168
+ }
169
+ const [name, path, pid, windowCount] = result.stdout.split("|");
170
+ return {
171
+ name,
172
+ path,
173
+ processId: parseInt(pid),
174
+ running: true,
175
+ windowCount: parseInt(windowCount)
176
+ };
177
+ }
178
+ async function activateApplication(appName) {
179
+ const script = `
180
+ tell application "${escapeString(appName)}"
181
+ activate
182
+ end tell
183
+ `;
184
+ return executeAppleScript(script);
185
+ }
186
+ async function quitApplication(appName, force = false) {
187
+ let script;
188
+ if (force) {
189
+ script = `
190
+ tell application "System Events"
191
+ set theProcess to process "${escapeString(appName)}"
192
+ set thePID to unix id of theProcess
193
+ do shell script "kill -9 " & thePID
194
+ end tell
195
+ `;
196
+ } else {
197
+ script = `
198
+ tell application "${escapeString(appName)}"
199
+ quit
200
+ end tell
201
+ `;
202
+ }
203
+ return executeAppleScript(script);
204
+ }
205
+ async function launchApplication(appName) {
206
+ const script = `
207
+ tell application "${escapeString(appName)}"
208
+ launch
209
+ end tell
210
+ `;
211
+ return executeAppleScript(script);
212
+ }
213
+ async function getFrontmostApplication() {
214
+ const script = `
215
+ tell application "System Events"
216
+ name of first process whose frontmost is true
217
+ end tell
218
+ `;
219
+ const result = await executeAppleScript(script);
220
+ if (!result.success) {
221
+ throw new Error(`Failed to get frontmost app: ${result.stderr}`);
222
+ }
223
+ return result.stdout;
224
+ }
225
+ async function getApplicationWindows(appName) {
226
+ const script = `
227
+ tell application "System Events"
228
+ tell process "${escapeString(appName)}"
229
+ set windowList to {}
230
+ repeat with theWindow in windows
231
+ set windowName to name of theWindow
232
+ set windowId to id of theWindow
233
+ set windowIndex to index of theWindow
234
+ set end of windowList to windowName & "|" & windowId & "|" & windowIndex
235
+ end repeat
236
+ return windowList as text
237
+ end tell
238
+ end tell
239
+ `;
240
+ const result = await executeAppleScript(script);
241
+ if (!result.success) {
242
+ return [];
243
+ }
244
+ if (!result.stdout.trim()) {
245
+ return [];
246
+ }
247
+ const windows = result.stdout.split(", ").filter((w) => w.trim()).map((w) => {
248
+ const [name, id, index] = w.split("|");
249
+ return {
250
+ name: name || null,
251
+ id: parseInt(id) || 0,
252
+ index: parseInt(index) || 0
253
+ };
254
+ });
255
+ return windows;
256
+ }
257
+ async function setWindowBounds(appName, windowIndex, bounds) {
258
+ const script = `
259
+ tell application "System Events"
260
+ tell process "${escapeString(appName)}"
261
+ set position of window ${windowIndex} to {${bounds.x}, ${bounds.y}}
262
+ set size of window ${windowIndex} to {${bounds.width}, ${bounds.height}}
263
+ end tell
264
+ end tell
265
+ `;
266
+ return executeAppleScript(script);
267
+ }
268
+ async function sendKeystroke(options) {
269
+ const { key, modifiers = [], delay = 0 } = options;
270
+ const modifierStr = modifiers.map((m) => m + " down").join(", ");
271
+ const usingStr = modifierStr ? ` using {${modifierStr}}` : "";
272
+ const script = `
273
+ tell application "System Events"
274
+ keystroke "${escapeString(key)}"${usingStr}
275
+ ${delay > 0 ? `delay ${delay}` : ""}
276
+ end tell
277
+ `;
278
+ return executeAppleScript(script);
279
+ }
280
+ async function sendKeyCode(options) {
281
+ const { keyCode, modifiers = [] } = options;
282
+ const modifierStr = modifiers.map((m) => m + " down").join(", ");
283
+ const usingStr = modifierStr ? ` using {${modifierStr}}` : "";
284
+ const script = `
285
+ tell application "System Events"
286
+ key code ${keyCode}${usingStr}
287
+ end tell
288
+ `;
289
+ return executeAppleScript(script);
290
+ }
291
+ async function typeText(text, delayBetweenChars = 0.01) {
292
+ const script = `
293
+ tell application "System Events"
294
+ keystroke "${escapeString(text)}"
295
+ ${delayBetweenChars > 0 ? `delay ${delayBetweenChars}` : ""}
296
+ end tell
297
+ `;
298
+ return executeAppleScript(script);
299
+ }
300
+ async function getSafariURL() {
301
+ const script = `
302
+ tell application "Safari"
303
+ return URL of current tab of front window
304
+ end tell
305
+ `;
306
+ const result = await executeAppleScript(script);
307
+ if (!result.success) {
308
+ throw new Error(`Failed to get Safari URL: ${result.stderr}`);
309
+ }
310
+ return result.stdout;
311
+ }
312
+ async function setSafariURL(url) {
313
+ const script = `
314
+ tell application "Safari"
315
+ set URL of current tab of front window to "${escapeString(url)}"
316
+ end tell
317
+ `;
318
+ return executeAppleScript(script);
319
+ }
320
+ async function getSafariTabTitles() {
321
+ const script = `
322
+ tell application "Safari"
323
+ set tabTitles to name of every tab of front window
324
+ return tabTitles as text
325
+ end tell
326
+ `;
327
+ const result = await executeAppleScript(script);
328
+ if (!result.success) {
329
+ return [];
330
+ }
331
+ return result.stdout.split(", ").filter((t) => t.trim());
332
+ }
333
+ async function executeSafariJS(jsCode) {
334
+ const script = `
335
+ tell application "Safari"
336
+ do JavaScript "${escapeString(jsCode)}" in current tab of front window
337
+ end tell
338
+ `;
339
+ const result = await executeAppleScript(script);
340
+ if (!result.success) {
341
+ throw new Error(`Failed to execute Safari JS: ${result.stderr}`);
342
+ }
343
+ return result.stdout;
344
+ }
345
+ async function getFinderSelection() {
346
+ const script = `
347
+ tell application "Finder"
348
+ set selectedItems to selection
349
+ set output to ""
350
+ repeat with theItem in selectedItems
351
+ set itemName to name of theItem
352
+ set itemPath to POSIX path of (theItem as alias)
353
+ set itemKind to kind of theItem
354
+ set output to output & itemName & "|" & itemPath & "|" & itemKind & linefeed
355
+ end repeat
356
+ return output
357
+ end tell
358
+ `;
359
+ const result = await executeAppleScript(script);
360
+ if (!result.success || !result.stdout.trim()) {
361
+ return [];
362
+ }
363
+ return result.stdout.split(`
364
+ `).filter((line) => line.trim()).map((line) => {
365
+ const [name, path, kind] = line.split("|");
366
+ const type = kind?.toLowerCase().includes("folder") ? "folder" : kind?.toLowerCase().includes("application") ? "application" : "file";
367
+ return {
368
+ name,
369
+ path,
370
+ type,
371
+ exists: true
372
+ };
373
+ });
374
+ }
375
+ async function openFinderWindow(path) {
376
+ const script = `
377
+ tell application "Finder"
378
+ activate
379
+ open POSIX file "${escapeString(path)}"
380
+ end tell
381
+ `;
382
+ return executeAppleScript(script);
383
+ }
384
+ async function createFolder(parentPath, folderName) {
385
+ const script = `
386
+ tell application "Finder"
387
+ make new folder at POSIX file "${escapeString(parentPath)}" with properties {name:"${escapeString(folderName)}"}
388
+ end tell
389
+ `;
390
+ return executeAppleScript(script);
391
+ }
392
+ async function moveToTrash(path) {
393
+ const script = `
394
+ tell application "Finder"
395
+ delete POSIX file "${escapeString(path)}"
396
+ end tell
397
+ `;
398
+ return executeAppleScript(script);
399
+ }
400
+ async function getDisplayInfo() {
401
+ const script = `
402
+ ObjC.import('Cocoa');
403
+ var screens = $.NSScreen.screens;
404
+ var count = screens.count;
405
+ var result = [];
406
+ for (var i = 0; i < count; i++) {
407
+ var screen = screens.objectAtIndex(i);
408
+ var frame = screen.frame;
409
+ result.push({
410
+ id: i,
411
+ width: frame.size.width,
412
+ height: frame.size.height,
413
+ main: i === 0
414
+ });
415
+ }
416
+ JSON.stringify(result);
417
+ `;
418
+ const result = await executeJXA(script);
419
+ if (!result.success) {
420
+ const fallbackScript = `
421
+ tell application "System Events"
422
+ set screenWidth to word 3 of (do shell script "system_profiler SPDisplaysDataType | grep Resolution")
423
+ set screenHeight to word 5 of (do shell script "system_profiler SPDisplaysDataType | grep Resolution")
424
+ return screenWidth & "x" & screenHeight
425
+ end tell
426
+ `;
427
+ const fallback = await executeAppleScript(fallbackScript);
428
+ const [width, height] = fallback.stdout.split("x").map((n) => parseInt(n));
429
+ return [
430
+ {
431
+ id: 0,
432
+ width: width || 1920,
433
+ height: height || 1080,
434
+ main: true
435
+ }
436
+ ];
437
+ }
438
+ try {
439
+ return JSON.parse(result.stdout);
440
+ } catch {
441
+ return [
442
+ {
443
+ id: 0,
444
+ width: 1920,
445
+ height: 1080,
446
+ main: true
447
+ }
448
+ ];
449
+ }
450
+ }
451
+ async function getScreenResolution() {
452
+ const displays = await getDisplayInfo();
453
+ const mainDisplay = displays.find((d) => d.main) || displays[0];
454
+ return {
455
+ width: mainDisplay?.width || 1920,
456
+ height: mainDisplay?.height || 1080
457
+ };
458
+ }
459
+ function isMacOS() {
460
+ return process.platform === "darwin";
461
+ }
462
+ function escapeString(str) {
463
+ return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
464
+ }
465
+ async function sayText(text, voice, rate) {
466
+ let script = `say "${escapeString(text)}"`;
467
+ if (voice)
468
+ script += ` using "${voice}"`;
469
+ if (rate)
470
+ script += ` speaking rate ${rate}`;
471
+ return executeAppleScript(script);
472
+ }
473
+ async function beep(count = 1) {
474
+ return executeAppleScript(`beep ${count}`);
475
+ }
476
+ async function getSystemDateTime() {
477
+ const script = `
478
+ get current date
479
+ `;
480
+ const result = await executeAppleScript(script);
481
+ if (!result.success) {
482
+ throw new Error(`Failed to get system date: ${result.stderr}`);
483
+ }
484
+ return result.stdout;
485
+ }
486
+
487
+ // src/mcp/index.ts
488
+ var MCP_PORT = parseInt(process.env.MCP_PORT || "8914");
489
+ async function osascriptExecute(script, language = "applescript") {
490
+ const result = language === "javascript" ? await executeJXA(script) : await executeAppleScript(script);
491
+ return JSON.stringify(result, null, 2);
492
+ }
493
+ async function osascriptNotification(message, title = "Notification", subtitle, soundName) {
494
+ const options = { message, title, subtitle, soundName };
495
+ const result = await displayNotification(options);
496
+ return result.success ? `Notification sent: ${title}` : `Error: ${result.stderr}`;
497
+ }
498
+ async function osascriptDialog(message, title, buttons, defaultButton, icon) {
499
+ const options = { message, title, buttons, defaultButton, icon };
500
+ try {
501
+ const result = await displayDialog(options);
502
+ return `Button returned: ${result.buttonReturned}${result.gaveUp ? " (timed out)" : ""}`;
503
+ } catch (error) {
504
+ return `Error: ${error}`;
505
+ }
506
+ }
507
+ async function osascriptGetVolume() {
508
+ const info = await getVolume();
509
+ return `Volume: ${info.volume}%${info.muted ? " (muted)" : ""}`;
510
+ }
511
+ async function osascriptSetVolume(volume, muted) {
512
+ const result = await setVolume(volume, muted);
513
+ return result.success ? `Volume set to ${volume}%` : `Error: ${result.stderr}`;
514
+ }
515
+ async function osascriptGetClipboard() {
516
+ try {
517
+ const content = await getClipboard();
518
+ return content || "(clipboard is empty)";
519
+ } catch (error) {
520
+ return `Error: ${error}`;
521
+ }
522
+ }
523
+ async function osascriptSetClipboard(text) {
524
+ const result = await setClipboard(text);
525
+ return result.success ? "Clipboard updated" : `Error: ${result.stderr}`;
526
+ }
527
+ async function osascriptClearClipboard() {
528
+ const result = await clearClipboard();
529
+ return result.success ? "Clipboard cleared" : `Error: ${result.stderr}`;
530
+ }
531
+ async function osascriptListApps() {
532
+ const apps = await listRunningApplications();
533
+ const lines = ["Running Applications:", "=".repeat(40), ""];
534
+ for (const app of apps) {
535
+ lines.push(`- ${app.name}`);
536
+ }
537
+ return lines.join(`
538
+ `);
539
+ }
540
+ async function osascriptGetAppInfo(appName) {
541
+ const info = await getApplicationInfo(appName);
542
+ if (!info.running) {
543
+ return `Application "${appName}" is not running`;
544
+ }
545
+ const lines = [
546
+ `Application: ${info.name}`,
547
+ "=".repeat(40),
548
+ `Running: ${info.running}`,
549
+ info.path ? `Path: ${info.path}` : "",
550
+ info.processId ? `PID: ${info.processId}` : "",
551
+ info.windowCount !== undefined ? `Windows: ${info.windowCount}` : ""
552
+ ].filter(Boolean);
553
+ return lines.join(`
554
+ `);
555
+ }
556
+ async function osascriptActivateApp(appName) {
557
+ const result = await activateApplication(appName);
558
+ return result.success ? `Activated ${appName}` : `Error: ${result.stderr}`;
559
+ }
560
+ async function osascriptQuitApp(appName, force = false) {
561
+ const result = await quitApplication(appName, force);
562
+ return result.success ? `Quit ${appName}` : `Error: ${result.stderr}`;
563
+ }
564
+ async function osascriptLaunchApp(appName) {
565
+ const result = await launchApplication(appName);
566
+ return result.success ? `Launched ${appName}` : `Error: ${result.stderr}`;
567
+ }
568
+ async function osascriptGetFrontmostApp() {
569
+ const appName = await getFrontmostApplication();
570
+ return `Frontmost application: ${appName}`;
571
+ }
572
+ async function osascriptGetWindows(appName) {
573
+ const windows = await getApplicationWindows(appName);
574
+ if (windows.length === 0) {
575
+ return `No windows for ${appName}`;
576
+ }
577
+ const lines = [`Windows for ${appName}:`, "=".repeat(40), ""];
578
+ for (const win of windows) {
579
+ lines.push(`${win.index}. ${win.name || "(unnamed)"} (ID: ${win.id})`);
580
+ }
581
+ return lines.join(`
582
+ `);
583
+ }
584
+ async function osascriptSetWindowBounds(appName, windowIndex, x, y, width, height) {
585
+ const result = await setWindowBounds(appName, windowIndex, { x, y, width, height });
586
+ return result.success ? `Window ${windowIndex} bounds set to ${width}x${height} at (${x}, ${y})` : `Error: ${result.stderr}`;
587
+ }
588
+ function parseModifiers(modifiersStr) {
589
+ if (!modifiersStr)
590
+ return [];
591
+ return modifiersStr.split(",").map((m) => m.trim().toLowerCase()).filter((m) => ["command", "shift", "option", "control"].includes(m));
592
+ }
593
+ async function osascriptKeystroke(key, modifiers = []) {
594
+ const options = { key, modifiers };
595
+ const result = await sendKeystroke(options);
596
+ const modStr = modifiers.length > 0 ? ` with ${modifiers.join("+")}` : "";
597
+ return result.success ? `Sent keystroke: ${key}${modStr}` : `Error: ${result.stderr}`;
598
+ }
599
+ async function osascriptKeyCode(keyCode, modifiers = []) {
600
+ const options = { keyCode, modifiers };
601
+ const result = await sendKeyCode(options);
602
+ const modStr = modifiers.length > 0 ? ` with ${modifiers.join("+")}` : "";
603
+ return result.success ? `Sent key code: ${keyCode}${modStr}` : `Error: ${result.stderr}`;
604
+ }
605
+ async function osascriptTypeText(text) {
606
+ const result = await typeText(text);
607
+ return result.success ? `Typed: ${text}` : `Error: ${result.stderr}`;
608
+ }
609
+ async function osascriptSafariGetURL() {
610
+ try {
611
+ const url = await getSafariURL();
612
+ return `Current Safari URL: ${url}`;
613
+ } catch (error) {
614
+ return `Error: ${error}`;
615
+ }
616
+ }
617
+ async function osascriptSafariSetURL(url) {
618
+ const result = await setSafariURL(url);
619
+ return result.success ? `Navigated to: ${url}` : `Error: ${result.stderr}`;
620
+ }
621
+ async function osascriptSafariGetTabs() {
622
+ const titles = await getSafariTabTitles();
623
+ if (titles.length === 0) {
624
+ return "No Safari tabs";
625
+ }
626
+ const lines = ["Safari Tabs:", "=".repeat(40), ""];
627
+ titles.forEach((title, i) => lines.push(`${i + 1}. ${title}`));
628
+ return lines.join(`
629
+ `);
630
+ }
631
+ async function osascriptSafariExecJS(jsCode) {
632
+ try {
633
+ const result = await executeSafariJS(jsCode);
634
+ return result || "(no output)";
635
+ } catch (error) {
636
+ return `Error: ${error}`;
637
+ }
638
+ }
639
+ async function osascriptFinderGetSelection() {
640
+ const items = await getFinderSelection();
641
+ if (items.length === 0) {
642
+ return "No Finder selection";
643
+ }
644
+ const lines = ["Finder Selection:", "=".repeat(40), ""];
645
+ for (const item of items) {
646
+ lines.push(`- ${item.name} (${item.type})`);
647
+ lines.push(` ${item.path}`);
648
+ }
649
+ return lines.join(`
650
+ `);
651
+ }
652
+ async function osascriptFinderOpen(path) {
653
+ const result = await openFinderWindow(path);
654
+ return result.success ? `Opened Finder at: ${path}` : `Error: ${result.stderr}`;
655
+ }
656
+ async function osascriptFinderCreateFolder(parentPath, folderName) {
657
+ const result = await createFolder(parentPath, folderName);
658
+ return result.success ? `Created folder: ${folderName}` : `Error: ${result.stderr}`;
659
+ }
660
+ async function osascriptFinderTrash(path) {
661
+ const result = await moveToTrash(path);
662
+ return result.success ? `Moved to trash: ${path}` : `Error: ${result.stderr}`;
663
+ }
664
+ async function osascriptGetDisplays() {
665
+ const displays = await getDisplayInfo();
666
+ const lines = ["Display Information:", "=".repeat(40), ""];
667
+ for (const display of displays) {
668
+ lines.push(`Display ${display.id}: ${display.width}x${display.height}${display.main ? " (main)" : ""}`);
669
+ }
670
+ return lines.join(`
671
+ `);
672
+ }
673
+ async function osascriptGetResolution() {
674
+ const { width, height } = await getScreenResolution();
675
+ return `Screen resolution: ${width}x${height}`;
676
+ }
677
+ async function osascriptSay(text, voice, rate) {
678
+ const result = await sayText(text, voice, rate);
679
+ return result.success ? `Said: "${text}"` : `Error: ${result.stderr}`;
680
+ }
681
+ async function osascriptBeep(count = 1) {
682
+ const result = await beep(count);
683
+ return result.success ? `Beeped ${count} time(s)` : `Error: ${result.stderr}`;
684
+ }
685
+ async function osascriptGetDateTime() {
686
+ const dateTime = await getSystemDateTime();
687
+ return `System date/time: ${dateTime}`;
688
+ }
689
+ var TOOLS = [
690
+ { name: "osascript_execute", description: "Execute raw AppleScript or JXA code", args: ["script", "language?"] },
691
+ { name: "osascript_display_notification", description: "Display a macOS notification", args: ["message", "title?", "subtitle?", "sound_name?"] },
692
+ { name: "osascript_display_dialog", description: "Display a dialog with buttons", args: ["message", "title?", "buttons?", "default_button?", "icon?"] },
693
+ { name: "osascript_get_volume", description: "Get system volume level", args: [] },
694
+ { name: "osascript_set_volume", description: "Set system volume level (0-100)", args: ["volume", "muted?"] },
695
+ { name: "osascript_get_clipboard", description: "Get clipboard contents", args: [] },
696
+ { name: "osascript_set_clipboard", description: "Set clipboard contents", args: ["text"] },
697
+ { name: "osascript_clear_clipboard", description: "Clear clipboard contents", args: [] },
698
+ { name: "osascript_list_apps", description: "List all running applications", args: [] },
699
+ { name: "osascript_get_app_info", description: "Get detailed information about an application", args: ["app_name"] },
700
+ { name: "osascript_activate_app", description: "Activate (bring to front) an application", args: ["app_name"] },
701
+ { name: "osascript_quit_app", description: "Quit an application", args: ["app_name", "force?"] },
702
+ { name: "osascript_launch_app", description: "Launch an application", args: ["app_name"] },
703
+ { name: "osascript_get_frontmost_app", description: "Get the name of the frontmost application", args: [] },
704
+ { name: "osascript_get_windows", description: "Get window list for an application", args: ["app_name"] },
705
+ { name: "osascript_set_window_bounds", description: "Set window position and size", args: ["app_name", "window_index", "x", "y", "width", "height"] },
706
+ { name: "osascript_keystroke", description: "Send keystroke with optional modifiers", args: ["key", "modifiers?"] },
707
+ { name: "osascript_key_code", description: "Send key code (for special keys)", args: ["key_code", "modifiers?"] },
708
+ { name: "osascript_type_text", description: "Type text (simulate keyboard input)", args: ["text"] },
709
+ { name: "osascript_safari_get_url", description: "Get current Safari URL", args: [] },
710
+ { name: "osascript_safari_set_url", description: "Navigate Safari to URL", args: ["url"] },
711
+ { name: "osascript_safari_get_tabs", description: "Get Safari tab titles", args: [] },
712
+ { name: "osascript_safari_exec_js", description: "Execute JavaScript in Safari", args: ["js_code"] },
713
+ { name: "osascript_finder_get_selection", description: "Get currently selected files in Finder", args: [] },
714
+ { name: "osascript_finder_open", description: "Open Finder window at path", args: ["path"] },
715
+ { name: "osascript_finder_create_folder", description: "Create new folder in Finder", args: ["parent_path", "folder_name"] },
716
+ { name: "osascript_finder_trash", description: "Move file/folder to trash", args: ["path"] },
717
+ { name: "osascript_get_displays", description: "Get display information", args: [] },
718
+ { name: "osascript_get_resolution", description: "Get main screen resolution", args: [] },
719
+ { name: "osascript_say", description: "Say text using system voice", args: ["text", "voice?", "rate?"] },
720
+ { name: "osascript_beep", description: "Play system beep", args: ["count?"] },
721
+ { name: "osascript_get_datetime", description: "Get current system date/time", args: [] }
722
+ ];
723
+ Bun.serve({
724
+ port: MCP_PORT,
725
+ fetch: async (req) => {
726
+ const url = new URL(req.url);
727
+ if (url.pathname === "/health") {
728
+ return Response.json({
729
+ status: "ok",
730
+ port: MCP_PORT,
731
+ service: "osascript-mcp",
732
+ platform: process.platform,
733
+ macos: isMacOS()
734
+ });
735
+ }
736
+ if (url.pathname === "/mcp") {
737
+ if (req.method === "GET") {
738
+ return Response.json({
739
+ name: "osascript-mcp",
740
+ version: "1.0.0",
741
+ description: "macOS automation via osascript (AppleScript/JXA)",
742
+ platform: process.platform,
743
+ macos: isMacOS(),
744
+ tools: TOOLS
745
+ });
746
+ }
747
+ if (req.method === "POST") {
748
+ if (!isMacOS()) {
749
+ return Response.json({ error: "osascript MCP only works on macOS" }, { status: 500 });
750
+ }
751
+ const body = await req.json();
752
+ const { tool, args = {} } = body;
753
+ try {
754
+ let result;
755
+ switch (tool) {
756
+ case "osascript_execute":
757
+ result = await osascriptExecute(args?.script, args?.language || "applescript");
758
+ break;
759
+ case "osascript_display_notification":
760
+ result = await osascriptNotification(args?.message, args?.title, args?.subtitle, args?.sound_name);
761
+ break;
762
+ case "osascript_display_dialog":
763
+ result = await osascriptDialog(args?.message, args?.title, args?.buttons?.split(","), args?.default_button, args?.icon);
764
+ break;
765
+ case "osascript_get_volume":
766
+ result = await osascriptGetVolume();
767
+ break;
768
+ case "osascript_set_volume":
769
+ result = await osascriptSetVolume(args?.volume, args?.muted);
770
+ break;
771
+ case "osascript_get_clipboard":
772
+ result = await osascriptGetClipboard();
773
+ break;
774
+ case "osascript_set_clipboard":
775
+ result = await osascriptSetClipboard(args?.text);
776
+ break;
777
+ case "osascript_clear_clipboard":
778
+ result = await osascriptClearClipboard();
779
+ break;
780
+ case "osascript_list_apps":
781
+ result = await osascriptListApps();
782
+ break;
783
+ case "osascript_get_app_info":
784
+ result = await osascriptGetAppInfo(args?.app_name);
785
+ break;
786
+ case "osascript_activate_app":
787
+ result = await osascriptActivateApp(args?.app_name);
788
+ break;
789
+ case "osascript_quit_app":
790
+ result = await osascriptQuitApp(args?.app_name, args?.force);
791
+ break;
792
+ case "osascript_launch_app":
793
+ result = await osascriptLaunchApp(args?.app_name);
794
+ break;
795
+ case "osascript_get_frontmost_app":
796
+ result = await osascriptGetFrontmostApp();
797
+ break;
798
+ case "osascript_get_windows":
799
+ result = await osascriptGetWindows(args?.app_name);
800
+ break;
801
+ case "osascript_set_window_bounds":
802
+ result = await osascriptSetWindowBounds(args?.app_name, args?.window_index, args?.x, args?.y, args?.width, args?.height);
803
+ break;
804
+ case "osascript_keystroke":
805
+ result = await osascriptKeystroke(args?.key, parseModifiers(args?.modifiers));
806
+ break;
807
+ case "osascript_key_code":
808
+ result = await osascriptKeyCode(args?.key_code, parseModifiers(args?.modifiers));
809
+ break;
810
+ case "osascript_type_text":
811
+ result = await osascriptTypeText(args?.text);
812
+ break;
813
+ case "osascript_safari_get_url":
814
+ result = await osascriptSafariGetURL();
815
+ break;
816
+ case "osascript_safari_set_url":
817
+ result = await osascriptSafariSetURL(args?.url);
818
+ break;
819
+ case "osascript_safari_get_tabs":
820
+ result = await osascriptSafariGetTabs();
821
+ break;
822
+ case "osascript_safari_exec_js":
823
+ result = await osascriptSafariExecJS(args?.js_code);
824
+ break;
825
+ case "osascript_finder_get_selection":
826
+ result = await osascriptFinderGetSelection();
827
+ break;
828
+ case "osascript_finder_open":
829
+ result = await osascriptFinderOpen(args?.path);
830
+ break;
831
+ case "osascript_finder_create_folder":
832
+ result = await osascriptFinderCreateFolder(args?.parent_path, args?.folder_name);
833
+ break;
834
+ case "osascript_finder_trash":
835
+ result = await osascriptFinderTrash(args?.path);
836
+ break;
837
+ case "osascript_get_displays":
838
+ result = await osascriptGetDisplays();
839
+ break;
840
+ case "osascript_get_resolution":
841
+ result = await osascriptGetResolution();
842
+ break;
843
+ case "osascript_say":
844
+ result = await osascriptSay(args?.text, args?.voice, args?.rate);
845
+ break;
846
+ case "osascript_beep":
847
+ result = await osascriptBeep(args?.count || 1);
848
+ break;
849
+ case "osascript_get_datetime":
850
+ result = await osascriptGetDateTime();
851
+ break;
852
+ default:
853
+ return Response.json({ error: `Unknown tool: ${tool}` }, { status: 400 });
854
+ }
855
+ return Response.json({ result });
856
+ } catch (error) {
857
+ return Response.json({ error: String(error) }, { status: 500 });
858
+ }
859
+ }
860
+ }
861
+ return Response.json({ error: "Not found" }, { status: 404 });
862
+ }
863
+ });
864
+ console.log(`osascript MCP Server running on port ${MCP_PORT}`);
865
+ console.log(` Health: http://localhost:${MCP_PORT}/health`);
866
+ console.log(` MCP: http://localhost:${MCP_PORT}/mcp`);