@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.
- package/README.md +123 -0
- package/dist/index.js +574 -0
- package/dist/mcp/index.js +866 -0
- package/dist/mcp/stdio.js +1704 -0
- package/package.json +79 -0
- package/src/index.ts +844 -0
- package/src/mcp/events.ts +468 -0
- package/src/mcp/index.ts +565 -0
- package/src/mcp/stdio.ts +1284 -0
- package/src/types.ts +189 -0
package/src/mcp/stdio.ts
ADDED
|
@@ -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);
|