@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,1284 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * osascript MCP Server (stdio protocol)
4
+ *
5
+ * Model Context Protocol server for macOS automation via osascript
6
+ * Uses stdio transport for Claude Code integration
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
+ minimizeWindow,
28
+ sendKeystroke,
29
+ sendKeyCode,
30
+ typeText,
31
+ KeyCodes,
32
+ getSafariURL,
33
+ setSafariURL,
34
+ getSafariTabTitles,
35
+ executeSafariJS,
36
+ getFinderSelection,
37
+ openFinderWindow,
38
+ createFolder,
39
+ moveToTrash,
40
+ getDisplayInfo,
41
+ getScreenResolution,
42
+ sayText,
43
+ beep,
44
+ getSystemDateTime,
45
+ isMacOS,
46
+ ensureMacOS,
47
+ } from "../index.js";
48
+
49
+ import type {
50
+ OsascriptResult,
51
+ NotificationOptions,
52
+ DialogOptions,
53
+ KeystrokeOptions,
54
+ KeyCodeOptions,
55
+ } from "../types.js";
56
+
57
+ // Action Events System
58
+ import {
59
+ eventBus,
60
+ createEvent,
61
+ ActionEvent,
62
+ EventType,
63
+ startAppMonitor,
64
+ startClipboardMonitor,
65
+ stopMonitor,
66
+ stopAllMonitors,
67
+ registerEventListener,
68
+ unregisterEventListener,
69
+ listEventListeners,
70
+ toggleListener,
71
+ getEventHistory,
72
+ clearEventHistory,
73
+ sendEventWebhook,
74
+ emitLifecycleEvent,
75
+ getActiveMonitorCount,
76
+ getActiveListenerCount,
77
+ } from "./events.js";
78
+
79
+ // ==============
80
+ // MCP Protocol Types
81
+ // ==============
82
+
83
+ interface JSONRPCMessage {
84
+ jsonrpc: "2.0";
85
+ id?: number | string;
86
+ method?: string;
87
+ params?: any;
88
+ result?: any;
89
+ error?: {
90
+ code: number;
91
+ message: string;
92
+ data?: any;
93
+ };
94
+ }
95
+
96
+ interface MCPTool {
97
+ name: string;
98
+ description: string;
99
+ inputSchema: {
100
+ type: "object";
101
+ properties: Record<
102
+ string,
103
+ {
104
+ type: string;
105
+ description: string;
106
+ enum?: string[];
107
+ default?: any;
108
+ }
109
+ >;
110
+ required: string[];
111
+ };
112
+ }
113
+
114
+ // ==============
115
+ // Tool Implementations
116
+ // ==============
117
+
118
+ async function osascriptExecute(script: string, language: "applescript" | "javascript" = "applescript"): Promise<string> {
119
+ const result = language === "javascript" ? await executeJXA(script) : await executeAppleScript(script);
120
+ return JSON.stringify(result, null, 2);
121
+ }
122
+
123
+ async function osascriptNotification(
124
+ message: string,
125
+ title: string = "Notification",
126
+ subtitle?: string,
127
+ soundName?: string
128
+ ): Promise<string> {
129
+ const options: NotificationOptions = { message, title, subtitle, soundName };
130
+ const result = await displayNotification(options);
131
+ return result.success ? `Notification sent: ${title}` : `Error: ${result.stderr}`;
132
+ }
133
+
134
+ async function osascriptDialog(
135
+ message: string,
136
+ title?: string,
137
+ buttons?: string[],
138
+ defaultButton?: number,
139
+ icon?: "note" | "caution" | "stop"
140
+ ): Promise<string> {
141
+ const options: DialogOptions = { message, title, buttons, defaultButton, icon };
142
+ try {
143
+ const result = await displayDialog(options);
144
+ return `Button returned: ${result.buttonReturned}${result.gaveUp ? " (timed out)" : ""}`;
145
+ } catch (error) {
146
+ return `Error: ${error}`;
147
+ }
148
+ }
149
+
150
+ async function osascriptGetVolume(): Promise<string> {
151
+ const info = await getVolume();
152
+ return `Volume: ${info.volume}%${info.muted ? " (muted)" : ""}`;
153
+ }
154
+
155
+ async function osascriptSetVolume(volume: number, muted?: boolean): Promise<string> {
156
+ const result = await setVolume(volume, muted);
157
+ return result.success ? `Volume set to ${volume}%` : `Error: ${result.stderr}`;
158
+ }
159
+
160
+ async function osascriptGetClipboard(): Promise<string> {
161
+ try {
162
+ const content = await getClipboard();
163
+ return content || "(clipboard is empty)";
164
+ } catch (error) {
165
+ return `Error: ${error}`;
166
+ }
167
+ }
168
+
169
+ async function osascriptSetClipboard(text: string): Promise<string> {
170
+ const result = await setClipboard(text);
171
+ return result.success ? "Clipboard updated" : `Error: ${result.stderr}`;
172
+ }
173
+
174
+ async function osascriptClearClipboard(): Promise<string> {
175
+ const result = await clearClipboard();
176
+ return result.success ? "Clipboard cleared" : `Error: ${result.stderr}`;
177
+ }
178
+
179
+ async function osascriptListApps(): Promise<string> {
180
+ const apps = await listRunningApplications();
181
+ const lines = ["Running Applications:", "=".repeat(40), ""];
182
+ for (const app of apps) {
183
+ lines.push(`- ${app.name}`);
184
+ }
185
+ return lines.join("\n");
186
+ }
187
+
188
+ async function osascriptGetAppInfo(appName: string): Promise<string> {
189
+ const info = await getApplicationInfo(appName);
190
+ if (!info.running) {
191
+ return `Application "${appName}" is not running`;
192
+ }
193
+ const lines = [
194
+ `Application: ${info.name}`,
195
+ "=".repeat(40),
196
+ `Running: ${info.running}`,
197
+ info.path ? `Path: ${info.path}` : "",
198
+ info.processId ? `PID: ${info.processId}` : "",
199
+ info.windowCount !== undefined ? `Windows: ${info.windowCount}` : "",
200
+ ].filter(Boolean);
201
+ return lines.join("\n");
202
+ }
203
+
204
+ async function osascriptActivateApp(appName: string): Promise<string> {
205
+ const result = await activateApplication(appName);
206
+ return result.success ? `Activated ${appName}` : `Error: ${result.stderr}`;
207
+ }
208
+
209
+ async function osascriptQuitApp(appName: string, force: boolean = false): Promise<string> {
210
+ const result = await quitApplication(appName, force);
211
+ return result.success ? `Quit ${appName}` : `Error: ${result.stderr}`;
212
+ }
213
+
214
+ async function osascriptLaunchApp(appName: string): Promise<string> {
215
+ const result = await launchApplication(appName);
216
+ return result.success ? `Launched ${appName}` : `Error: ${result.stderr}`;
217
+ }
218
+
219
+ async function osascriptGetFrontmostApp(): Promise<string> {
220
+ const appName = await getFrontmostApplication();
221
+ return `Frontmost application: ${appName}`;
222
+ }
223
+
224
+ async function osascriptGetWindows(appName: string): Promise<string> {
225
+ const windows = await getApplicationWindows(appName);
226
+ if (windows.length === 0) {
227
+ return `No windows for ${appName}`;
228
+ }
229
+ const lines = [`Windows for ${appName}:`, "=".repeat(40), ""];
230
+ for (const win of windows) {
231
+ lines.push(`${win.index}. ${win.name || "(unnamed)"} (ID: ${win.id})`);
232
+ }
233
+ return lines.join("\n");
234
+ }
235
+
236
+ async function osascriptSetWindowBounds(
237
+ appName: string,
238
+ windowIndex: number,
239
+ x: number,
240
+ y: number,
241
+ width: number,
242
+ height: number
243
+ ): Promise<string> {
244
+ const result = await setWindowBounds(appName, windowIndex, { x, y, width, height });
245
+ return result.success
246
+ ? `Window ${windowIndex} bounds set to ${width}x${height} at (${x}, ${y})`
247
+ : `Error: ${result.stderr}`;
248
+ }
249
+
250
+ async function osascriptKeystroke(
251
+ key: string,
252
+ modifiers: ("command" | "shift" | "option" | "control")[] = []
253
+ ): Promise<string> {
254
+ const options: KeystrokeOptions = { key, modifiers };
255
+ const result = await sendKeystroke(options);
256
+ const modStr = modifiers.length > 0 ? ` with ${modifiers.join("+")}` : "";
257
+ return result.success ? `Sent keystroke: ${key}${modStr}` : `Error: ${result.stderr}`;
258
+ }
259
+
260
+ async function osascriptKeyCode(
261
+ keyCode: number,
262
+ modifiers: ("command" | "shift" | "option" | "control")[] = []
263
+ ): Promise<string> {
264
+ const options: KeyCodeOptions = { keyCode, modifiers };
265
+ const result = await sendKeyCode(options);
266
+ const modStr = modifiers.length > 0 ? ` with ${modifiers.join("+")}` : "";
267
+ return result.success ? `Sent key code: ${keyCode}${modStr}` : `Error: ${result.stderr}`;
268
+ }
269
+
270
+ async function osascriptTypeText(text: string): Promise<string> {
271
+ const result = await typeText(text);
272
+ return result.success ? `Typed: ${text}` : `Error: ${result.stderr}`;
273
+ }
274
+
275
+ async function osascriptSafariGetURL(): Promise<string> {
276
+ try {
277
+ const url = await getSafariURL();
278
+ return `Current Safari URL: ${url}`;
279
+ } catch (error) {
280
+ return `Error: ${error}`;
281
+ }
282
+ }
283
+
284
+ async function osascriptSafariSetURL(url: string): Promise<string> {
285
+ const result = await setSafariURL(url);
286
+ return result.success ? `Navigated to: ${url}` : `Error: ${result.stderr}`;
287
+ }
288
+
289
+ async function osascriptSafariGetTabs(): Promise<string> {
290
+ const titles = await getSafariTabTitles();
291
+ if (titles.length === 0) {
292
+ return "No Safari tabs";
293
+ }
294
+ const lines = ["Safari Tabs:", "=".repeat(40), ""];
295
+ titles.forEach((title, i) => lines.push(`${i + 1}. ${title}`));
296
+ return lines.join("\n");
297
+ }
298
+
299
+ async function osascriptSafariExecJS(jsCode: string): Promise<string> {
300
+ try {
301
+ const result = await executeSafariJS(jsCode);
302
+ return result || "(no output)";
303
+ } catch (error) {
304
+ return `Error: ${error}`;
305
+ }
306
+ }
307
+
308
+ async function osascriptFinderGetSelection(): Promise<string> {
309
+ const items = await getFinderSelection();
310
+ if (items.length === 0) {
311
+ return "No Finder selection";
312
+ }
313
+ const lines = ["Finder Selection:", "=".repeat(40), ""];
314
+ for (const item of items) {
315
+ lines.push(`- ${item.name} (${item.type})`);
316
+ lines.push(` ${item.path}`);
317
+ }
318
+ return lines.join("\n");
319
+ }
320
+
321
+ async function osascriptFinderOpen(path: string): Promise<string> {
322
+ const result = await openFinderWindow(path);
323
+ return result.success ? `Opened Finder at: ${path}` : `Error: ${result.stderr}`;
324
+ }
325
+
326
+ async function osascriptFinderCreateFolder(parentPath: string, folderName: string): Promise<string> {
327
+ const result = await createFolder(parentPath, folderName);
328
+ return result.success ? `Created folder: ${folderName}` : `Error: ${result.stderr}`;
329
+ }
330
+
331
+ async function osascriptFinderTrash(path: string): Promise<string> {
332
+ const result = await moveToTrash(path);
333
+ return result.success ? `Moved to trash: ${path}` : `Error: ${result.stderr}`;
334
+ }
335
+
336
+ async function osascriptGetDisplays(): Promise<string> {
337
+ const displays = await getDisplayInfo();
338
+ const lines = ["Display Information:", "=".repeat(40), ""];
339
+ for (const display of displays) {
340
+ lines.push(`Display ${display.id}: ${display.width}x${display.height}${display.main ? " (main)" : ""}`);
341
+ }
342
+ return lines.join("\n");
343
+ }
344
+
345
+ async function osascriptGetResolution(): Promise<string> {
346
+ const { width, height } = await getScreenResolution();
347
+ return `Screen resolution: ${width}x${height}`;
348
+ }
349
+
350
+ async function osascriptSay(text: string, voice?: string, rate?: number): Promise<string> {
351
+ const result = await sayText(text, voice, rate);
352
+ return result.success ? `Said: "${text}"` : `Error: ${result.stderr}`;
353
+ }
354
+
355
+ async function osascriptBeep(count: number = 1): Promise<string> {
356
+ const result = await beep(count);
357
+ return result.success ? `Beeped ${count} time(s)` : `Error: ${result.stderr}`;
358
+ }
359
+
360
+ async function osascriptGetDateTime(): Promise<string> {
361
+ const dateTime = await getSystemDateTime();
362
+ return `System date/time: ${dateTime}`;
363
+ }
364
+
365
+ // Claude Code session spawning
366
+ async function osascriptSpawnClaudeSession(
367
+ sessionId: string,
368
+ workingDir?: string,
369
+ dopplerProject: string = "seed",
370
+ dopplerConfig: string = "prd"
371
+ ): Promise<string> {
372
+ const cdCmd = workingDir ? `cd ${workingDir} && ` : "";
373
+ // NO_COLOR=1 and TERM=dumb disable colors
374
+ const script = `tell application "Terminal" to do script "${cdCmd}unset CLAUDECODE && NO_COLOR=1 TERM=dumb doppler run --project ${dopplerProject} --config ${dopplerConfig} --command \\\"claude -r ${sessionId}\\\""`;
375
+
376
+ const result = await executeAppleScript(script);
377
+ if (result.success) {
378
+ return `Spawned Claude Code session ${sessionId} in ${result.stdout}`;
379
+ }
380
+ return `Error: ${result.stderr}`;
381
+ }
382
+
383
+ async function osascriptSpawnClaudeSessionsBatch(
384
+ sessionIdsStr: string,
385
+ workingDir?: string,
386
+ dopplerProject: string = "seed",
387
+ dopplerConfig: string = "prd"
388
+ ): Promise<string> {
389
+ const sessionIds = sessionIdsStr.split(",").map((s) => s.trim()).filter(Boolean);
390
+ const cdCmd = workingDir ? `cd ${workingDir} && ` : "";
391
+
392
+ const results: string[] = [];
393
+ for (const sessionId of sessionIds) {
394
+ // NO_COLOR=1 and TERM=dumb disable colors
395
+ const script = `tell application "Terminal" to do script "${cdCmd}unset CLAUDECODE && NO_COLOR=1 TERM=dumb doppler run --project ${dopplerProject} --config ${dopplerConfig} --command \\\"claude -r ${sessionId}\\\""`;
396
+ const result = await executeAppleScript(script);
397
+ results.push(`${sessionId.slice(0, 8)}: ${result.success ? result.stdout : result.stderr}`);
398
+ }
399
+
400
+ return `Spawned ${sessionIds.length} sessions:\n${results.join("\n")}`;
401
+ }
402
+
403
+ // ==============
404
+ // Tool Definitions
405
+ // ==============
406
+
407
+ const TOOLS: MCPTool[] = [
408
+ // Core
409
+ {
410
+ name: "osascript_execute",
411
+ description: "Execute raw AppleScript code",
412
+ inputSchema: {
413
+ type: "object",
414
+ properties: {
415
+ script: { type: "string", description: "AppleScript code to execute" },
416
+ language: {
417
+ type: "string",
418
+ description: "Script language",
419
+ enum: ["applescript", "javascript"],
420
+ default: "applescript",
421
+ },
422
+ },
423
+ required: ["script"],
424
+ },
425
+ },
426
+ // System
427
+ {
428
+ name: "osascript_display_notification",
429
+ description: "Display a macOS notification",
430
+ inputSchema: {
431
+ type: "object",
432
+ properties: {
433
+ message: { type: "string", description: "Notification body text" },
434
+ title: { type: "string", description: "Notification title", default: "Notification" },
435
+ subtitle: { type: "string", description: "Notification subtitle" },
436
+ sound_name: { type: "string", description: "Sound name (e.g., 'Ping', 'Glass')" },
437
+ },
438
+ required: ["message"],
439
+ },
440
+ },
441
+ {
442
+ name: "osascript_display_dialog",
443
+ description: "Display a dialog with buttons",
444
+ inputSchema: {
445
+ type: "object",
446
+ properties: {
447
+ message: { type: "string", description: "Dialog message" },
448
+ title: { type: "string", description: "Dialog title" },
449
+ buttons: {
450
+ type: "string",
451
+ description: "Comma-separated button labels (e.g., 'OK,Cancel')",
452
+ },
453
+ default_button: { type: "number", description: "Default button index (1-based)" },
454
+ icon: {
455
+ type: "string",
456
+ description: "Dialog icon",
457
+ enum: ["note", "caution", "stop"],
458
+ },
459
+ },
460
+ required: ["message"],
461
+ },
462
+ },
463
+ // Volume
464
+ {
465
+ name: "osascript_get_volume",
466
+ description: "Get system volume level",
467
+ inputSchema: {
468
+ type: "object",
469
+ properties: {},
470
+ required: [],
471
+ },
472
+ },
473
+ {
474
+ name: "osascript_set_volume",
475
+ description: "Set system volume level (0-100)",
476
+ inputSchema: {
477
+ type: "object",
478
+ properties: {
479
+ volume: { type: "number", description: "Volume level 0-100" },
480
+ muted: { type: "boolean", description: "Whether to mute" },
481
+ },
482
+ required: ["volume"],
483
+ },
484
+ },
485
+ // Clipboard
486
+ {
487
+ name: "osascript_get_clipboard",
488
+ description: "Get clipboard contents",
489
+ inputSchema: {
490
+ type: "object",
491
+ properties: {},
492
+ required: [],
493
+ },
494
+ },
495
+ {
496
+ name: "osascript_set_clipboard",
497
+ description: "Set clipboard contents",
498
+ inputSchema: {
499
+ type: "object",
500
+ properties: {
501
+ text: { type: "string", description: "Text to copy to clipboard" },
502
+ },
503
+ required: ["text"],
504
+ },
505
+ },
506
+ {
507
+ name: "osascript_clear_clipboard",
508
+ description: "Clear clipboard contents",
509
+ inputSchema: {
510
+ type: "object",
511
+ properties: {},
512
+ required: [],
513
+ },
514
+ },
515
+ // Applications
516
+ {
517
+ name: "osascript_list_apps",
518
+ description: "List all running applications",
519
+ inputSchema: {
520
+ type: "object",
521
+ properties: {},
522
+ required: [],
523
+ },
524
+ },
525
+ {
526
+ name: "osascript_get_app_info",
527
+ description: "Get detailed information about an application",
528
+ inputSchema: {
529
+ type: "object",
530
+ properties: {
531
+ app_name: { type: "string", description: "Application name" },
532
+ },
533
+ required: ["app_name"],
534
+ },
535
+ },
536
+ {
537
+ name: "osascript_activate_app",
538
+ description: "Activate (bring to front) an application",
539
+ inputSchema: {
540
+ type: "object",
541
+ properties: {
542
+ app_name: { type: "string", description: "Application name to activate" },
543
+ },
544
+ required: ["app_name"],
545
+ },
546
+ },
547
+ {
548
+ name: "osascript_quit_app",
549
+ description: "Quit an application",
550
+ inputSchema: {
551
+ type: "object",
552
+ properties: {
553
+ app_name: { type: "string", description: "Application name to quit" },
554
+ force: { type: "boolean", description: "Force quit (kill -9)" },
555
+ },
556
+ required: ["app_name"],
557
+ },
558
+ },
559
+ {
560
+ name: "osascript_launch_app",
561
+ description: "Launch an application",
562
+ inputSchema: {
563
+ type: "object",
564
+ properties: {
565
+ app_name: { type: "string", description: "Application name to launch" },
566
+ },
567
+ required: ["app_name"],
568
+ },
569
+ },
570
+ {
571
+ name: "osascript_get_frontmost_app",
572
+ description: "Get the name of the frontmost (active) application",
573
+ inputSchema: {
574
+ type: "object",
575
+ properties: {},
576
+ required: [],
577
+ },
578
+ },
579
+ // Windows
580
+ {
581
+ name: "osascript_get_windows",
582
+ description: "Get window list for an application",
583
+ inputSchema: {
584
+ type: "object",
585
+ properties: {
586
+ app_name: { type: "string", description: "Application name" },
587
+ },
588
+ required: ["app_name"],
589
+ },
590
+ },
591
+ {
592
+ name: "osascript_set_window_bounds",
593
+ description: "Set window position and size",
594
+ inputSchema: {
595
+ type: "object",
596
+ properties: {
597
+ app_name: { type: "string", description: "Application name" },
598
+ window_index: { type: "number", description: "Window index (1-based)" },
599
+ x: { type: "number", description: "X position" },
600
+ y: { type: "number", description: "Y position" },
601
+ width: { type: "number", description: "Window width" },
602
+ height: { type: "number", description: "Window height" },
603
+ },
604
+ required: ["app_name", "window_index", "x", "y", "width", "height"],
605
+ },
606
+ },
607
+ // Keyboard
608
+ {
609
+ name: "osascript_keystroke",
610
+ description: "Send keystroke with optional modifiers",
611
+ inputSchema: {
612
+ type: "object",
613
+ properties: {
614
+ key: { type: "string", description: "Key to press" },
615
+ modifiers: {
616
+ type: "string",
617
+ description: "Comma-separated modifiers: command, shift, option, control",
618
+ },
619
+ },
620
+ required: ["key"],
621
+ },
622
+ },
623
+ {
624
+ name: "osascript_key_code",
625
+ description: "Send key code (for special keys)",
626
+ inputSchema: {
627
+ type: "object",
628
+ properties: {
629
+ key_code: { type: "number", description: "Key code number (e.g., 36=Return, 48=Tab)" },
630
+ modifiers: {
631
+ type: "string",
632
+ description: "Comma-separated modifiers: command, shift, option, control",
633
+ },
634
+ },
635
+ required: ["key_code"],
636
+ },
637
+ },
638
+ {
639
+ name: "osascript_type_text",
640
+ description: "Type text (simulate keyboard input)",
641
+ inputSchema: {
642
+ type: "object",
643
+ properties: {
644
+ text: { type: "string", description: "Text to type" },
645
+ },
646
+ required: ["text"],
647
+ },
648
+ },
649
+ // Safari
650
+ {
651
+ name: "osascript_safari_get_url",
652
+ description: "Get current Safari URL",
653
+ inputSchema: {
654
+ type: "object",
655
+ properties: {},
656
+ required: [],
657
+ },
658
+ },
659
+ {
660
+ name: "osascript_safari_set_url",
661
+ description: "Navigate Safari to URL",
662
+ inputSchema: {
663
+ type: "object",
664
+ properties: {
665
+ url: { type: "string", description: "URL to navigate to" },
666
+ },
667
+ required: ["url"],
668
+ },
669
+ },
670
+ {
671
+ name: "osascript_safari_get_tabs",
672
+ description: "Get Safari tab titles",
673
+ inputSchema: {
674
+ type: "object",
675
+ properties: {},
676
+ required: [],
677
+ },
678
+ },
679
+ {
680
+ name: "osascript_safari_exec_js",
681
+ description: "Execute JavaScript in Safari",
682
+ inputSchema: {
683
+ type: "object",
684
+ properties: {
685
+ js_code: { type: "string", description: "JavaScript code to execute" },
686
+ },
687
+ required: ["js_code"],
688
+ },
689
+ },
690
+ // Finder
691
+ {
692
+ name: "osascript_finder_get_selection",
693
+ description: "Get currently selected files in Finder",
694
+ inputSchema: {
695
+ type: "object",
696
+ properties: {},
697
+ required: [],
698
+ },
699
+ },
700
+ {
701
+ name: "osascript_finder_open",
702
+ description: "Open Finder window at path",
703
+ inputSchema: {
704
+ type: "object",
705
+ properties: {
706
+ path: { type: "string", description: "Path to open" },
707
+ },
708
+ required: ["path"],
709
+ },
710
+ },
711
+ {
712
+ name: "osascript_finder_create_folder",
713
+ description: "Create new folder in Finder",
714
+ inputSchema: {
715
+ type: "object",
716
+ properties: {
717
+ parent_path: { type: "string", description: "Parent directory path" },
718
+ folder_name: { type: "string", description: "New folder name" },
719
+ },
720
+ required: ["parent_path", "folder_name"],
721
+ },
722
+ },
723
+ {
724
+ name: "osascript_finder_trash",
725
+ description: "Move file/folder to trash",
726
+ inputSchema: {
727
+ type: "object",
728
+ properties: {
729
+ path: { type: "string", description: "Path to move to trash" },
730
+ },
731
+ required: ["path"],
732
+ },
733
+ },
734
+ // Display
735
+ {
736
+ name: "osascript_get_displays",
737
+ description: "Get display information",
738
+ inputSchema: {
739
+ type: "object",
740
+ properties: {},
741
+ required: [],
742
+ },
743
+ },
744
+ {
745
+ name: "osascript_get_resolution",
746
+ description: "Get main screen resolution",
747
+ inputSchema: {
748
+ type: "object",
749
+ properties: {},
750
+ required: [],
751
+ },
752
+ },
753
+ // Audio
754
+ {
755
+ name: "osascript_say",
756
+ description: "Say text using system voice",
757
+ inputSchema: {
758
+ type: "object",
759
+ properties: {
760
+ text: { type: "string", description: "Text to speak" },
761
+ voice: { type: "string", description: "Voice name (e.g., 'Alex', 'Samantha')" },
762
+ rate: { type: "number", description: "Speaking rate" },
763
+ },
764
+ required: ["text"],
765
+ },
766
+ },
767
+ {
768
+ name: "osascript_beep",
769
+ description: "Play system beep",
770
+ inputSchema: {
771
+ type: "object",
772
+ properties: {
773
+ count: { type: "number", description: "Number of beeps" },
774
+ },
775
+ required: [],
776
+ },
777
+ },
778
+ // Date/Time
779
+ {
780
+ name: "osascript_get_datetime",
781
+ description: "Get current system date/time",
782
+ inputSchema: {
783
+ type: "object",
784
+ properties: {},
785
+ required: [],
786
+ },
787
+ },
788
+ // Claude Code
789
+ {
790
+ name: "osascript_spawn_claude_session",
791
+ description: "Open new Terminal tab with Claude Code resume session (auto-unsets CLAUDECODE to bypass nested session check)",
792
+ inputSchema: {
793
+ type: "object",
794
+ properties: {
795
+ session_id: { type: "string", description: "Claude Code session ID to resume" },
796
+ working_dir: { type: "string", description: "Working directory (default: current directory)" },
797
+ doppler_project: { type: "string", description: "Doppler project name (default: seed)" },
798
+ doppler_config: { type: "string", description: "Doppler config (default: prd)" },
799
+ },
800
+ required: ["session_id"],
801
+ },
802
+ },
803
+ {
804
+ name: "osascript_spawn_claude_sessions_batch",
805
+ description: "Open multiple Terminal tabs with Claude Code resume sessions in parallel",
806
+ inputSchema: {
807
+ type: "object",
808
+ properties: {
809
+ session_ids: { type: "string", description: "Comma-separated list of Claude Code session IDs" },
810
+ working_dir: { type: "string", description: "Working directory (default: current directory)" },
811
+ doppler_project: { type: "string", description: "Doppler project name (default: seed)" },
812
+ doppler_config: { type: "string", description: "Doppler config (default: prd)" },
813
+ },
814
+ required: ["session_ids"],
815
+ },
816
+ },
817
+ // ==============
818
+ // Action Events
819
+ // ==============
820
+ {
821
+ name: "osascript_event_start_app_monitor",
822
+ description: "Start monitoring application switches (fires event when frontmost app changes)",
823
+ inputSchema: {
824
+ type: "object",
825
+ properties: {},
826
+ required: [],
827
+ },
828
+ },
829
+ {
830
+ name: "osascript_event_start_clipboard_monitor",
831
+ description: "Start monitoring clipboard changes (fires event when clipboard content changes)",
832
+ inputSchema: {
833
+ type: "object",
834
+ properties: {},
835
+ required: [],
836
+ },
837
+ },
838
+ {
839
+ name: "osascript_event_stop_monitor",
840
+ description: "Stop a specific monitor by ID",
841
+ inputSchema: {
842
+ type: "object",
843
+ properties: {
844
+ monitor_id: { type: "string", description: "Monitor ID to stop" },
845
+ },
846
+ required: ["monitor_id"],
847
+ },
848
+ },
849
+ {
850
+ name: "osascript_event_stop_all_monitors",
851
+ description: "Stop all active monitors",
852
+ inputSchema: {
853
+ type: "object",
854
+ properties: {},
855
+ required: [],
856
+ },
857
+ },
858
+ {
859
+ name: "osascript_event_register_listener",
860
+ description: "Register an event listener with optional webhook callback",
861
+ inputSchema: {
862
+ type: "object",
863
+ properties: {
864
+ event_type: {
865
+ type: "string",
866
+ description: "Event type to listen for (e.g., 'keystroke', 'app_activate', '*' for all)",
867
+ },
868
+ webhook_url: { type: "string", description: "Webhook URL to POST events to" },
869
+ },
870
+ required: ["event_type"],
871
+ },
872
+ },
873
+ {
874
+ name: "osascript_event_unregister_listener",
875
+ description: "Unregister an event listener",
876
+ inputSchema: {
877
+ type: "object",
878
+ properties: {
879
+ listener_id: { type: "string", description: "Listener ID to remove" },
880
+ },
881
+ required: ["listener_id"],
882
+ },
883
+ },
884
+ {
885
+ name: "osascript_event_list_listeners",
886
+ description: "List all registered event listeners",
887
+ inputSchema: {
888
+ type: "object",
889
+ properties: {},
890
+ required: [],
891
+ },
892
+ },
893
+ {
894
+ name: "osascript_event_get_history",
895
+ description: "Get event history, optionally filtered by type or time",
896
+ inputSchema: {
897
+ type: "object",
898
+ properties: {
899
+ since: { type: "number", description: "Unix timestamp to get events since" },
900
+ event_type: { type: "string", description: "Filter by event type" },
901
+ },
902
+ required: [],
903
+ },
904
+ },
905
+ {
906
+ name: "osascript_event_clear_history",
907
+ description: "Clear all event history",
908
+ inputSchema: {
909
+ type: "object",
910
+ properties: {},
911
+ required: [],
912
+ },
913
+ },
914
+ {
915
+ name: "osascript_event_send_webhook",
916
+ description: "Manually send an event to a webhook URL",
917
+ inputSchema: {
918
+ type: "object",
919
+ properties: {
920
+ url: { type: "string", description: "Webhook URL" },
921
+ event_type: { type: "string", description: "Event type" },
922
+ data: { type: "string", description: "JSON string of event data" },
923
+ },
924
+ required: ["url", "event_type"],
925
+ },
926
+ },
927
+ {
928
+ name: "osascript_event_emit",
929
+ description: "Emit a custom event to the event bus",
930
+ inputSchema: {
931
+ type: "object",
932
+ properties: {
933
+ event_type: { type: "string", description: "Event type to emit" },
934
+ source: { type: "string", description: "Source identifier" },
935
+ data: { type: "string", description: "JSON string of event data" },
936
+ },
937
+ required: ["event_type", "source"],
938
+ },
939
+ },
940
+ {
941
+ name: "osascript_event_status",
942
+ description: "Get status of event system (active monitors, listeners, event count)",
943
+ inputSchema: {
944
+ type: "object",
945
+ properties: {},
946
+ required: [],
947
+ },
948
+ },
949
+ ];
950
+
951
+ // ==============
952
+ // MCP Server
953
+ // ==============
954
+
955
+ function parseModifiers(modifiersStr?: string): ("command" | "shift" | "option" | "control")[] {
956
+ if (!modifiersStr) return [];
957
+ return modifiersStr
958
+ .split(",")
959
+ .map((m) => m.trim().toLowerCase() as "command" | "shift" | "option" | "control")
960
+ .filter((m) => ["command", "shift", "option", "control"].includes(m));
961
+ }
962
+
963
+ async function handleToolCall(name: string, args: any): Promise<string> {
964
+ // Check macOS availability
965
+ if (!isMacOS()) {
966
+ throw new Error("osascript MCP only works on macOS");
967
+ }
968
+
969
+ switch (name) {
970
+ // Core
971
+ case "osascript_execute":
972
+ return await osascriptExecute(args.script, args.language || "applescript");
973
+
974
+ // System
975
+ case "osascript_display_notification":
976
+ return await osascriptNotification(args.message, args.title, args.subtitle, args.sound_name);
977
+ case "osascript_display_dialog":
978
+ return await osascriptDialog(
979
+ args.message,
980
+ args.title,
981
+ args.buttons?.split(","),
982
+ args.default_button,
983
+ args.icon
984
+ );
985
+
986
+ // Volume
987
+ case "osascript_get_volume":
988
+ return await osascriptGetVolume();
989
+ case "osascript_set_volume":
990
+ return await osascriptSetVolume(args.volume, args.muted);
991
+
992
+ // Clipboard
993
+ case "osascript_get_clipboard":
994
+ return await osascriptGetClipboard();
995
+ case "osascript_set_clipboard":
996
+ return await osascriptSetClipboard(args.text);
997
+ case "osascript_clear_clipboard":
998
+ return await osascriptClearClipboard();
999
+
1000
+ // Applications
1001
+ case "osascript_list_apps":
1002
+ return await osascriptListApps();
1003
+ case "osascript_get_app_info":
1004
+ return await osascriptGetAppInfo(args.app_name);
1005
+ case "osascript_activate_app":
1006
+ return await osascriptActivateApp(args.app_name);
1007
+ case "osascript_quit_app":
1008
+ return await osascriptQuitApp(args.app_name, args.force);
1009
+ case "osascript_launch_app":
1010
+ return await osascriptLaunchApp(args.app_name);
1011
+ case "osascript_get_frontmost_app":
1012
+ return await osascriptGetFrontmostApp();
1013
+
1014
+ // Windows
1015
+ case "osascript_get_windows":
1016
+ return await osascriptGetWindows(args.app_name);
1017
+ case "osascript_set_window_bounds":
1018
+ return await osascriptSetWindowBounds(
1019
+ args.app_name,
1020
+ args.window_index,
1021
+ args.x,
1022
+ args.y,
1023
+ args.width,
1024
+ args.height
1025
+ );
1026
+
1027
+ // Keyboard
1028
+ case "osascript_keystroke":
1029
+ return await osascriptKeystroke(args.key, parseModifiers(args.modifiers));
1030
+ case "osascript_key_code":
1031
+ return await osascriptKeyCode(args.key_code, parseModifiers(args.modifiers));
1032
+ case "osascript_type_text":
1033
+ return await osascriptTypeText(args.text);
1034
+
1035
+ // Safari
1036
+ case "osascript_safari_get_url":
1037
+ return await osascriptSafariGetURL();
1038
+ case "osascript_safari_set_url":
1039
+ return await osascriptSafariSetURL(args.url);
1040
+ case "osascript_safari_get_tabs":
1041
+ return await osascriptSafariGetTabs();
1042
+ case "osascript_safari_exec_js":
1043
+ return await osascriptSafariExecJS(args.js_code);
1044
+
1045
+ // Finder
1046
+ case "osascript_finder_get_selection":
1047
+ return await osascriptFinderGetSelection();
1048
+ case "osascript_finder_open":
1049
+ return await osascriptFinderOpen(args.path);
1050
+ case "osascript_finder_create_folder":
1051
+ return await osascriptFinderCreateFolder(args.parent_path, args.folder_name);
1052
+ case "osascript_finder_trash":
1053
+ return await osascriptFinderTrash(args.path);
1054
+
1055
+ // Display
1056
+ case "osascript_get_displays":
1057
+ return await osascriptGetDisplays();
1058
+ case "osascript_get_resolution":
1059
+ return await osascriptGetResolution();
1060
+
1061
+ // Audio
1062
+ case "osascript_say":
1063
+ return await osascriptSay(args.text, args.voice, args.rate);
1064
+ case "osascript_beep":
1065
+ return await osascriptBeep(args.count || 1);
1066
+
1067
+ // Date/Time
1068
+ case "osascript_get_datetime":
1069
+ return await osascriptGetDateTime();
1070
+
1071
+ // Claude Code
1072
+ case "osascript_spawn_claude_session":
1073
+ return await osascriptSpawnClaudeSession(
1074
+ args.session_id,
1075
+ args.working_dir,
1076
+ args.doppler_project,
1077
+ args.doppler_config
1078
+ );
1079
+ case "osascript_spawn_claude_sessions_batch":
1080
+ return await osascriptSpawnClaudeSessionsBatch(
1081
+ args.session_ids,
1082
+ args.working_dir,
1083
+ args.doppler_project,
1084
+ args.doppler_config
1085
+ );
1086
+
1087
+ // ==============
1088
+ // Action Events
1089
+ // ==============
1090
+ case "osascript_event_start_app_monitor": {
1091
+ const monitorId = await startAppMonitor();
1092
+ return `Started app monitor: ${monitorId}`;
1093
+ }
1094
+ case "osascript_event_start_clipboard_monitor": {
1095
+ const monitorId = await startClipboardMonitor();
1096
+ return `Started clipboard monitor: ${monitorId}`;
1097
+ }
1098
+ case "osascript_event_stop_monitor": {
1099
+ const stopped = stopMonitor(args.monitor_id);
1100
+ return stopped ? `Stopped monitor: ${args.monitor_id}` : `Monitor not found: ${args.monitor_id}`;
1101
+ }
1102
+ case "osascript_event_stop_all_monitors": {
1103
+ stopAllMonitors();
1104
+ return "Stopped all monitors";
1105
+ }
1106
+ case "osascript_event_register_listener": {
1107
+ const listenerId = registerEventListener(args.event_type as EventType | "*", {
1108
+ webhookUrl: args.webhook_url,
1109
+ });
1110
+ return `Registered listener: ${listenerId}`;
1111
+ }
1112
+ case "osascript_event_unregister_listener": {
1113
+ const removed = unregisterEventListener(args.listener_id);
1114
+ return removed ? `Unregistered listener: ${args.listener_id}` : `Listener not found: ${args.listener_id}`;
1115
+ }
1116
+ case "osascript_event_list_listeners": {
1117
+ const listeners = listEventListeners();
1118
+ if (listeners.length === 0) return "No listeners registered";
1119
+ const lines = ["Event Listeners:", "=".repeat(40), ""];
1120
+ for (const l of listeners) {
1121
+ lines.push(`${l.id}: ${l.eventType} (${l.active ? "active" : "inactive"})${l.webhookUrl ? ` -> ${l.webhookUrl}` : ""}`);
1122
+ }
1123
+ return lines.join("\n");
1124
+ }
1125
+ case "osascript_event_get_history": {
1126
+ const events = getEventHistory(args.since, args.event_type as EventType);
1127
+ if (events.length === 0) return "No events in history";
1128
+ return JSON.stringify(events, null, 2);
1129
+ }
1130
+ case "osascript_event_clear_history": {
1131
+ clearEventHistory();
1132
+ return "Event history cleared";
1133
+ }
1134
+ case "osascript_event_send_webhook": {
1135
+ const data = args.data ? JSON.parse(args.data) : {};
1136
+ const event = createEvent(args.event_type as EventType, "manual", data);
1137
+ const result = await sendEventWebhook(args.url, event);
1138
+ return result.success ? `Webhook sent: ${event.id}` : `Webhook failed: ${result.error}`;
1139
+ }
1140
+ case "osascript_event_emit": {
1141
+ const data = args.data ? JSON.parse(args.data) : {};
1142
+ const event = createEvent(args.event_type as EventType, args.source, data);
1143
+ eventBus.emit(event);
1144
+ return `Emitted event: ${event.id}`;
1145
+ }
1146
+ case "osascript_event_status": {
1147
+ const status = {
1148
+ activeMonitors: getActiveMonitorCount(),
1149
+ activeListeners: getActiveListenerCount(),
1150
+ historySize: getEventHistory().length,
1151
+ };
1152
+ return `Event System Status:\n${JSON.stringify(status, null, 2)}`;
1153
+ }
1154
+
1155
+ default:
1156
+ throw new Error(`Unknown tool: ${name}`);
1157
+ }
1158
+ }
1159
+
1160
+ function sendMessage(message: JSONRPCMessage): void {
1161
+ console.log(JSON.stringify(message));
1162
+ }
1163
+
1164
+ async function processMessage(message: JSONRPCMessage): Promise<void> {
1165
+ const { id, method, params } = message;
1166
+
1167
+ try {
1168
+ switch (method) {
1169
+ case "initialize":
1170
+ sendMessage({
1171
+ jsonrpc: "2.0",
1172
+ id,
1173
+ result: {
1174
+ protocolVersion: "2024-11-05",
1175
+ capabilities: {
1176
+ tools: {},
1177
+ },
1178
+ serverInfo: {
1179
+ name: "osascript-mcp",
1180
+ version: "1.0.0",
1181
+ description: "macOS automation via osascript (AppleScript/JXA)",
1182
+ },
1183
+ },
1184
+ });
1185
+ break;
1186
+
1187
+ case "tools/list":
1188
+ sendMessage({
1189
+ jsonrpc: "2.0",
1190
+ id,
1191
+ result: {
1192
+ tools: TOOLS,
1193
+ },
1194
+ });
1195
+ break;
1196
+
1197
+ case "tools/call":
1198
+ const { name, arguments: args } = params;
1199
+ const result = await handleToolCall(name, args || {});
1200
+ sendMessage({
1201
+ jsonrpc: "2.0",
1202
+ id,
1203
+ result: {
1204
+ content: [
1205
+ {
1206
+ type: "text",
1207
+ text: result,
1208
+ },
1209
+ ],
1210
+ },
1211
+ });
1212
+ break;
1213
+
1214
+ case "shutdown":
1215
+ sendMessage({
1216
+ jsonrpc: "2.0",
1217
+ id,
1218
+ result: {},
1219
+ });
1220
+ process.exit(0);
1221
+ break;
1222
+
1223
+ default:
1224
+ sendMessage({
1225
+ jsonrpc: "2.0",
1226
+ id,
1227
+ error: {
1228
+ code: -32601,
1229
+ message: `Method not found: ${method}`,
1230
+ },
1231
+ });
1232
+ }
1233
+ } catch (error) {
1234
+ sendMessage({
1235
+ jsonrpc: "2.0",
1236
+ id,
1237
+ error: {
1238
+ code: -32603,
1239
+ message: `Internal error: ${error}`,
1240
+ },
1241
+ });
1242
+ }
1243
+ }
1244
+
1245
+ // ==============
1246
+ // Main Loop
1247
+ // ==============
1248
+
1249
+ async function main() {
1250
+ // Verify macOS
1251
+ if (!isMacOS()) {
1252
+ console.error("ERROR: osascript MCP only works on macOS");
1253
+ process.exit(1);
1254
+ }
1255
+
1256
+ const decoder = new TextDecoder();
1257
+ let buffer = "";
1258
+
1259
+ for await (const chunk of process.stdin) {
1260
+ buffer += decoder.decode(chunk);
1261
+
1262
+ const lines = buffer.split("\n");
1263
+ buffer = lines.pop() || "";
1264
+
1265
+ for (const line of lines) {
1266
+ if (!line.trim()) continue;
1267
+
1268
+ try {
1269
+ const message: JSONRPCMessage = JSON.parse(line);
1270
+ await processMessage(message);
1271
+ } catch (error) {
1272
+ sendMessage({
1273
+ jsonrpc: "2.0",
1274
+ error: {
1275
+ code: -32700,
1276
+ message: `Parse error: ${error}`,
1277
+ },
1278
+ });
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+
1284
+ main().catch(console.error);