@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,565 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * osascript MCP Server (HTTP)
4
+ *
5
+ * Model Context Protocol server for macOS automation via osascript
6
+ * Uses HTTP transport for remote access
7
+ */
8
+
9
+ import {
10
+ executeAppleScript,
11
+ executeJXA,
12
+ displayNotification,
13
+ displayDialog,
14
+ getVolume,
15
+ setVolume,
16
+ getClipboard,
17
+ setClipboard,
18
+ clearClipboard,
19
+ listRunningApplications,
20
+ getApplicationInfo,
21
+ activateApplication,
22
+ quitApplication,
23
+ launchApplication,
24
+ getFrontmostApplication,
25
+ getApplicationWindows,
26
+ setWindowBounds,
27
+ sendKeystroke,
28
+ sendKeyCode,
29
+ typeText,
30
+ getSafariURL,
31
+ setSafariURL,
32
+ getSafariTabTitles,
33
+ executeSafariJS,
34
+ getFinderSelection,
35
+ openFinderWindow,
36
+ createFolder,
37
+ moveToTrash,
38
+ getDisplayInfo,
39
+ getScreenResolution,
40
+ sayText,
41
+ beep,
42
+ getSystemDateTime,
43
+ isMacOS,
44
+ } from "../index.js";
45
+
46
+ import type { NotificationOptions, DialogOptions, KeystrokeOptions, KeyCodeOptions } from "../types.js";
47
+
48
+ // ==============
49
+ // Configuration
50
+ // ==============
51
+
52
+ const MCP_PORT = parseInt(process.env.MCP_PORT || "8914");
53
+
54
+ // ==============
55
+ // Tool Implementations
56
+ // ==============
57
+
58
+ async function osascriptExecute(
59
+ script: string,
60
+ language: "applescript" | "javascript" = "applescript"
61
+ ): Promise<string> {
62
+ const result = language === "javascript" ? await executeJXA(script) : await executeAppleScript(script);
63
+ return JSON.stringify(result, null, 2);
64
+ }
65
+
66
+ async function osascriptNotification(
67
+ message: string,
68
+ title: string = "Notification",
69
+ subtitle?: string,
70
+ soundName?: string
71
+ ): Promise<string> {
72
+ const options: NotificationOptions = { message, title, subtitle, soundName };
73
+ const result = await displayNotification(options);
74
+ return result.success ? `Notification sent: ${title}` : `Error: ${result.stderr}`;
75
+ }
76
+
77
+ async function osascriptDialog(
78
+ message: string,
79
+ title?: string,
80
+ buttons?: string[],
81
+ defaultButton?: number,
82
+ icon?: "note" | "caution" | "stop"
83
+ ): Promise<string> {
84
+ const options: DialogOptions = { message, title, buttons, defaultButton, icon };
85
+ try {
86
+ const result = await displayDialog(options);
87
+ return `Button returned: ${result.buttonReturned}${result.gaveUp ? " (timed out)" : ""}`;
88
+ } catch (error) {
89
+ return `Error: ${error}`;
90
+ }
91
+ }
92
+
93
+ async function osascriptGetVolume(): Promise<string> {
94
+ const info = await getVolume();
95
+ return `Volume: ${info.volume}%${info.muted ? " (muted)" : ""}`;
96
+ }
97
+
98
+ async function osascriptSetVolume(volume: number, muted?: boolean): Promise<string> {
99
+ const result = await setVolume(volume, muted);
100
+ return result.success ? `Volume set to ${volume}%` : `Error: ${result.stderr}`;
101
+ }
102
+
103
+ async function osascriptGetClipboard(): Promise<string> {
104
+ try {
105
+ const content = await getClipboard();
106
+ return content || "(clipboard is empty)";
107
+ } catch (error) {
108
+ return `Error: ${error}`;
109
+ }
110
+ }
111
+
112
+ async function osascriptSetClipboard(text: string): Promise<string> {
113
+ const result = await setClipboard(text);
114
+ return result.success ? "Clipboard updated" : `Error: ${result.stderr}`;
115
+ }
116
+
117
+ async function osascriptClearClipboard(): Promise<string> {
118
+ const result = await clearClipboard();
119
+ return result.success ? "Clipboard cleared" : `Error: ${result.stderr}`;
120
+ }
121
+
122
+ async function osascriptListApps(): Promise<string> {
123
+ const apps = await listRunningApplications();
124
+ const lines = ["Running Applications:", "=".repeat(40), ""];
125
+ for (const app of apps) {
126
+ lines.push(`- ${app.name}`);
127
+ }
128
+ return lines.join("\n");
129
+ }
130
+
131
+ async function osascriptGetAppInfo(appName: string): Promise<string> {
132
+ const info = await getApplicationInfo(appName);
133
+ if (!info.running) {
134
+ return `Application "${appName}" is not running`;
135
+ }
136
+ const lines = [
137
+ `Application: ${info.name}`,
138
+ "=".repeat(40),
139
+ `Running: ${info.running}`,
140
+ info.path ? `Path: ${info.path}` : "",
141
+ info.processId ? `PID: ${info.processId}` : "",
142
+ info.windowCount !== undefined ? `Windows: ${info.windowCount}` : "",
143
+ ].filter(Boolean);
144
+ return lines.join("\n");
145
+ }
146
+
147
+ async function osascriptActivateApp(appName: string): Promise<string> {
148
+ const result = await activateApplication(appName);
149
+ return result.success ? `Activated ${appName}` : `Error: ${result.stderr}`;
150
+ }
151
+
152
+ async function osascriptQuitApp(appName: string, force: boolean = false): Promise<string> {
153
+ const result = await quitApplication(appName, force);
154
+ return result.success ? `Quit ${appName}` : `Error: ${result.stderr}`;
155
+ }
156
+
157
+ async function osascriptLaunchApp(appName: string): Promise<string> {
158
+ const result = await launchApplication(appName);
159
+ return result.success ? `Launched ${appName}` : `Error: ${result.stderr}`;
160
+ }
161
+
162
+ async function osascriptGetFrontmostApp(): Promise<string> {
163
+ const appName = await getFrontmostApplication();
164
+ return `Frontmost application: ${appName}`;
165
+ }
166
+
167
+ async function osascriptGetWindows(appName: string): Promise<string> {
168
+ const windows = await getApplicationWindows(appName);
169
+ if (windows.length === 0) {
170
+ return `No windows for ${appName}`;
171
+ }
172
+ const lines = [`Windows for ${appName}:`, "=".repeat(40), ""];
173
+ for (const win of windows) {
174
+ lines.push(`${win.index}. ${win.name || "(unnamed)"} (ID: ${win.id})`);
175
+ }
176
+ return lines.join("\n");
177
+ }
178
+
179
+ async function osascriptSetWindowBounds(
180
+ appName: string,
181
+ windowIndex: number,
182
+ x: number,
183
+ y: number,
184
+ width: number,
185
+ height: number
186
+ ): Promise<string> {
187
+ const result = await setWindowBounds(appName, windowIndex, { x, y, width, height });
188
+ return result.success
189
+ ? `Window ${windowIndex} bounds set to ${width}x${height} at (${x}, ${y})`
190
+ : `Error: ${result.stderr}`;
191
+ }
192
+
193
+ function parseModifiers(modifiersStr?: string): ("command" | "shift" | "option" | "control")[] {
194
+ if (!modifiersStr) return [];
195
+ return modifiersStr
196
+ .split(",")
197
+ .map((m) => m.trim().toLowerCase() as "command" | "shift" | "option" | "control")
198
+ .filter((m) => ["command", "shift", "option", "control"].includes(m));
199
+ }
200
+
201
+ async function osascriptKeystroke(
202
+ key: string,
203
+ modifiers: ("command" | "shift" | "option" | "control")[] = []
204
+ ): Promise<string> {
205
+ const options: KeystrokeOptions = { key, modifiers };
206
+ const result = await sendKeystroke(options);
207
+ const modStr = modifiers.length > 0 ? ` with ${modifiers.join("+")}` : "";
208
+ return result.success ? `Sent keystroke: ${key}${modStr}` : `Error: ${result.stderr}`;
209
+ }
210
+
211
+ async function osascriptKeyCode(
212
+ keyCode: number,
213
+ modifiers: ("command" | "shift" | "option" | "control")[] = []
214
+ ): Promise<string> {
215
+ const options: KeyCodeOptions = { keyCode, modifiers };
216
+ const result = await sendKeyCode(options);
217
+ const modStr = modifiers.length > 0 ? ` with ${modifiers.join("+")}` : "";
218
+ return result.success ? `Sent key code: ${keyCode}${modStr}` : `Error: ${result.stderr}`;
219
+ }
220
+
221
+ async function osascriptTypeText(text: string): Promise<string> {
222
+ const result = await typeText(text);
223
+ return result.success ? `Typed: ${text}` : `Error: ${result.stderr}`;
224
+ }
225
+
226
+ async function osascriptSafariGetURL(): Promise<string> {
227
+ try {
228
+ const url = await getSafariURL();
229
+ return `Current Safari URL: ${url}`;
230
+ } catch (error) {
231
+ return `Error: ${error}`;
232
+ }
233
+ }
234
+
235
+ async function osascriptSafariSetURL(url: string): Promise<string> {
236
+ const result = await setSafariURL(url);
237
+ return result.success ? `Navigated to: ${url}` : `Error: ${result.stderr}`;
238
+ }
239
+
240
+ async function osascriptSafariGetTabs(): Promise<string> {
241
+ const titles = await getSafariTabTitles();
242
+ if (titles.length === 0) {
243
+ return "No Safari tabs";
244
+ }
245
+ const lines = ["Safari Tabs:", "=".repeat(40), ""];
246
+ titles.forEach((title, i) => lines.push(`${i + 1}. ${title}`));
247
+ return lines.join("\n");
248
+ }
249
+
250
+ async function osascriptSafariExecJS(jsCode: string): Promise<string> {
251
+ try {
252
+ const result = await executeSafariJS(jsCode);
253
+ return result || "(no output)";
254
+ } catch (error) {
255
+ return `Error: ${error}`;
256
+ }
257
+ }
258
+
259
+ async function osascriptFinderGetSelection(): Promise<string> {
260
+ const items = await getFinderSelection();
261
+ if (items.length === 0) {
262
+ return "No Finder selection";
263
+ }
264
+ const lines = ["Finder Selection:", "=".repeat(40), ""];
265
+ for (const item of items) {
266
+ lines.push(`- ${item.name} (${item.type})`);
267
+ lines.push(` ${item.path}`);
268
+ }
269
+ return lines.join("\n");
270
+ }
271
+
272
+ async function osascriptFinderOpen(path: string): Promise<string> {
273
+ const result = await openFinderWindow(path);
274
+ return result.success ? `Opened Finder at: ${path}` : `Error: ${result.stderr}`;
275
+ }
276
+
277
+ async function osascriptFinderCreateFolder(parentPath: string, folderName: string): Promise<string> {
278
+ const result = await createFolder(parentPath, folderName);
279
+ return result.success ? `Created folder: ${folderName}` : `Error: ${result.stderr}`;
280
+ }
281
+
282
+ async function osascriptFinderTrash(path: string): Promise<string> {
283
+ const result = await moveToTrash(path);
284
+ return result.success ? `Moved to trash: ${path}` : `Error: ${result.stderr}`;
285
+ }
286
+
287
+ async function osascriptGetDisplays(): Promise<string> {
288
+ const displays = await getDisplayInfo();
289
+ const lines = ["Display Information:", "=".repeat(40), ""];
290
+ for (const display of displays) {
291
+ lines.push(`Display ${display.id}: ${display.width}x${display.height}${display.main ? " (main)" : ""}`);
292
+ }
293
+ return lines.join("\n");
294
+ }
295
+
296
+ async function osascriptGetResolution(): Promise<string> {
297
+ const { width, height } = await getScreenResolution();
298
+ return `Screen resolution: ${width}x${height}`;
299
+ }
300
+
301
+ async function osascriptSay(text: string, voice?: string, rate?: number): Promise<string> {
302
+ const result = await sayText(text, voice, rate);
303
+ return result.success ? `Said: "${text}"` : `Error: ${result.stderr}`;
304
+ }
305
+
306
+ async function osascriptBeep(count: number = 1): Promise<string> {
307
+ const result = await beep(count);
308
+ return result.success ? `Beeped ${count} time(s)` : `Error: ${result.stderr}`;
309
+ }
310
+
311
+ async function osascriptGetDateTime(): Promise<string> {
312
+ const dateTime = await getSystemDateTime();
313
+ return `System date/time: ${dateTime}`;
314
+ }
315
+
316
+ // ==============
317
+ // Tool Definitions
318
+ // ==============
319
+
320
+ const TOOLS = [
321
+ // Core
322
+ { name: "osascript_execute", description: "Execute raw AppleScript or JXA code", args: ["script", "language?"] },
323
+ // System
324
+ { name: "osascript_display_notification", description: "Display a macOS notification", args: ["message", "title?", "subtitle?", "sound_name?"] },
325
+ { name: "osascript_display_dialog", description: "Display a dialog with buttons", args: ["message", "title?", "buttons?", "default_button?", "icon?"] },
326
+ // Volume
327
+ { name: "osascript_get_volume", description: "Get system volume level", args: [] },
328
+ { name: "osascript_set_volume", description: "Set system volume level (0-100)", args: ["volume", "muted?"] },
329
+ // Clipboard
330
+ { name: "osascript_get_clipboard", description: "Get clipboard contents", args: [] },
331
+ { name: "osascript_set_clipboard", description: "Set clipboard contents", args: ["text"] },
332
+ { name: "osascript_clear_clipboard", description: "Clear clipboard contents", args: [] },
333
+ // Applications
334
+ { name: "osascript_list_apps", description: "List all running applications", args: [] },
335
+ { name: "osascript_get_app_info", description: "Get detailed information about an application", args: ["app_name"] },
336
+ { name: "osascript_activate_app", description: "Activate (bring to front) an application", args: ["app_name"] },
337
+ { name: "osascript_quit_app", description: "Quit an application", args: ["app_name", "force?"] },
338
+ { name: "osascript_launch_app", description: "Launch an application", args: ["app_name"] },
339
+ { name: "osascript_get_frontmost_app", description: "Get the name of the frontmost application", args: [] },
340
+ // Windows
341
+ { name: "osascript_get_windows", description: "Get window list for an application", args: ["app_name"] },
342
+ { name: "osascript_set_window_bounds", description: "Set window position and size", args: ["app_name", "window_index", "x", "y", "width", "height"] },
343
+ // Keyboard
344
+ { name: "osascript_keystroke", description: "Send keystroke with optional modifiers", args: ["key", "modifiers?"] },
345
+ { name: "osascript_key_code", description: "Send key code (for special keys)", args: ["key_code", "modifiers?"] },
346
+ { name: "osascript_type_text", description: "Type text (simulate keyboard input)", args: ["text"] },
347
+ // Safari
348
+ { name: "osascript_safari_get_url", description: "Get current Safari URL", args: [] },
349
+ { name: "osascript_safari_set_url", description: "Navigate Safari to URL", args: ["url"] },
350
+ { name: "osascript_safari_get_tabs", description: "Get Safari tab titles", args: [] },
351
+ { name: "osascript_safari_exec_js", description: "Execute JavaScript in Safari", args: ["js_code"] },
352
+ // Finder
353
+ { name: "osascript_finder_get_selection", description: "Get currently selected files in Finder", args: [] },
354
+ { name: "osascript_finder_open", description: "Open Finder window at path", args: ["path"] },
355
+ { name: "osascript_finder_create_folder", description: "Create new folder in Finder", args: ["parent_path", "folder_name"] },
356
+ { name: "osascript_finder_trash", description: "Move file/folder to trash", args: ["path"] },
357
+ // Display
358
+ { name: "osascript_get_displays", description: "Get display information", args: [] },
359
+ { name: "osascript_get_resolution", description: "Get main screen resolution", args: [] },
360
+ // Audio
361
+ { name: "osascript_say", description: "Say text using system voice", args: ["text", "voice?", "rate?"] },
362
+ { name: "osascript_beep", description: "Play system beep", args: ["count?"] },
363
+ // Date/Time
364
+ { name: "osascript_get_datetime", description: "Get current system date/time", args: [] },
365
+ ];
366
+
367
+ // ==============
368
+ // HTTP Server
369
+ // ==============
370
+
371
+ Bun.serve({
372
+ port: MCP_PORT,
373
+ fetch: async (req: Request) => {
374
+ const url = new URL(req.url);
375
+
376
+ // Health check
377
+ if (url.pathname === "/health") {
378
+ return Response.json({
379
+ status: "ok",
380
+ port: MCP_PORT,
381
+ service: "osascript-mcp",
382
+ platform: process.platform,
383
+ macos: isMacOS(),
384
+ });
385
+ }
386
+
387
+ // MCP endpoint
388
+ if (url.pathname === "/mcp") {
389
+ if (req.method === "GET") {
390
+ // Return available tools
391
+ return Response.json({
392
+ name: "osascript-mcp",
393
+ version: "1.0.0",
394
+ description: "macOS automation via osascript (AppleScript/JXA)",
395
+ platform: process.platform,
396
+ macos: isMacOS(),
397
+ tools: TOOLS,
398
+ });
399
+ }
400
+
401
+ if (req.method === "POST") {
402
+ // Verify macOS
403
+ if (!isMacOS()) {
404
+ return Response.json({ error: "osascript MCP only works on macOS" }, { status: 500 });
405
+ }
406
+
407
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
408
+ const body = (await req.json()) as any;
409
+ const { tool, args = {} } = body;
410
+
411
+ try {
412
+ let result: string;
413
+
414
+ switch (tool) {
415
+ // Core
416
+ case "osascript_execute":
417
+ result = await osascriptExecute(args?.script, args?.language || "applescript");
418
+ break;
419
+
420
+ // System
421
+ case "osascript_display_notification":
422
+ result = await osascriptNotification(args?.message, args?.title, args?.subtitle, args?.sound_name);
423
+ break;
424
+ case "osascript_display_dialog":
425
+ result = await osascriptDialog(
426
+ args?.message,
427
+ args?.title,
428
+ args?.buttons?.split(","),
429
+ args?.default_button,
430
+ args?.icon
431
+ );
432
+ break;
433
+
434
+ // Volume
435
+ case "osascript_get_volume":
436
+ result = await osascriptGetVolume();
437
+ break;
438
+ case "osascript_set_volume":
439
+ result = await osascriptSetVolume(args?.volume, args?.muted);
440
+ break;
441
+
442
+ // Clipboard
443
+ case "osascript_get_clipboard":
444
+ result = await osascriptGetClipboard();
445
+ break;
446
+ case "osascript_set_clipboard":
447
+ result = await osascriptSetClipboard(args?.text);
448
+ break;
449
+ case "osascript_clear_clipboard":
450
+ result = await osascriptClearClipboard();
451
+ break;
452
+
453
+ // Applications
454
+ case "osascript_list_apps":
455
+ result = await osascriptListApps();
456
+ break;
457
+ case "osascript_get_app_info":
458
+ result = await osascriptGetAppInfo(args?.app_name);
459
+ break;
460
+ case "osascript_activate_app":
461
+ result = await osascriptActivateApp(args?.app_name);
462
+ break;
463
+ case "osascript_quit_app":
464
+ result = await osascriptQuitApp(args?.app_name, args?.force);
465
+ break;
466
+ case "osascript_launch_app":
467
+ result = await osascriptLaunchApp(args?.app_name);
468
+ break;
469
+ case "osascript_get_frontmost_app":
470
+ result = await osascriptGetFrontmostApp();
471
+ break;
472
+
473
+ // Windows
474
+ case "osascript_get_windows":
475
+ result = await osascriptGetWindows(args?.app_name);
476
+ break;
477
+ case "osascript_set_window_bounds":
478
+ result = await osascriptSetWindowBounds(
479
+ args?.app_name,
480
+ args?.window_index,
481
+ args?.x,
482
+ args?.y,
483
+ args?.width,
484
+ args?.height
485
+ );
486
+ break;
487
+
488
+ // Keyboard
489
+ case "osascript_keystroke":
490
+ result = await osascriptKeystroke(args?.key, parseModifiers(args?.modifiers));
491
+ break;
492
+ case "osascript_key_code":
493
+ result = await osascriptKeyCode(args?.key_code, parseModifiers(args?.modifiers));
494
+ break;
495
+ case "osascript_type_text":
496
+ result = await osascriptTypeText(args?.text);
497
+ break;
498
+
499
+ // Safari
500
+ case "osascript_safari_get_url":
501
+ result = await osascriptSafariGetURL();
502
+ break;
503
+ case "osascript_safari_set_url":
504
+ result = await osascriptSafariSetURL(args?.url);
505
+ break;
506
+ case "osascript_safari_get_tabs":
507
+ result = await osascriptSafariGetTabs();
508
+ break;
509
+ case "osascript_safari_exec_js":
510
+ result = await osascriptSafariExecJS(args?.js_code);
511
+ break;
512
+
513
+ // Finder
514
+ case "osascript_finder_get_selection":
515
+ result = await osascriptFinderGetSelection();
516
+ break;
517
+ case "osascript_finder_open":
518
+ result = await osascriptFinderOpen(args?.path);
519
+ break;
520
+ case "osascript_finder_create_folder":
521
+ result = await osascriptFinderCreateFolder(args?.parent_path, args?.folder_name);
522
+ break;
523
+ case "osascript_finder_trash":
524
+ result = await osascriptFinderTrash(args?.path);
525
+ break;
526
+
527
+ // Display
528
+ case "osascript_get_displays":
529
+ result = await osascriptGetDisplays();
530
+ break;
531
+ case "osascript_get_resolution":
532
+ result = await osascriptGetResolution();
533
+ break;
534
+
535
+ // Audio
536
+ case "osascript_say":
537
+ result = await osascriptSay(args?.text, args?.voice, args?.rate);
538
+ break;
539
+ case "osascript_beep":
540
+ result = await osascriptBeep(args?.count || 1);
541
+ break;
542
+
543
+ // Date/Time
544
+ case "osascript_get_datetime":
545
+ result = await osascriptGetDateTime();
546
+ break;
547
+
548
+ default:
549
+ return Response.json({ error: `Unknown tool: ${tool}` }, { status: 400 });
550
+ }
551
+
552
+ return Response.json({ result });
553
+ } catch (error) {
554
+ return Response.json({ error: String(error) }, { status: 500 });
555
+ }
556
+ }
557
+ }
558
+
559
+ return Response.json({ error: "Not found" }, { status: 404 });
560
+ },
561
+ });
562
+
563
+ console.log(`osascript MCP Server running on port ${MCP_PORT}`);
564
+ console.log(` Health: http://localhost:${MCP_PORT}/health`);
565
+ console.log(` MCP: http://localhost:${MCP_PORT}/mcp`);