#
tokens: 29278/50000 20/20 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   └── workflows
│       ├── node.js.yml
│       └── release.yml
├── .gitignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── categories
│   │   ├── calendar.ts
│   │   ├── clipboard.ts
│   │   ├── finder.ts
│   │   ├── iterm.ts
│   │   ├── mail.ts
│   │   ├── messages.ts
│   │   ├── notes.ts
│   │   ├── notifications.ts
│   │   ├── pages.ts
│   │   ├── shortcuts.ts
│   │   └── system.ts
│   ├── framework.ts
│   ├── index.ts
│   └── types
│       └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
5 | /dist
6 | .DS_Store
7 | helps/*
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # applescript-mcp MCP Server
  2 | 
  3 | A Model Context Protocol server that enables LLM applications to interact with macOS through AppleScript.
  4 | This server provides a standardized interface for AI applications to control system functions, manage files, handle notifications, and more.
  5 | 
  6 | [![Node.js CI](https://github.com/joshrutkowski/applescript-mcp/actions/workflows/node.js.yml/badge.svg)](https://github.com/joshrutkowski/applescript-mcp/actions/workflows/node.js.yml)
  7 | 
  8 | <a href="https://glama.ai/mcp/servers/0t5gydjcqw"><img width="380" height="200" src="https://glama.ai/mcp/servers/0t5gydjcqw/badge" alt="applescript-mcp MCP server" /></a>
  9 | 
 10 | ## Features
 11 | 
 12 | - 🗓️ Calendar management (events, reminders)
 13 | - 📋 Clipboard operations
 14 | - 🔍 Finder integration
 15 | - 🔔 System notifications
 16 | - ⚙️ System controls (volume, dark mode, apps)
 17 | - 📟 iTerm terminal integration
 18 | - 📬 Mail (create new email, list emails, get email)
 19 | - 🔄 Shortcuts automation
 20 | - 💬 Messages (list chats, get messages, search messages, send a message)
 21 | - 🗒️ Notes (create formatted notes, list notes, search notes)
 22 | - 📄 Pages (create documents)
 23 | 
 24 | ### Planned Features
 25 | 
 26 | - 🧭 Safari (open in Safari, save page content, get selected page/tab)
 27 | - ✅ Reminders (create, get)
 28 | 
 29 | ## Prerequisites
 30 | 
 31 | - macOS 10.15 or later
 32 | - Node.js 18 or later
 33 | 
 34 | ## Available Categories
 35 | 
 36 | ### Calendar
 37 | 
 38 | | Command | Description           | Parameters                                          |
 39 | | ------- | --------------------- | --------------------------------------------------- |
 40 | | `add`   | Create calendar event | `title`, `startDate`, `endDate`, `calendar` (optional) |
 41 | | `list`  | List today's events   | None                                                |
 42 | 
 43 | #### Examples
 44 | 
 45 | ```
 46 | // Create a new calendar event
 47 | Create a calendar event titled "Team Meeting" starting tomorrow at 2pm for 1 hour
 48 | 
 49 | // List today's events
 50 | What events do I have scheduled for today?
 51 | ```
 52 | 
 53 | ### Clipboard
 54 | 
 55 | | Command           | Description            | Parameters |
 56 | | ----------------- | ---------------------- | ---------- |
 57 | | `set_clipboard`   | Copy to clipboard      | `content`  |
 58 | | `get_clipboard`   | Get clipboard contents | None       |
 59 | | `clear_clipboard` | Clear clipboard        | None       |
 60 | 
 61 | #### Examples
 62 | 
 63 | ```
 64 | // Copy text to clipboard
 65 | Copy "Remember to buy groceries" to my clipboard
 66 | 
 67 | // Get clipboard contents
 68 | What's currently in my clipboard?
 69 | 
 70 | // Clear clipboard
 71 | Clear my clipboard
 72 | ```
 73 | 
 74 | ### Finder
 75 | 
 76 | | Command              | Description        | Parameters                     |
 77 | | -------------------- | ------------------ | ------------------------------ |
 78 | | `get_selected_files` | Get selected files | None                           |
 79 | | `search_files`       | Search for files   | `query`, `location` (optional) |
 80 | | `quick_look`         | Preview file       | `path`                         |
 81 | 
 82 | #### Examples
 83 | 
 84 | ```
 85 | // Get selected files in Finder
 86 | What files do I currently have selected in Finder?
 87 | 
 88 | // Search for files
 89 | Find all PDF files in my Documents folder
 90 | 
 91 | // Preview a file
 92 | Show me a preview of ~/Documents/report.pdf
 93 | ```
 94 | 
 95 | ### Notifications
 96 | 
 97 | > Note: Sending notification requires that you enable notifications in System Settings > Notifications > Script Editor.
 98 | 
 99 | | Command                 | Description       | Parameters                             |
100 | | ----------------------- | ----------------- | -------------------------------------- |
101 | | `send_notification`     | Show notification | `title`, `message`, `sound` (optional) |
102 | | `toggle_do_not_disturb` | Toggle DND mode   | None                                   |
103 | 
104 | #### Examples
105 | 
106 | ```
107 | // Send a notification
108 | Send me a notification with the title "Reminder" and message "Time to take a break"
109 | 
110 | // Toggle Do Not Disturb
111 | Turn on Do Not Disturb mode
112 | ```
113 | 
114 | ### System
115 | 
116 | | Command             | Description       | Parameters                 |
117 | | ------------------- | ----------------- | -------------------------- |
118 | | `volume`            | Set system volume | `level` (0-100)            |
119 | | `get_frontmost_app` | Get active app    | None                       |
120 | | `launch_app`        | Open application  | `name`                     |
121 | | `quit_app`          | Close application | `name`, `force` (optional) |
122 | | `toggle_dark_mode`  | Toggle dark mode  | None                       |
123 | 
124 | #### Examples
125 | 
126 | ```
127 | // Set system volume
128 | Set my Mac's volume to 50%
129 | 
130 | // Get active application
131 | What app am I currently using?
132 | 
133 | // Launch an application
134 | Open Safari
135 | 
136 | // Quit an application
137 | Close Spotify
138 | 
139 | // Toggle dark mode
140 | Switch to dark mode
141 | ```
142 | 
143 | ### iTerm
144 | 
145 | | Command           | Description     | Parameters                        |
146 | | ----------------- | --------------- | --------------------------------- |
147 | | `paste_clipboard` | Paste to iTerm  | None                              |
148 | | `run`             | Execute command | `command`, `newWindow` (optional) |
149 | 
150 | #### Examples
151 | 
152 | ```
153 | // Paste clipboard to iTerm
154 | Paste my clipboard contents into iTerm
155 | 
156 | // Run a command in iTerm
157 | Run "ls -la" in iTerm
158 | 
159 | // Run a command in a new iTerm window
160 | Run "top" in a new iTerm window
161 | ```
162 | 
163 | ### Shortcuts
164 | 
165 | | Command          | Description                                | Parameters                                           |
166 | | ---------------- | ------------------------------------------ | ---------------------------------------------------- |
167 | | `run_shortcut`   | Run a shortcut                             | `name`, `input` (optional)                           |
168 | | `list_shortcuts` | List all available shortcuts               | `limit` (optional)                                   |
169 | 
170 | #### Examples
171 | 
172 | ```
173 | // List available shortcuts
174 | List all my available shortcuts
175 | 
176 | // List with limit
177 | Show me my top 5 shortcuts
178 | 
179 | // Run a shortcut
180 | Run my "Daily Note in Bear" shortcut
181 | 
182 | // Run a shortcut with input
183 | Run my "Add to-do" shortcut with input "Buy groceries"
184 | ```
185 | 
186 | ### Mail
187 | 
188 | | Command       | Description                      | Parameters                                                |
189 | | ------------- | -------------------------------- | --------------------------------------------------------- |
190 | | `create_email`| Create a new email in Mail.app   | `recipient`, `subject`, `body`                            |
191 | | `list_emails` | List emails from a mailbox       | `mailbox` (optional), `count` (optional), `unreadOnly` (optional) |
192 | | `get_email`   | Get a specific email by search   | `subject` (optional), `sender` (optional), `dateReceived` (optional), `mailbox` (optional), `account` (optional), `unreadOnly` (optional), `includeBody` (optional) |
193 | 
194 | #### Examples
195 | 
196 | ```
197 | // Create a new email
198 | Compose an email to [email protected] with subject "Meeting Tomorrow" and body "Hi John, Can we meet tomorrow at 2pm?"
199 | 
200 | // List emails
201 | Show me my 10 most recent unread emails
202 | 
203 | // Get a specific email
204 | Find the email from [email protected] about "Project Update"
205 | ```
206 | 
207 | ### Messages
208 | 
209 | | Command           | Description                                  | Parameters                                                |
210 | | ----------------- | -------------------------------------------- | --------------------------------------------------------- |
211 | | `list_chats`      | List available iMessage and SMS chats        | `includeParticipantDetails` (optional, default: false)    |
212 | | `get_messages`    | Get messages from the Messages app           | `limit` (optional, default: 100)                          |
213 | | `search_messages` | Search for messages containing specific text | `searchText`, `sender` (optional), `chatId` (optional), `limit` (optional, default: 50), `daysBack` (optional, default: 30) |
214 | | `compose_message` | Open Messages app with pre-filled message or auto-send   | `recipient` (required), `body` (optional), `auto` (optional, default: false) |
215 | 
216 | #### Examples
217 | 
218 | ```
219 | // List available chats
220 | Show me my recent message conversations
221 | 
222 | // Get recent messages
223 | Show me my last 20 messages
224 | 
225 | // Search messages
226 | Find messages containing "dinner plans" from John in the last week
227 | 
228 | // Compose a message
229 | Send a message to 555-123-4567 saying "I'll be there in 10 minutes"
230 | ```
231 | 
232 | ### Notes
233 | 
234 | | Command           | Description                                  | Parameters                                                |
235 | | ----------------- | -------------------------------------------- | --------------------------------------------------------- |
236 | | `create`          | Create a note with markdown-like formatting  | `title`, `content`, `format` (optional with formatting options) |
237 | | `createRawHtml`   | Create a note with direct HTML content       | `title`, `html`                                           |
238 | | `list`            | List notes, optionally from a specific folder| `folder` (optional)                                       |
239 | | `get`             | Get a specific note by title                 | `title`, `folder` (optional)                              |
240 | | `search`          | Search for notes containing specific text    | `query`, `folder` (optional), `limit` (optional, default: 5), `includeBody` (optional, default: true) |
241 | 
242 | #### Examples
243 | 
244 | ```
245 | // Create a new note with markdown formatting
246 | Create a note titled "Meeting Minutes" with content "# Discussion Points\n- Project timeline\n- Budget review\n- Next steps" and format headings and lists
247 | 
248 | // Create a note with HTML
249 | Create a note titled "Formatted Report" with HTML content "<h1>Quarterly Report</h1><p>Sales increased by <strong>15%</strong></p>"
250 | 
251 | // List notes
252 | Show me all my notes in the "Work" folder
253 | 
254 | // Get a specific note
255 | Show me my note titled "Shopping List"
256 | 
257 | // Search notes
258 | Find notes containing "recipe" in my "Cooking" folder
259 | ```
260 | 
261 | ### Pages
262 | 
263 | | Command            | Description                                  | Parameters                                                |
264 | | ----------------- | -------------------------------------------- | --------------------------------------------------------- |
265 | | `create_document` | Create a new Pages document with plain text  | `content`                                                 |
266 | 
267 | #### Examples
268 | 
269 | ```
270 | // Create a new Pages document
271 | Create a Pages document with the content "Project Proposal\n\nThis document outlines the scope and timeline for the upcoming project."
272 | ```
273 | 
274 | ## Architecture
275 | 
276 | The applescript-mcp server is built using TypeScript and follows a modular architecture:
277 | 
278 | ### Core Components
279 | 
280 | 1. **AppleScriptFramework** (`framework.ts`): The main server class that:
281 |    - Manages MCP protocol communication
282 |    - Handles tool registration and execution
283 |    - Provides logging functionality
284 |    - Executes AppleScript commands
285 | 
286 | 2. **Categories** (`src/categories/*.ts`): Modular script collections organized by functionality:
287 |    - Each category contains related scripts (e.g., calendar, system, notes)
288 |    - Categories are registered with the framework in `index.ts`
289 | 
290 | 3. **Types** (`src/types/index.ts`): TypeScript interfaces defining:
291 |    - `ScriptDefinition`: Structure for individual scripts
292 |    - `ScriptCategory`: Collection of related scripts
293 |    - `LogLevel`: Standard logging levels
294 |    - `FrameworkOptions`: Configuration options
295 | 
296 | ### Execution Flow
297 | 
298 | 1. Client sends a tool request via MCP protocol
299 | 2. Server identifies the appropriate category and script
300 | 3. Script content is generated (static or dynamically via function)
301 | 4. AppleScript is executed via macOS `osascript` command
302 | 5. Results are returned to the client
303 | 
304 | ### Logging System
305 | 
306 | The framework includes a comprehensive logging system that:
307 | - Logs to both stderr and MCP logging protocol
308 | - Supports multiple severity levels (debug, info, warning, error, etc.)
309 | - Provides detailed execution information for troubleshooting
310 | 
311 | ## Development
312 | 
313 | ### Setup
314 | 
315 | ```bash
316 | # Install dependencies
317 | npm install
318 | 
319 | # Build the server
320 | npm run build
321 | 
322 | # Launch MCP Inspector
323 | # See: https://modelcontextprotocol.io/docs/tools/inspector
324 | npx @modelcontextprotocol/inspector node path/to/server/index.js args...
325 | ```
326 | 
327 | ### Adding New Functionality
328 | 
329 | #### 1. Create Category File
330 | 
331 | Create `src/categories/newcategory.ts`:
332 | 
333 | ```typescript
334 | import { ScriptCategory } from "../types/index.js";
335 | 
336 | export const newCategory: ScriptCategory = {
337 |   name: "category_name",
338 |   description: "Category description",
339 |   scripts: [
340 |     // Scripts will go here
341 |   ],
342 | };
343 | ```
344 | 
345 | #### 2. Add Scripts
346 | 
347 | ```typescript
348 | {
349 |   name: "script_name",
350 |   description: "What the script does",
351 |   schema: {
352 |     type: "object",
353 |     properties: {
354 |       paramName: {
355 |         type: "string",
356 |         description: "Parameter description"
357 |       }
358 |     },
359 |     required: ["paramName"]
360 |   },
361 |   script: (args) => `
362 |     tell application "App"
363 |       // AppleScript code using ${args.paramName}
364 |     end tell
365 |   `
366 | }
367 | ```
368 | 
369 | #### 3. Register Category
370 | 
371 | Update `src/index.ts`:
372 | 
373 | ```typescript
374 | import { newCategory } from "./categories/newcategory.js";
375 | // ...
376 | server.addCategory(newCategory);
377 | ```
378 | 
379 | ### Advanced Script Development
380 | 
381 | For more complex scripts, you can:
382 | 
383 | 1. **Use dynamic script generation**:
384 |    ```typescript
385 |    script: (args) => {
386 |      // Process arguments and build script dynamically
387 |      let scriptContent = `tell application "App"\n`;
388 |      
389 |      if (args.condition) {
390 |        scriptContent += `  // Conditional logic\n`;
391 |      }
392 |      
393 |      scriptContent += `end tell`;
394 |      return scriptContent;
395 |    }
396 |    ```
397 | 
398 | 2. **Process complex data**:
399 |    ```typescript
400 |    // Example from Notes category
401 |    function generateNoteHtml(args: any): string {
402 |      // Process markdown-like syntax into HTML
403 |      let processedContent = content;
404 |      
405 |      if (format.headings) {
406 |        processedContent = processedContent.replace(/^# (.+)$/gm, '<h1>$1</h1>');
407 |        // ...
408 |      }
409 |      
410 |      return processedContent;
411 |    }
412 |    ```
413 | 
414 | ## Debugging
415 | 
416 | ### Using MCP Inspector
417 | 
418 | The MCP Inspector provides a web interface for testing and debugging your server:
419 | 
420 | ```bash
421 | npm run inspector
422 | ```
423 | 
424 | ### Logging
425 | 
426 | Enable debug logging by setting the environment variable:
427 | 
428 | ```bash
429 | DEBUG=applescript-mcp* npm start
430 | ```
431 | 
432 | ### Example configuration
433 | After running `npm run build` add the following to your `mcp.json` file:
434 | 
435 | ```json
436 | {
437 |   "mcpServers": {
438 |     "applescript-mcp-server": {
439 |       "command": "node",
440 |       "args": ["/path/to/applescript-mcp/dist/index.js"]
441 |     }
442 |   }
443 | }
444 | ```
445 | 
446 | ### Common Issues
447 | 
448 | - **Permission Errors**: Check System Preferences > Security & Privacy > Privacy > Automation
449 | - **Script Failures**: Test scripts directly in Script Editor.app before integration
450 | - **Communication Issues**: Check stdio streams aren't being redirected
451 | - **Database Access**: Some features (like Messages) require Full Disk Access permission
452 | 
453 | ## Resources
454 | 
455 | - [AppleScript Language Guide](https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html)
456 | - [MCP Protocol Documentation](https://modelcontextprotocol.io)
457 | - [Issue Tracker](https://github.com/joshrutkowski/applescript-mcp/issues)
458 | 
459 | ## Contributing
460 | 
461 | 1. Fork the repository
462 | 2. Create a feature branch
463 | 3. Commit your changes
464 | 4. Push to the branch
465 | 5. Create a Pull Request
466 | 
467 | ## License
468 | 
469 | MIT License - see [LICENSE](LICENSE) for details
470 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "outDir": "./dist",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "applescript-mcp",
 3 |   "version": "1.0.4",
 4 |   "description": "AppleScript MCP Framework",
 5 |   "main": "dist/index.js",
 6 |   "type": "module",
 7 |   "scripts": {
 8 |     "build": "tsc",
 9 |     "start": "node dist/index.js",
10 |     "dev": "ts-node --esm src/index.ts"
11 |   },
12 |   "keywords": [
13 |     "mcp",
14 |     "applescript",
15 |     "macos"
16 |   ],
17 |   "author": "Josh Rutkowski",
18 |   "license": "MIT",
19 |   "dependencies": {
20 |     "@modelcontextprotocol/sdk": "^1.1.1"
21 |   },
22 |   "devDependencies": {
23 |     "@types/node": "^20.0.0",
24 |     "typescript": "^5.0.0",
25 |     "ts-node": "^10.9.0"
26 |   }
27 | }
28 | 
```

--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
 3 | 
 4 | name: Node.js CI
 5 | 
 6 | on:
 7 |   push:
 8 |     branches: [ "main" ]
 9 |   pull_request:
10 |     branches: [ "main" ]
11 | 
12 | jobs:
13 |   build:
14 |     runs-on: macos-latest
15 | 
16 |     strategy:
17 |       matrix:
18 |         node-version: [18.x, 20.x]
19 |         # Using macOS runners since this is an AppleScript project
20 | 
21 |     steps:
22 |     - uses: actions/checkout@v4
23 |     - name: Use Node.js ${{ matrix.node-version }}
24 |       uses: actions/setup-node@v4
25 |       with:
26 |         node-version: ${{ matrix.node-version }}
27 |         cache: 'npm'
28 |     - run: npm ci
29 |     - run: npm run build
30 | 
```

--------------------------------------------------------------------------------
/src/categories/pages.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ScriptCategory } from "../types/index.js";
 2 | 
 3 | /**
 4 |  * Pages-related scripts.
 5 |  * * create_document: Create a new Pages document with plain text content
 6 |  */
 7 | export const pagesCategory: ScriptCategory = {
 8 |   name: "pages",
 9 |   description: "Pages document operations",
10 |   scripts: [
11 |     {
12 |       name: "create_document",
13 |       description: "Create a new Pages document with plain text content (no formatting)",
14 |       schema: {
15 |         type: "object",
16 |         properties: {
17 |           content: {
18 |             type: "string",
19 |             description: "The plain text content to add to the document (no formatting)"
20 |           }
21 |         },
22 |         required: ["content"]
23 |       },
24 |       script: (args) => `
25 |         try
26 |           tell application "Pages"
27 |             -- Create new document
28 |             set newDoc to make new document
29 |             
30 |             set the body text of newDoc to "${args.content.replace(/"/g, '\\"')}"
31 |             activate
32 |             return "Document created successfully with plain text content"
33 |           end tell
34 |         on error errMsg
35 |           return "Failed to create document: " & errMsg
36 |         end try
37 |       `
38 |     }
39 |   ]
40 | };
41 | 
```

--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export interface ScriptDefinition {
 2 |   /**
 3 |    * The name of the script.
 4 |    */
 5 |   name: string;
 6 | 
 7 |   /**
 8 |    * A brief description of what the script does.
 9 |    */
10 |   description: string;
11 | 
12 |   /**
13 |    * The script content, which can be a string or a function that returns a string.
14 |    */
15 |   script: string | ((args: any) => string);
16 | 
17 |   /**
18 |    * Optional schema defining the structure of the script's input parameters.
19 |    */
20 |   schema?: {
21 |     type: "object";
22 |     properties: Record<string, any>;
23 | 
24 |     /**
25 |      * Optional list of required properties in the schema.
26 |      */
27 |     required?: string[];
28 |   };
29 | }
30 | 
31 | export interface ScriptCategory {
32 |   /**
33 |    * The name of the script category.
34 |    */
35 |   name: string;
36 | 
37 |   /**
38 |    * A brief description of the script category.
39 |    */
40 |   description: string;
41 | 
42 |   /**
43 |    * A list of scripts that belong to this category.
44 |    */
45 |   scripts: ScriptDefinition[];
46 | }
47 | 
48 | /**
49 |  * Standard log levels for the framework's logging system.
50 |  * Follows the RFC 5424 syslog severity levels.
51 |  */
52 | export type LogLevel = 
53 |   | "emergency" // System is unusable
54 |   | "alert"     // Action must be taken immediately
55 |   | "critical"  // Critical conditions
56 |   | "error"     // Error conditions
57 |   | "warning"   // Warning conditions
58 |   | "notice"    // Normal but significant condition
59 |   | "info"      // Informational messages
60 |   | "debug";    // Debug-level messages
61 | 
62 | export interface FrameworkOptions {
63 |   /**
64 |    * Optional name of the framework.
65 |    */
66 |   name?: string;
67 | 
68 |   /**
69 |    * Optional version of the framework.
70 |    */
71 |   version?: string;
72 | 
73 |   /**
74 |    * Optional flag to enable or disable debug mode.
75 |    */
76 |   debug?: boolean;
77 | }
78 | 
```

--------------------------------------------------------------------------------
/src/categories/notifications.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // src/categories/notifications.ts
 2 | import { ScriptCategory } from "../types/index.js";
 3 | 
 4 | /**
 5 |  * Notification-related scripts.
 6 |  * * toggle_do_not_disturb: Toggle Do Not Disturb mode. NOTE: Requires keyboard shortcut to be set up in System Preferences.
 7 |  * * send_notification: Send a system notification
 8 |  */
 9 | export const notificationsCategory: ScriptCategory = {
10 |   name: "notifications",
11 |   description: "Notification management",
12 |   scripts: [
13 |     {
14 |       name: "toggle_do_not_disturb",
15 |       description: "Toggle Do Not Disturb mode using keyboard shortcut",
16 |       script: `
17 |         try
18 |           tell application "System Events"
19 |             keystroke "z" using {control down, option down, command down}
20 |           end tell
21 |           return "Toggled Do Not Disturb mode"
22 |         on error errMsg
23 |           return "Failed to toggle Do Not Disturb: " & errMsg
24 |         end try
25 |       `,
26 |     },
27 |     {
28 |       name: "send_notification",
29 |       description: "Send a system notification",
30 |       schema: {
31 |         type: "object",
32 |         properties: {
33 |           title: {
34 |             type: "string",
35 |             description: "Notification title",
36 |           },
37 |           message: {
38 |             type: "string",
39 |             description: "Notification message",
40 |           },
41 |           sound: {
42 |             type: "boolean",
43 |             description: "Play sound with notification",
44 |             default: true,
45 |           },
46 |         },
47 |         required: ["title", "message"],
48 |       },
49 |       script: (args) => `
50 |         display notification "${args.message}" with title "${args.title}" ${args.sound ? 'sound name "default"' : ""}
51 |       `,
52 |     },
53 |   ],
54 | };
55 | 
```

--------------------------------------------------------------------------------
/src/categories/iterm.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ScriptCategory } from "../types/index.js";
 2 | 
 3 | /**
 4 |  * iTerm-related scripts.
 5 |  * * paste_clipboard: Pastes the clipboard to an iTerm window
 6 |  * * run: Run a command in iTerm
 7 |  */
 8 | export const itermCategory: ScriptCategory = {
 9 |   name: "iterm",
10 |   description: "iTerm terminal operations",
11 |   scripts: [
12 |     {
13 |       name: "paste_clipboard",
14 |       description: "Paste clipboard content into iTerm",
15 |       script: `
16 |         tell application "System Events" to keystroke "c" using {command down}
17 |         delay 0.1
18 |         tell application "iTerm"
19 |           set w to current window
20 |           tell w's current session to write text (the clipboard)
21 |           activate
22 |         end tell
23 |       `,
24 |     },
25 |     {
26 |       name: "run",
27 |       description: "Run a command in iTerm",
28 |       schema: {
29 |         type: "object",
30 |         properties: {
31 |           command: {
32 |             type: "string",
33 |             description: "Command to run in iTerm",
34 |           },
35 |           newWindow: {
36 |             type: "boolean",
37 |             description: "Whether to open in a new window (default: false)",
38 |             default: false,
39 |           },
40 |         },
41 |         required: ["command"],
42 |       },
43 |       script: (args) => `
44 |         tell application "iTerm"
45 |           ${
46 |             args.newWindow
47 |               ? `
48 |             set newWindow to (create window with default profile)
49 |             tell current session of newWindow
50 |           `
51 |               : `
52 |             set w to current window
53 |             tell w's current session
54 |           `
55 |           }
56 |             write text "${args.command}"
57 |             activate
58 |           end tell
59 |         end tell
60 |       `,
61 |     },
62 |   ],
63 | };
64 | 
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Create Release
 2 | 
 3 | on:
 4 |   push:
 5 |     tags:
 6 |       - 'v*' # Trigger on version tags (v1.0.0, v1.2.3, etc.)
 7 | 
 8 | permissions:
 9 |   contents: write 
10 |   pull-requests: write
11 |   repository-projects: write
12 | 
13 | jobs:
14 |   build:
15 |     runs-on: macos-latest
16 |     
17 |     steps:
18 |       - name: Checkout code
19 |         uses: actions/checkout@v4
20 |         
21 |       - name: Setup Node.js
22 |         uses: actions/setup-node@v4
23 |         with:
24 |           node-version: '20.x'
25 |           registry-url: 'https://registry.npmjs.org'
26 |           cache: 'npm'
27 |           
28 |       - name: Install dependencies
29 |         run: npm ci
30 |         
31 |       - name: Build package
32 |         run: npm run build
33 |         
34 |       - name: Get version from tag
35 |         id: get_version
36 |         run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
37 |         
38 |       - name: Create package archive
39 |         run: |
40 |           mkdir -p dist-package
41 |           cp -r dist package.json README.md LICENSE dist-package/
42 |           cd dist-package
43 |           npm pack
44 |           mv *.tgz ../applescript-mcp-${{ steps.get_version.outputs.VERSION }}.tgz
45 |           
46 |       - name: Create GitHub Release
47 |         uses: softprops/action-gh-release@v1
48 |         with:
49 |           name: Release ${{ github.ref_name }}
50 |           draft: false
51 |           prerelease: false
52 |           body: |
53 |             Release of applescript-mcp version ${{ steps.get_version.outputs.VERSION }}
54 |             
55 |             ## Changes
56 |             
57 |             <!-- Add your release notes here -->
58 |           files: |
59 |             ./applescript-mcp-${{ steps.get_version.outputs.VERSION }}.tgz
60 |           
61 |       # Uncomment the following step if you want to publish to npm
62 |       # - name: Publish to npm
63 |       #   run: npm publish
64 |       #   env:
65 |       #     NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
66 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { AppleScriptFramework } from "./framework.js";
 2 | import { systemCategory } from "./categories/system.js";
 3 | import { calendarCategory } from "./categories/calendar.js";
 4 | import { finderCategory } from "./categories/finder.js";
 5 | import { clipboardCategory } from "./categories/clipboard.js";
 6 | import { notificationsCategory } from "./categories/notifications.js";
 7 | import { itermCategory } from "./categories/iterm.js";
 8 | import { mailCategory } from "./categories/mail.js";
 9 | import { pagesCategory } from "./categories/pages.js";
10 | import { shortcutsCategory } from "./categories/shortcuts.js";
11 | import { messagesCategory } from "./categories/messages.js";
12 | import { notesCategory } from "./categories/notes.js";
13 | 
14 | const server = new AppleScriptFramework({
15 |   name: "applescript-server",
16 |   version: "1.0.4",
17 |   debug: false,
18 | });
19 | 
20 | // Log startup information using stderr (server isn't connected yet)
21 | console.error(`[INFO] Starting AppleScript MCP server - PID: ${process.pid}`);
22 | 
23 | // Add all categories
24 | console.error("[INFO] Registering categories...");
25 | server.addCategory(systemCategory);
26 | server.addCategory(calendarCategory);
27 | server.addCategory(finderCategory);
28 | server.addCategory(clipboardCategory);
29 | server.addCategory(notificationsCategory);
30 | server.addCategory(itermCategory);
31 | server.addCategory(mailCategory);
32 | server.addCategory(pagesCategory);
33 | server.addCategory(shortcutsCategory);
34 | server.addCategory(messagesCategory);
35 | server.addCategory(notesCategory);
36 | console.error(`[INFO] Registered ${11} categories successfully`);
37 | 
38 | // Start the server
39 | console.error("[INFO] Starting server...");
40 | server.run()
41 |   .then(() => {
42 |     console.error("[NOTICE] Server started successfully");
43 |   })
44 |   .catch(error => {
45 |     const errorMessage = error instanceof Error ? error.message : String(error);
46 |     console.error(`[EMERGENCY] Failed to start server: ${errorMessage}`);
47 |     console.error(error);
48 |   });
49 | 
```

--------------------------------------------------------------------------------
/src/categories/shortcuts.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ScriptCategory } from "../types/index.js";
 2 | 
 3 | /**
 4 |  * Shortcuts-related scripts.
 5 |  * * run_shortcut: Run a shortcut with optional input
 6 |  * * list_shortcuts: List available shortcuts
 7 |  */
 8 | export const shortcutsCategory: ScriptCategory = {
 9 |   name: "shortcuts",
10 |   description: "Shortcuts operations",
11 |   scripts: [
12 |     {
13 |       name: "run_shortcut",
14 |       description: "Run a shortcut with optional input. Uses Shortcuts Events to run in background without opening the app.",
15 |       schema: {
16 |         type: "object",
17 |         properties: {
18 |           name: {
19 |             type: "string",
20 |             description: "Name of the shortcut to run",
21 |           },
22 |           input: {
23 |             type: "string",
24 |             description: "Optional input to provide to the shortcut",
25 |           },
26 |         },
27 |         required: ["name"],
28 |       },
29 |       script: (args) => `
30 |         try
31 |           tell application "Shortcuts Events"
32 |             ${args.input ? 
33 |               `run shortcut "${args.name}" with input "${args.input}"` :
34 |               `run shortcut "${args.name}"`
35 |             }
36 |           end tell
37 |           return "Shortcut '${args.name}' executed successfully"
38 |         on error errMsg
39 |           return "Failed to run shortcut: " & errMsg
40 |         end try
41 |       `,
42 |     },
43 |     {
44 |       name: "list_shortcuts",
45 |       description: "List all available shortcuts with optional limit",
46 |       schema: {
47 |         type: "object",
48 |         properties: {
49 |           limit: {
50 |             type: "number",
51 |             description: "Optional limit on the number of shortcuts to return",
52 |           },
53 |         },
54 |       },
55 |       script: (args) => `
56 |         try
57 |           tell application "Shortcuts"
58 |             set shortcutNames to name of every shortcut
59 |             
60 |             ${args.limit ? `
61 |             -- Apply limit if specified
62 |             if (count of shortcutNames) > ${args.limit} then
63 |               set shortcutNames to items 1 through ${args.limit} of shortcutNames
64 |             end if
65 |             ` : ``}
66 |           end tell
67 |           
68 |           -- Convert to JSON string manually
69 |           set jsonOutput to "{"
70 |           set jsonOutput to jsonOutput & "\\"status\\": \\"success\\","
71 |           set jsonOutput to jsonOutput & "\\"shortcuts\\": ["
72 |           
73 |           repeat with i from 1 to count of shortcutNames
74 |             set currentName to item i of shortcutNames
75 |             set jsonOutput to jsonOutput & "{\\"name\\": \\"" & currentName & "\\"}"
76 |             if i < count of shortcutNames then
77 |               set jsonOutput to jsonOutput & ", "
78 |             end if
79 |           end repeat
80 |           
81 |           set jsonOutput to jsonOutput & "]}"
82 |           return jsonOutput
83 |           
84 |         on error errMsg
85 |           return "{\\"status\\": \\"error\\", \\"message\\": \\"" & errMsg & "\\"}"
86 |         end try
87 |       `,
88 |     },
89 |   ],
90 | };
91 | 
```

--------------------------------------------------------------------------------
/src/categories/clipboard.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/categories/clipboard.ts
  2 | import { ScriptCategory } from "../types/index.js";
  3 | 
  4 | /**
  5 |  * Clipboard-related scripts.
  6 |  * * get_clipboard: Returns the current clipboard content
  7 |  * * set_clipboard: Sets the clipboard to a specified value
  8 |  * * clear_clipboard: Resets the clipboard content
  9 |  */
 10 | export const clipboardCategory: ScriptCategory = {
 11 |   name: "clipboard",
 12 |   description: "Clipboard management operations",
 13 |   scripts: [
 14 |     {
 15 |       name: "get_clipboard",
 16 |       description: "Get current clipboard content",
 17 |       schema: {
 18 |         type: "object",
 19 |         properties: {
 20 |           type: {
 21 |             type: "string",
 22 |             enum: ["text", "file_paths"],
 23 |             description: "Type of clipboard content to get",
 24 |             default: "text",
 25 |           },
 26 |         },
 27 |       },
 28 |       script: (args) => {
 29 |         if (args?.type === "file_paths") {
 30 |           return `
 31 |             tell application "System Events"
 32 |               try
 33 |                 set theClipboard to the clipboard
 34 |                 if theClipboard starts with "file://" then
 35 |                   set AppleScript's text item delimiters to linefeed
 36 |                   set filePaths to {}
 37 |                   repeat with aPath in paragraphs of (the clipboard as string)
 38 |                     if aPath starts with "file://" then
 39 |                       set end of filePaths to (POSIX path of (aPath as alias))
 40 |                     end if
 41 |                   end repeat
 42 |                   return filePaths as string
 43 |                 else
 44 |                   return "No file paths in clipboard"
 45 |                 end if
 46 |               on error errMsg
 47 |                 return "Failed to get clipboard: " & errMsg
 48 |               end try
 49 |             end tell
 50 |           `;
 51 |         } else {
 52 |           return `
 53 |             tell application "System Events"
 54 |               try
 55 |                 return (the clipboard as text)
 56 |               on error errMsg
 57 |                 return "Failed to get clipboard: " & errMsg
 58 |               end try
 59 |             end tell
 60 |           `;
 61 |         }
 62 |       },
 63 |     },
 64 |     {
 65 |       name: "set_clipboard",
 66 |       description: "Set clipboard content",
 67 |       schema: {
 68 |         type: "object",
 69 |         properties: {
 70 |           content: {
 71 |             type: "string",
 72 |             description: "Content to copy to clipboard",
 73 |           },
 74 |         },
 75 |         required: ["content"],
 76 |       },
 77 |       script: (args) => `
 78 |         try
 79 |           set the clipboard to "${args.content}"
 80 |           return "Clipboard content set successfully"
 81 |         on error errMsg
 82 |           return "Failed to set clipboard: " & errMsg
 83 |         end try
 84 |       `,
 85 |     },
 86 |     {
 87 |       name: "clear_clipboard",
 88 |       description: "Clear clipboard content",
 89 |       script: `
 90 |         try
 91 |           set the clipboard to ""
 92 |           return "Clipboard cleared successfully"
 93 |         on error errMsg
 94 |           return "Failed to clear clipboard: " & errMsg
 95 |         end try
 96 |       `,
 97 |     },
 98 |   ],
 99 | };
100 | 
```

--------------------------------------------------------------------------------
/src/categories/system.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ScriptCategory } from "../types/index.js";
  2 | 
  3 | /**
  4 |  * System-related scripts.
  5 |  * * volume: Set system volume
  6 |  * * get_frontmost_app: Get the name of the frontmost application
  7 |  * * launch_app: Launch an application
  8 |  * * quit_app: Quit an application
  9 |  * * toggle_dark_mode: Toggle system dark mode
 10 |  */
 11 | export const systemCategory: ScriptCategory = {
 12 |   name: "system",
 13 |   description: "System control and information",
 14 |   scripts: [
 15 |     {
 16 |       name: "volume",
 17 |       description: "Set system volume",
 18 |       schema: {
 19 |         type: "object",
 20 |         properties: {
 21 |           level: {
 22 |             type: "number",
 23 |             minimum: 0,
 24 |             maximum: 100,
 25 |           },
 26 |         },
 27 |         required: ["level"],
 28 |       },
 29 |       script: (args) => `set volume ${Math.round((args.level / 100) * 7)}`,
 30 |     },
 31 |     {
 32 |       name: "get_frontmost_app",
 33 |       description: "Get the name of the frontmost application",
 34 |       script:
 35 |         'tell application "System Events" to get name of first process whose frontmost is true',
 36 |     },
 37 |     {
 38 |       name: "launch_app",
 39 |       description: "Launch an application",
 40 |       schema: {
 41 |         type: "object",
 42 |         properties: {
 43 |           name: {
 44 |             type: "string",
 45 |             description: "Application name",
 46 |           },
 47 |         },
 48 |         required: ["name"],
 49 |       },
 50 |       script: (args) => `
 51 |             try
 52 |               tell application "${args.name}"
 53 |                 activate
 54 |               end tell
 55 |               return "Application ${args.name} launched successfully"
 56 |             on error errMsg
 57 |               return "Failed to launch application: " & errMsg
 58 |             end try
 59 |           `,
 60 |     },
 61 |     {
 62 |       name: "quit_app",
 63 |       description: "Quit an application",
 64 |       schema: {
 65 |         type: "object",
 66 |         properties: {
 67 |           name: {
 68 |             type: "string",
 69 |             description: "Application name",
 70 |           },
 71 |           force: {
 72 |             type: "boolean",
 73 |             description: "Force quit if true",
 74 |             default: false,
 75 |           },
 76 |         },
 77 |         required: ["name"],
 78 |       },
 79 |       script: (args) => `
 80 |             try
 81 |               tell application "${args.name}"
 82 |                 ${args.force ? "quit saving no" : "quit"}
 83 |               end tell
 84 |               return "Application ${args.name} quit successfully"
 85 |             on error errMsg
 86 |               return "Failed to quit application: " & errMsg
 87 |             end try
 88 |           `,
 89 |     },
 90 |     {
 91 |       name: "toggle_dark_mode",
 92 |       description: "Toggle system dark mode",
 93 |       script: `
 94 |             tell application "System Events"
 95 |               tell appearance preferences
 96 |                 set dark mode to not dark mode
 97 |                 return "Dark mode is now " & (dark mode as text)
 98 |               end tell
 99 |             end tell
100 |           `,
101 |     },
102 |     {
103 |       name: "get_battery_status",
104 |       description: "Get battery level and charging status",
105 |       script: `
106 |             try
107 |               set powerSource to do shell script "pmset -g batt"
108 |               return powerSource
109 |             on error errMsg
110 |               return "Failed to get battery status: " & errMsg
111 |             end try
112 |           `,
113 |     },
114 |   ],
115 | };
116 | 
```

--------------------------------------------------------------------------------
/src/categories/finder.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/categories/finder.ts
  2 | import { ScriptCategory } from "../types/index.js";
  3 | 
  4 | /**
  5 |  * Finder-related scripts.
  6 |  * * get_selected_files: Get currently selected files in Finder
  7 |  * * search_files: Search for files by name
  8 |  * * quick_look_file: Preview a file using Quick Look
  9 |  *
 10 |  */
 11 | export const finderCategory: ScriptCategory = {
 12 |   name: "finder",
 13 |   description: "Finder and file operations",
 14 |   scripts: [
 15 |     {
 16 |       name: "get_selected_files",
 17 |       description: "Get currently selected files in Finder",
 18 |       script: `
 19 |         tell application "Finder"
 20 |           try
 21 |             set selectedItems to selection
 22 |             if selectedItems is {} then
 23 |               return "No items selected"
 24 |             end if
 25 | 
 26 |             set itemPaths to ""
 27 |             repeat with theItem in selectedItems
 28 |               set itemPaths to itemPaths & (POSIX path of (theItem as alias)) & linefeed
 29 |             end repeat
 30 | 
 31 |             return itemPaths
 32 |           on error errMsg
 33 |             return "Failed to get selected files: " & errMsg
 34 |           end try
 35 |         end tell
 36 |       `,
 37 |     },
 38 |     {
 39 |       name: "search_files",
 40 |       description: "Search for files by name",
 41 |       schema: {
 42 |         type: "object",
 43 |         properties: {
 44 |           query: {
 45 |             type: "string",
 46 |             description: "Search term",
 47 |           },
 48 |           location: {
 49 |             type: "string",
 50 |             description: "Search location (default: home folder)",
 51 |             default: "~",
 52 |           },
 53 |         },
 54 |         required: ["query"],
 55 |       },
 56 |       script: (args) => `
 57 |         set searchPath to "/Users/joshrutkowski/Downloads"
 58 |         tell application "Finder"
 59 |           try
 60 |             set theFolder to POSIX file searchPath as alias
 61 |             set theFiles to every file of folder theFolder whose name contains "${args.query}"
 62 |             set resultList to ""
 63 |             repeat with aFile in theFiles
 64 |               set resultList to resultList & (POSIX path of (aFile as alias)) & return
 65 |             end repeat
 66 |             if resultList is "" then
 67 |               return "No files found matching '${args.query}'"
 68 |             end if
 69 |             return resultList
 70 |           on error errMsg
 71 |             return "Failed to search files: " & errMsg
 72 |           end try
 73 |         end tell
 74 |       `,
 75 |     },
 76 |     {
 77 |       name: "quick_look_file",
 78 |       description: "Preview a file using Quick Look",
 79 |       schema: {
 80 |         type: "object",
 81 |         properties: {
 82 |           path: {
 83 |             type: "string",
 84 |             description: "File path to preview",
 85 |           },
 86 |         },
 87 |         required: ["path"],
 88 |       },
 89 |       script: (args) => `
 90 |         try
 91 |           set filePath to POSIX file "${args.path}"
 92 |           tell application "Finder"
 93 |             activate
 94 |             select filePath
 95 |             tell application "System Events"
 96 |               -- Press Space to trigger Quick Look
 97 |               delay 0.5 -- Small delay to ensure Finder is ready
 98 |               key code 49 -- Space key
 99 |             end tell
100 |           end tell
101 |           return "Quick Look preview opened for ${args.path}"
102 |         on error errMsg
103 |           return "Failed to open Quick Look: " & errMsg
104 |         end try
105 |       `,
106 |     },
107 |   ],
108 | };
109 | 
```

--------------------------------------------------------------------------------
/src/categories/calendar.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ScriptCategory } from "../types/index.js";
 2 | 
 3 | /**
 4 |  * Calendar-related scripts.
 5 |  * * add: adds a new event to Calendar
 6 |  * * list: List events for today
 7 |  */
 8 | export const calendarCategory: ScriptCategory = {
 9 |   name: "calendar",
10 |   description: "Calendar operations",
11 |   scripts: [
12 |     {
13 |       name: "add",
14 |       description: "Add a new event to Calendar",
15 |       schema: {
16 |         type: "object",
17 |         properties: {
18 |           title: {
19 |             type: "string",
20 |             description: "Event title",
21 |           },
22 |           startDate: {
23 |             type: "string",
24 |             description: "Start date and time (YYYY-MM-DD HH:MM:SS)",
25 |           },
26 |           endDate: {
27 |             type: "string",
28 |             description: "End date and time (YYYY-MM-DD HH:MM:SS)",
29 |           },
30 |           calendar: {
31 |             type: "string",
32 |             description: "Calendar name (optional)",
33 |             default: "Calendar",
34 |           },
35 |         },
36 |         required: ["title", "startDate", "endDate"],
37 |       },
38 |       script: (args) => `
39 |         tell application "Calendar"
40 |           set theStartDate to current date
41 |           set hours of theStartDate to ${args.startDate.slice(11, 13)}
42 |           set minutes of theStartDate to ${args.startDate.slice(14, 16)}
43 |           set seconds of theStartDate to ${args.startDate.slice(17, 19)}
44 | 
45 |           set theEndDate to theStartDate + (1 * hours)
46 |           set hours of theEndDate to ${args.endDate.slice(11, 13)}
47 |           set minutes of theEndDate to ${args.endDate.slice(14, 16)}
48 |           set seconds of theEndDate to ${args.endDate.slice(17, 19)}
49 | 
50 |           tell calendar "${args.calendar || "Calendar"}"
51 |             make new event with properties {summary:"${args.title}", start date:theStartDate, end date:theEndDate}
52 |           end tell
53 |         end tell
54 |       `,
55 |     },
56 |     {
57 |       name: "list",
58 |       description: "List all events for today",
59 |       script: `
60 |       tell application "Calendar"
61 |           set todayStart to (current date)
62 |           set time of todayStart to 0
63 |           set todayEnd to todayStart + 1 * days
64 |           set eventList to {}
65 |           repeat with calendarAccount in calendars
66 |               set eventList to eventList & (every event of calendarAccount whose start date is greater than or equal to todayStart and start date is less than todayEnd)
67 |           end repeat
68 |           set output to ""
69 |           repeat with anEvent in eventList
70 |               set eventStartDate to start date of anEvent
71 |               set eventEndDate to end date of anEvent
72 | 
73 |               -- Format the time parts
74 |               set startHours to hours of eventStartDate
75 |               set startMinutes to minutes of eventStartDate
76 |               set endHours to hours of eventEndDate
77 |               set endMinutes to minutes of eventEndDate
78 | 
79 |               set output to output & "Event: " & summary of anEvent & "\n"
80 |               set output to output & "Start: " & startHours & ":" & text -2 thru -1 of ("0" & startMinutes) & "\n"
81 |               set output to output & "End: " & endHours & ":" & text -2 thru -1 of ("0" & endMinutes) & "\n"
82 |               set output to output & "-------------------\n"
83 |           end repeat
84 |           return output
85 |       end tell
86 |       `,
87 |     },
88 |   ],
89 | };
90 | 
```

--------------------------------------------------------------------------------
/src/categories/messages.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ScriptCategory } from "../types/index.js";
  2 | 
  3 | /**
  4 |  * iMessage related scripts
  5 |  */
  6 | export const messagesCategory: ScriptCategory = {
  7 |   name: "messages",
  8 |   description: "iMessage operations",
  9 |   scripts: [
 10 |     {
 11 |       name: "list_chats",
 12 |       description: "List available iMessage and SMS chats",
 13 |       schema: {
 14 |         type: "object",
 15 |         properties: {
 16 |           includeParticipantDetails: {
 17 |             type: "boolean",
 18 |             description: "Include detailed participant information",
 19 |             default: false
 20 |           }
 21 |         }
 22 |       },
 23 |       script: (args) => `
 24 |         tell application "Messages"
 25 |           set chatList to {}
 26 |           repeat with aChat in chats
 27 |             set chatName to name of aChat
 28 |             if chatName is missing value then
 29 |               set chatName to ""
 30 |               -- Try to get the contact name for individual chats
 31 |               try
 32 |                 set theParticipants to participants of aChat
 33 |                 if (count of theParticipants) is 1 then
 34 |                   set theParticipant to item 1 of theParticipants
 35 |                   set chatName to name of theParticipant
 36 |                 end if
 37 |               end try
 38 |             end if
 39 |             
 40 |             set chatInfo to {id:id of aChat, name:chatName, isGroupChat:(id of aChat contains "+")}
 41 |             
 42 |             ${args.includeParticipantDetails ? `
 43 |             -- Add participant details if requested
 44 |             set participantList to {}
 45 |             repeat with aParticipant in participants of aChat
 46 |               set participantInfo to {id:id of aParticipant, handle:handle of aParticipant}
 47 |               try
 48 |                 set participantInfo to participantInfo & {name:name of aParticipant}
 49 |               end try
 50 |               copy participantInfo to end of participantList
 51 |             end repeat
 52 |             set chatInfo to chatInfo & {participant:participantList}
 53 |             ` : ''}
 54 |             
 55 |             copy chatInfo to end of chatList
 56 |           end repeat
 57 |           return chatList
 58 |         end tell
 59 |       `
 60 |     },
 61 |     {
 62 |       name: "get_messages",
 63 |       description: "Get messages from the Messages app",
 64 |       schema: {
 65 |         type: "object",
 66 |         properties: {
 67 |           limit: {
 68 |             type: "number",
 69 |             description: "Maximum number of messages to retrieve",
 70 |             default: 100
 71 |           }
 72 |         }
 73 |       },
 74 |       script: (args) => `
 75 |  on run
 76 | 	-- Path to the Messages database
 77 | 	set dbPath to (do shell script "echo ~/Library/Messages/chat.db")
 78 | 	
 79 | 	-- Create a temporary SQL file for our query
 80 | 	set tempFile to (do shell script "mktemp /tmp/imessage_query.XXXXXX")
 81 | 	
 82 | 	-- Write SQL query to temp file
 83 | 	do shell script "cat > " & quoted form of tempFile & " << 'EOF'
 84 | SELECT
 85 |     datetime(message.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as message_date,
 86 |     handle.id as sender,
 87 |     message.text as message_text,
 88 |     chat.display_name as chat_name
 89 | FROM
 90 |     message
 91 |     LEFT JOIN handle ON message.handle_id = handle.ROWID
 92 |     LEFT JOIN chat_message_join ON message.ROWID = chat_message_join.message_id
 93 |     LEFT JOIN chat ON chat_message_join.chat_id = chat.ROWID
 94 | ORDER BY
 95 |     message.date DESC
 96 | LIMIT ${args.limit};
 97 | EOF"
 98 | 	
 99 | 	-- Execute the query
100 | 	set queryResult to do shell script "sqlite3 " & quoted form of dbPath & " < " & quoted form of tempFile
101 | 	
102 | 	-- Clean up temp file
103 | 	do shell script "rm " & quoted form of tempFile
104 | 	
105 | 	-- Process and display results
106 | 	set resultList to paragraphs of queryResult
107 | 	set messageData to {}
108 | 	
109 | 	repeat with messageLine in resultList
110 | 		set messageData to messageData & messageLine
111 | 	end repeat
112 | 	
113 | 	return messageData
114 | end run
115 |       `
116 |     },
117 |     {
118 |       name: "search_messages",
119 |       description: "Search for messages containing specific text or from a specific sender",
120 |       schema: {
121 |         type: "object",
122 |         properties: {
123 |           searchText: {
124 |             type: "string",
125 |             description: "Text to search for in messages",
126 |             default: ""
127 |           },
128 |           sender: {
129 |             type: "string",
130 |             description: "Search for messages from a specific sender (phone number or email)",
131 |             default: ""
132 |           },
133 |           chatId: {
134 |             type: "string",
135 |             description: "Limit search to a specific chat ID",
136 |             default: ""
137 |           },
138 |           limit: {
139 |             type: "number",
140 |             description: "Maximum number of messages to retrieve",
141 |             default: 50
142 |           },
143 |           daysBack: {
144 |             type: "number",
145 |             description: "Limit search to messages from the last N days",
146 |             default: 30
147 |           }
148 |         },
149 |         required: ["searchText"]
150 |       },
151 |       script: (args) => `
152 | on run
153 | 	-- Path to the Messages database
154 | 	set dbPath to (do shell script "echo ~/Library/Messages/chat.db")
155 | 	
156 | 	-- Create a temporary SQL file for our query
157 | 	set tempFile to (do shell script "mktemp /tmp/imessage_search.XXXXXX")
158 | 	
159 | 	-- Build WHERE clause based on provided parameters
160 | 	set whereClause to ""
161 | 	
162 | 	${args.searchText ? `
163 | 	-- Add search text condition if provided
164 | 	set whereClause to whereClause & "message.text LIKE '%${args.searchText.replace(/'/g, "''")}%' "
165 | 	` : ''}
166 | 	
167 | 	${args.sender ? `
168 | 	-- Add sender condition if provided
169 | 	if length of whereClause > 0 then
170 | 		set whereClause to whereClause & "AND "
171 | 	end if
172 | 	set whereClause to whereClause & "handle.id LIKE '%${args.sender.replace(/'/g, "''")}%' "
173 | 	` : ''}
174 | 	
175 | 	${args.chatId ? `
176 | 	-- Add chat ID condition if provided
177 | 	if length of whereClause > 0 then
178 | 		set whereClause to whereClause & "AND "
179 | 	end if
180 | 	set whereClause to whereClause & "chat.chat_identifier = '${args.chatId.replace(/'/g, "''")}' "
181 | 	` : ''}
182 | 	
183 | 	${args.daysBack ? `
184 | 	-- Add date range condition
185 | 	if length of whereClause > 0 then
186 | 		set whereClause to whereClause & "AND "
187 | 	end if
188 | 	set whereClause to whereClause & "message.date > (strftime('%s', 'now', '-${args.daysBack} days') - strftime('%s', '2001-01-01')) * 1000000000 "
189 | 	` : ''}
190 | 	
191 | 	-- If no search parameters were provided, add a default condition to avoid returning all messages
192 | 	if length of whereClause = 0 then
193 | 		set whereClause to "1=1 "
194 | 	end if
195 | 	
196 | 	-- Write SQL query to temp file
197 | 	do shell script "cat > " & quoted form of tempFile & " << 'EOF'
198 | SELECT
199 |     datetime(message.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as message_date,
200 |     handle.id as sender,
201 |     message.text as message_text,
202 |     chat.display_name as chat_name,
203 |     chat.chat_identifier as chat_id
204 | FROM
205 |     message
206 |     LEFT JOIN handle ON message.handle_id = handle.ROWID
207 |     LEFT JOIN chat_message_join ON message.ROWID = chat_message_join.message_id
208 |     LEFT JOIN chat ON chat_message_join.chat_id = chat.ROWID
209 | WHERE
210 |     " & whereClause & "
211 | ORDER BY
212 |     message.date DESC
213 | LIMIT ${args.limit};
214 | EOF"
215 | 	
216 | 	-- Execute the query
217 | 	set queryResult to do shell script "sqlite3 " & quoted form of dbPath & " < " & quoted form of tempFile
218 | 	
219 | 	-- Clean up temp file
220 | 	do shell script "rm " & quoted form of tempFile
221 | 	
222 | 	-- Process and display results
223 | 	set resultList to paragraphs of queryResult
224 | 	set messageData to {}
225 | 	
226 | 	repeat with messageLine in resultList
227 | 		set messageData to messageData & messageLine
228 | 	end repeat
229 | 	
230 | 	return messageData
231 | end run
232 |       `
233 |     },
234 |     {
235 |       name: "compose_message",
236 |       description: "Open Messages app with a pre-filled message to a recipient or automatically send a message",
237 |       schema: {
238 |         type: "object",
239 |         properties: {
240 |           recipient: {
241 |             type: "string",
242 |             description: "Phone number or email of the recipient"
243 |           },
244 |           body: {
245 |             type: "string",
246 |             description: "Message body text",
247 |             default: ""
248 |           },
249 |           auto: {
250 |             type: "boolean",
251 |             description: "Automatically send the message without user confirmation",
252 |             default: false
253 |           }
254 |         },
255 |         required: ["recipient"]
256 |       },
257 |       script: (args) => `
258 | on run
259 |   -- Get the recipient and message body
260 |   set recipient to "${args.recipient}"
261 |   set messageBody to "${args.body || ''}"
262 |   set autoSend to ${args.auto === true ? "true" : "false"}
263 |   
264 |   if autoSend then
265 |     -- Automatically send the message using AppleScript
266 |     tell application "Messages"
267 |       -- Get the service (iMessage or SMS)
268 |       set targetService to 1st service whose service type = iMessage
269 |       
270 |       -- Send the message
271 |       set targetBuddy to buddy "${args.recipient}" of targetService
272 |       send "${args.body || ''}" to targetBuddy
273 |       
274 |       return "Message sent to " & "${args.recipient}"
275 |     end tell
276 |   else
277 |     -- Just open Messages app with pre-filled content
278 |     -- Create the SMS URL with proper URL encoding
279 |     set smsURL to "sms:" & recipient
280 |     
281 |     if messageBody is not equal to "" then
282 |       -- Use percent encoding for spaces instead of plus signs
283 |       set encodedBody to ""
284 |       repeat with i from 1 to count of characters of messageBody
285 |         set c to character i of messageBody
286 |         if c is space then
287 |           set encodedBody to encodedBody & "%20"
288 |         else
289 |           set encodedBody to encodedBody & c
290 |         end if
291 |       end repeat
292 |       
293 |       set smsURL to smsURL & "&body=" & encodedBody
294 |     end if
295 |     
296 |     -- Open the URL with the default handler (Messages app)
297 |     do shell script "open " & quoted form of smsURL
298 |     
299 |     return "Opening Messages app with recipient: " & recipient
300 |   end if
301 | end run
302 |       `
303 |     }
304 |   ]
305 | };
306 | 
```

--------------------------------------------------------------------------------
/src/framework.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  3 | import {
  4 |   ListToolsRequestSchema,
  5 |   CallToolRequestSchema,
  6 |   ErrorCode,
  7 |   McpError,
  8 | } from "@modelcontextprotocol/sdk/types.js";
  9 | import { exec } from "child_process";
 10 | import { promisify } from "util";
 11 | import os from "os";
 12 | import {
 13 |   ScriptCategory,
 14 |   ScriptDefinition,
 15 |   FrameworkOptions,
 16 |   LogLevel,
 17 | } from "./types/index.js";
 18 | 
 19 | const execAsync = promisify(exec);
 20 | 
 21 | // Get system information for logging
 22 | const systemInfo = {
 23 |   platform: os.platform(),
 24 |   release: os.release(),
 25 |   hostname: os.hostname(),
 26 |   arch: os.arch(),
 27 |   nodeVersion: process.version
 28 | };
 29 | 
 30 | 
 31 | 
 32 | export class AppleScriptFramework {
 33 |   private server: Server;
 34 |   private categories: ScriptCategory[] = [];
 35 |   private _initInfo: Record<string, any> = {};
 36 |   private _isConnected: boolean = false;
 37 |   private _pendingCategories: Array<Record<string, any>> = [];
 38 | 
 39 |   /**
 40 |    * Constructs an instance of AppleScriptFramework.
 41 |    * @param options - Configuration options for the framework.
 42 |    */
 43 |   constructor(options: FrameworkOptions = {}) {
 44 |     const serverName = options.name || "applescript-server";
 45 |     const serverVersion = options.version || "1.0.0";
 46 |     
 47 |     this.server = new Server(
 48 |       {
 49 |         name: serverName,
 50 |         version: serverVersion,
 51 |       },
 52 |       {
 53 |         capabilities: {
 54 |           tools: {},
 55 |           logging: {}, // Enable logging capability
 56 |         },
 57 |       },
 58 |     );
 59 | 
 60 |     if (options.debug) {
 61 |       this.enableDebugLogging();
 62 |     }
 63 |     
 64 |     // Log server initialization with stderr (server isn't connected yet)
 65 |     console.error(`[INFO] AppleScript MCP server initialized - ${serverName} v${serverVersion}`);
 66 |     
 67 |     // Store initialization info for later logging after connection
 68 |     this._initInfo = {
 69 |       name: serverName,
 70 |       version: serverVersion,
 71 |       debug: !!options.debug,
 72 |       system: systemInfo
 73 |     };
 74 |   }
 75 | 
 76 |   /**
 77 |    * Enables debug logging for the server.
 78 |    * Sets up error handlers and configures detailed logging.
 79 |    */
 80 |   private enableDebugLogging(): void {
 81 |     console.error("[INFO] Debug logging enabled");
 82 |     
 83 |     this.server.onerror = (error) => {
 84 |       const errorMessage = error instanceof Error ? error.message : String(error);
 85 |       console.error("[MCP Error]", error);
 86 |       
 87 |       // Only use MCP logging if connected
 88 |       if (this._isConnected) {
 89 |         this.log("error", "MCP server error", { error: errorMessage });
 90 |       }
 91 |     };
 92 |     
 93 |     // Set up additional debug event handlers if needed
 94 |     this.server.oninitialized = () => {
 95 |       this._isConnected = true;
 96 |       console.error("[DEBUG] Connection initialized");
 97 |       
 98 |       // We'll log initialization info in the run method after connection is fully established
 99 |       console.error("[DEBUG] Connection initialized");
100 |     };
101 |     
102 |     this.server.onclose = () => {
103 |       this._isConnected = false;
104 |       console.error("[DEBUG] Connection closed");
105 |       
106 |       // No MCP logging here since we're disconnected
107 |     };
108 |   }
109 | 
110 |   /**
111 |    * Adds a new script category to the framework.
112 |    * @param category - The script category to add.
113 |    */
114 |   addCategory(category: ScriptCategory): void {
115 |     this.categories.push(category);
116 |     
117 |     // Use console logging since this is called before server connection
118 |     console.error(`[DEBUG] Added category: ${category.name} (${category.scripts.length} scripts)`);
119 |     
120 |     // Store category info to log via MCP after connection
121 |     if (!this._pendingCategories) {
122 |       this._pendingCategories = [];
123 |     }
124 |     this._pendingCategories.push({
125 |       categoryName: category.name,
126 |       scriptCount: category.scripts.length,
127 |       description: category.description
128 |     });
129 |   }
130 | 
131 |   /**
132 |    * Logs a message with the specified severity level.
133 |    * Uses the MCP server's logging system to record events if available.
134 |    * Always logs to console for visibility.
135 |    * 
136 |    * @param level - The severity level of the log message following RFC 5424 syslog levels
137 |    * @param message - The message to log
138 |    * @param data - Optional additional data to include with the log message
139 |    * 
140 |    * @example
141 |    * // Log a debug message
142 |    * framework.log("debug", "Processing request", { requestId: "123" });
143 |    * 
144 |    * @example
145 |    * // Log an error
146 |    * framework.log("error", "Failed to execute script", { scriptName: "calendar_add" });
147 |    */
148 |   log(level: LogLevel, message: string, data?: Record<string, any>): void {
149 |     // Format for console output
150 |     const timestamp = new Date().toISOString();
151 |     const dataStr = data ? ` ${JSON.stringify(data)}` : '';
152 |     
153 |     // Always log to stderr for visibility
154 |     console.error(`[${timestamp}] [${level.toUpperCase()}] ${message}${dataStr}`);
155 |     
156 |     // Only try to use MCP logging if we're connected
157 |     if (this._isConnected) {
158 |       try {
159 |         this.server.sendLoggingMessage({
160 |           level: level,
161 |           message: message,
162 |           data: data || {},
163 |         });
164 |       } catch (error) {
165 |         // Silently ignore logging errors - we've already logged to console
166 |       }
167 |     }
168 |   }
169 | 
170 |   /**
171 |    * Executes an AppleScript and returns the result.
172 |    * @param script - The AppleScript to execute.
173 |    * @returns The result of the script execution.
174 |    * @throws Will throw an error if the script execution fails.
175 |    */
176 |   private async executeScript(script: string): Promise<string> {
177 |     // Log script execution (truncate long scripts for readability)
178 |     const scriptPreview = script.length > 100 ? script.substring(0, 100) + "..." : script;
179 |     this.log("debug", "Executing AppleScript", { scriptPreview });
180 |     
181 |     try {
182 |       const startTime = Date.now();
183 |       const { stdout } = await execAsync(
184 |         `osascript -e '${script.replace(/'/g, "'\"'\"'")}'`,
185 |       );
186 |       const executionTime = Date.now() - startTime;
187 |       
188 |       this.log("debug", "AppleScript executed successfully", { 
189 |         executionTimeMs: executionTime,
190 |         outputLength: stdout.length
191 |       });
192 |       
193 |       return stdout.trim();
194 |     } catch (error) {
195 |       // Properly type check the error object
196 |       let errorMessage = "Unknown error occurred";
197 |       if (error && typeof error === "object") {
198 |         if ("message" in error && typeof error.message === "string") {
199 |           errorMessage = error.message;
200 |         } else if (error instanceof Error) {
201 |           errorMessage = error.message;
202 |         }
203 |       } else if (typeof error === "string") {
204 |         errorMessage = error;
205 |       }
206 |       
207 |       this.log("error", "AppleScript execution failed", { 
208 |         error: errorMessage,
209 |         scriptPreview
210 |       });
211 |       
212 |       throw new Error(`AppleScript execution failed: ${errorMessage}`);
213 |     }
214 |   }
215 | 
216 |   /**
217 |    * Sets up request handlers for the server.
218 |    */
219 |   private setupHandlers(): void {
220 |     // List available tools
221 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
222 |       tools: this.categories.flatMap((category) =>
223 |         category.scripts.map((script) => ({
224 |           name: `${category.name}_${script.name}`, // Changed from dot to underscore
225 |           description: `[${category.description}] ${script.description}`,
226 |           inputSchema: script.schema || {
227 |             type: "object",
228 |             properties: {},
229 |           },
230 |         })),
231 |       ),
232 |     }));
233 | 
234 |     // Handle tool execution
235 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
236 |       const toolName = request.params.name;
237 |       this.log("info", "Tool execution requested", { 
238 |         tool: toolName,
239 |         hasArguments: !!request.params.arguments
240 |       });
241 |       
242 |       try {
243 |         // Split on underscore instead of dot
244 |         const [categoryName, ...scriptNameParts] =
245 |           toolName.split("_");
246 |         const scriptName = scriptNameParts.join("_"); // Rejoin in case script name has underscores
247 | 
248 |         const category = this.categories.find((c) => c.name === categoryName);
249 |         if (!category) {
250 |           this.log("warning", "Category not found", { categoryName });
251 |           throw new McpError(
252 |             ErrorCode.MethodNotFound,
253 |             `Category not found: ${categoryName}`,
254 |           );
255 |         }
256 | 
257 |         const script = category.scripts.find((s) => s.name === scriptName);
258 |         if (!script) {
259 |           this.log("warning", "Script not found", { 
260 |             categoryName, 
261 |             scriptName 
262 |           });
263 |           throw new McpError(
264 |             ErrorCode.MethodNotFound,
265 |             `Script not found: ${scriptName}`,
266 |           );
267 |         }
268 | 
269 |         this.log("debug", "Generating script content", { 
270 |           categoryName, 
271 |           scriptName,
272 |           isFunction: typeof script.script === "function"
273 |         });
274 |         
275 |         const scriptContent =
276 |           typeof script.script === "function"
277 |             ? script.script(request.params.arguments)
278 |             : script.script;
279 | 
280 |         const result = await this.executeScript(scriptContent);
281 |         
282 |         this.log("info", "Tool execution completed successfully", { 
283 |           tool: toolName,
284 |           resultLength: result.length
285 |         });
286 | 
287 |         return {
288 |           content: [
289 |             {
290 |               type: "text",
291 |               text: result,
292 |             },
293 |           ],
294 |         };
295 |       } catch (error) {
296 |         if (error instanceof McpError) {
297 |           this.log("error", "MCP error during tool execution", { 
298 |             tool: toolName,
299 |             errorCode: error.code,
300 |             errorMessage: error.message
301 |           });
302 |           throw error;
303 |         }
304 | 
305 |         let errorMessage = "Unknown error occurred";
306 |         if (error && typeof error === "object") {
307 |           if ("message" in error && typeof error.message === "string") {
308 |             errorMessage = error.message;
309 |           } else if (error instanceof Error) {
310 |             errorMessage = error.message;
311 |           }
312 |         } else if (typeof error === "string") {
313 |           errorMessage = error;
314 |         }
315 | 
316 |         this.log("error", "Error during tool execution", { 
317 |           tool: toolName,
318 |           errorMessage
319 |         });
320 | 
321 |         return {
322 |           content: [
323 |             {
324 |               type: "text",
325 |               text: `Error: ${errorMessage}`,
326 |             },
327 |           ],
328 |           isError: true,
329 |         };
330 |       }
331 |     });
332 |   }
333 | 
334 |   /**
335 |    * Runs the AppleScript framework server.
336 |    */
337 |   async run(): Promise<void> {
338 |     console.error("[INFO] Setting up request handlers");
339 |     this.setupHandlers();
340 |     
341 |     console.error("[INFO] Initializing StdioServerTransport");
342 |     const transport = new StdioServerTransport();
343 |     
344 |     try {
345 |       console.error("[INFO] Connecting server to transport");
346 |       await this.server.connect(transport);
347 |       this._isConnected = true;
348 |       
349 |       // Log server running status using console only
350 |       const totalScripts = this.categories.reduce((count, category) => count + category.scripts.length, 0);
351 |       console.error(`[NOTICE] AppleScript MCP server running with ${this.categories.length} categories and ${totalScripts} scripts`);
352 |       
353 |       console.error("AppleScript MCP server running");
354 |     } catch (error) {
355 |       let errorMessage = "Unknown error occurred";
356 |       if (error && typeof error === "object" && error instanceof Error) {
357 |         errorMessage = error.message;
358 |       }
359 |       
360 |       console.error("Failed to start AppleScript MCP server:", errorMessage);
361 |       throw error;
362 |     }
363 |   }
364 | }
365 | 
```

--------------------------------------------------------------------------------
/src/categories/notes.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ScriptCategory } from "../types/index.js";
  2 | 
  3 | /**
  4 |  * Generates HTML content for a note based on user input
  5 |  * @param args The arguments containing content specifications
  6 |  * @returns HTML string for the note
  7 |  */
  8 | function generateNoteHtml(args: any): string {
  9 |   const {
 10 |     title = "New Note",
 11 |     content = "",
 12 |     format = {
 13 |       headings: false,
 14 |       bold: false,
 15 |       italic: false,
 16 |       underline: false,
 17 |       links: false,
 18 |       lists: false
 19 |     }
 20 |   } = args;
 21 | 
 22 |   // Process content based on format options
 23 |   let processedContent = content;
 24 |   
 25 |   // If content contains markdown-like syntax and formatting is enabled, convert it
 26 |   if (format.headings) {
 27 |     // Convert # Heading to <h1>Heading</h1>, ## Heading to <h2>Heading</h2>, etc.
 28 |     processedContent = processedContent.replace(/^# (.+)$/gm, '<h1>$1</h1>');
 29 |     processedContent = processedContent.replace(/^## (.+)$/gm, '<h2>$1</h2>');
 30 |     processedContent = processedContent.replace(/^### (.+)$/gm, '<h3>$1</h3>');
 31 |   }
 32 |   
 33 |   if (format.bold) {
 34 |     // Convert **text** or __text__ to <b>text</b>
 35 |     processedContent = processedContent.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
 36 |     processedContent = processedContent.replace(/__(.+?)__/g, '<b>$1</b>');
 37 |   }
 38 |   
 39 |   if (format.italic) {
 40 |     // Convert *text* or _text_ to <i>text</i>
 41 |     processedContent = processedContent.replace(/\*(.+?)\*/g, '<i>$1</i>');
 42 |     processedContent = processedContent.replace(/_(.+?)_/g, '<i>$1</i>');
 43 |   }
 44 |   
 45 |   if (format.underline) {
 46 |     // Convert ~text~ to <u>text</u>
 47 |     processedContent = processedContent.replace(/~(.+?)~/g, '<u>$1</u>');
 48 |   }
 49 |   
 50 |   if (format.links) {
 51 |     // Convert [text](url) to <a href="url">text</a>
 52 |     processedContent = processedContent.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
 53 |   }
 54 |   
 55 |   if (format.lists) {
 56 |     // Handle unordered lists
 57 |     // Look for lines starting with - or * and convert to <li> items
 58 |     const listItems = processedContent.match(/^[*-] (.+)$/gm);
 59 |     if (listItems) {
 60 |       let listHtml = '<ul>';
 61 |       for (const item of listItems) {
 62 |         const content = item.replace(/^[*-] /, '');
 63 |         listHtml += `<li>${content}</li>`;
 64 |       }
 65 |       listHtml += '</ul>';
 66 |       
 67 |       // Replace the original list items with the HTML list
 68 |       for (const item of listItems) {
 69 |         processedContent = processedContent.replace(item, '');
 70 |       }
 71 |       processedContent = processedContent.replace(/\n+/g, '\n') + listHtml;
 72 |     }
 73 |     
 74 |     // Handle ordered lists (1. Item)
 75 |     const orderedItems = processedContent.match(/^\d+\. (.+)$/gm);
 76 |     if (orderedItems) {
 77 |       let listHtml = '<ol>';
 78 |       for (const item of orderedItems) {
 79 |         const content = item.replace(/^\d+\. /, '');
 80 |         listHtml += `<li>${content}</li>`;
 81 |       }
 82 |       listHtml += '</ol>';
 83 |       
 84 |       // Replace the original list items with the HTML list
 85 |       for (const item of orderedItems) {
 86 |         processedContent = processedContent.replace(item, '');
 87 |       }
 88 |       processedContent = processedContent.replace(/\n+/g, '\n') + listHtml;
 89 |     }
 90 |   }
 91 |   
 92 |   // Wrap paragraphs in <p> tags if they aren't already wrapped in HTML tags
 93 |   const paragraphs = processedContent.split('\n\n');
 94 |   processedContent = paragraphs
 95 |     .map((p: string) => {
 96 |       if (p.trim() && !p.trim().startsWith('<')) {
 97 |         return `<p>${p}</p>`;
 98 |       }
 99 |       return p;
100 |     })
101 |     .join('\n');
102 |   
103 |   return processedContent;
104 | }
105 | 
106 | export const notesCategory: ScriptCategory = {
107 |   name: "notes",
108 |   description: "Apple Notes operations",
109 |   scripts: [
110 |     {
111 |       name: "create",
112 |       description: "Create a new note with optional formatting",
113 |       script: (args) => {
114 |         const { title = "New Note", content = "", format = {} } = args;
115 |         const htmlContent = generateNoteHtml(args);
116 |         
117 |         return `
118 |           tell application "Notes"
119 |             make new note with properties {body:"${htmlContent}", name:"${title}"}
120 |           end tell
121 |         `;
122 |       },
123 |       schema: {
124 |         type: "object",
125 |         properties: {
126 |           title: {
127 |             type: "string",
128 |             description: "Title of the note"
129 |           },
130 |           content: {
131 |             type: "string",
132 |             description: "Content of the note, can include markdown-like syntax for formatting"
133 |           },
134 |           format: {
135 |             type: "object",
136 |             description: "Formatting options for the note content",
137 |             properties: {
138 |               headings: {
139 |                 type: "boolean",
140 |                 description: "Enable heading formatting (# Heading)"
141 |               },
142 |               bold: {
143 |                 type: "boolean",
144 |                 description: "Enable bold formatting (**text**)"
145 |               },
146 |               italic: {
147 |                 type: "boolean",
148 |                 description: "Enable italic formatting (*text*)"
149 |               },
150 |               underline: {
151 |                 type: "boolean",
152 |                 description: "Enable underline formatting (~text~)"
153 |               },
154 |               links: {
155 |                 type: "boolean",
156 |                 description: "Enable link formatting ([text](url))"
157 |               },
158 |               lists: {
159 |                 type: "boolean",
160 |                 description: "Enable list formatting (- item or 1. item)"
161 |               }
162 |             }
163 |           }
164 |         },
165 |         required: ["title", "content"]
166 |       }
167 |     },
168 |     {
169 |       name: "createRawHtml",
170 |       description: "Create a new note with direct HTML content",
171 |       script: (args) => {
172 |         const { title = "New Note", html = "" } = args;
173 |         
174 |         return `
175 |           tell application "Notes"
176 |             make new note with properties {body:"${html.replace(/"/g, '\\"')}", name:"${title}"}
177 |           end tell
178 |         `;
179 |       },
180 |       schema: {
181 |         type: "object",
182 |         properties: {
183 |           title: {
184 |             type: "string",
185 |             description: "Title of the note"
186 |           },
187 |           html: {
188 |             type: "string",
189 |             description: "Raw HTML content for the note"
190 |           }
191 |         },
192 |         required: ["title", "html"]
193 |       }
194 |     },
195 |     {
196 |       name: "list",
197 |       description: "List all notes or notes in a specific folder",
198 |       script: (args) => {
199 |         const { folder = "" } = args;
200 |         
201 |         if (folder) {
202 |           return `
203 |             tell application "Notes"
204 |               set folderList to folders whose name is "${folder}"
205 |               if length of folderList > 0 then
206 |                 set targetFolder to item 1 of folderList
207 |                 set noteNames to name of notes of targetFolder
208 |                 return noteNames as string
209 |               else
210 |                 return "Folder not found: ${folder}"
211 |               end if
212 |             end tell
213 |           `;
214 |         } else {
215 |           return `
216 |             tell application "Notes"
217 |               set noteNames to name of notes
218 |               return noteNames as string
219 |             end tell
220 |           `;
221 |         }
222 |       },
223 |       schema: {
224 |         type: "object",
225 |         properties: {
226 |           folder: {
227 |             type: "string",
228 |             description: "Optional folder name to list notes from"
229 |           }
230 |         }
231 |       }
232 |     },
233 |     {
234 |       name: "get",
235 |       description: "Get a specific note by title",
236 |       script: (args) => {
237 |         const { title, folder = "" } = args;
238 |         
239 |         if (folder) {
240 |           return `
241 |             tell application "Notes"
242 |               set folderList to folders whose name is "${folder}"
243 |               if length of folderList > 0 then
244 |                 set targetFolder to item 1 of folderList
245 |                 set matchingNotes to notes of targetFolder whose name is "${title}"
246 |                 if length of matchingNotes > 0 then
247 |                   set n to item 1 of matchingNotes
248 |                   set noteTitle to name of n
249 |                   set noteBody to body of n
250 |                   set noteCreationDate to creation date of n
251 |                   set noteModDate to modification date of n
252 |                   
253 |                   set jsonResult to "{\\"title\\": \\""
254 |                   set jsonResult to jsonResult & noteTitle & "\\""
255 |                   set jsonResult to jsonResult & ", \\"body\\": \\"" & noteBody & "\\""
256 |                   set jsonResult to jsonResult & ", \\"creationDate\\": \\"" & noteCreationDate & "\\""
257 |                   set jsonResult to jsonResult & ", \\"modificationDate\\": \\"" & noteModDate & "\\"}"
258 |                   
259 |                   return jsonResult
260 |                 else
261 |                   return "Note not found: ${title}"
262 |                 end if
263 |               else
264 |                 return "Folder not found: ${folder}"
265 |               end if
266 |             end tell
267 |           `;
268 |         } else {
269 |           return `
270 |             tell application "Notes"
271 |               set matchingNotes to notes whose name is "${title}"
272 |               if length of matchingNotes > 0 then
273 |                 set n to item 1 of matchingNotes
274 |                 set noteTitle to name of n
275 |                 set noteBody to body of n
276 |                 set noteCreationDate to creation date of n
277 |                 set noteModDate to modification date of n
278 |                 
279 |                 set jsonResult to "{\\"title\\": \\""
280 |                 set jsonResult to jsonResult & noteTitle & "\\""
281 |                 set jsonResult to jsonResult & ", \\"body\\": \\"" & noteBody & "\\""
282 |                 set jsonResult to jsonResult & ", \\"creationDate\\": \\"" & noteCreationDate & "\\""
283 |                 set jsonResult to jsonResult & ", \\"modificationDate\\": \\"" & noteModDate & "\\"}"
284 |                 
285 |                 return jsonResult
286 |               else
287 |                 return "Note not found: ${title}"
288 |               end if
289 |             end tell
290 |           `;
291 |         }
292 |       },
293 |       schema: {
294 |         type: "object",
295 |         properties: {
296 |           title: {
297 |             type: "string",
298 |             description: "Title of the note to retrieve"
299 |           },
300 |           folder: {
301 |             type: "string",
302 |             description: "Optional folder name to search in"
303 |           }
304 |         },
305 |         required: ["title"]
306 |       }
307 |     },
308 |     {
309 |       name: "search",
310 |       description: "Search for notes containing specific text",
311 |       script: (args) => {
312 |         const { query, folder = "", limit = 5, includeBody = true } = args;
313 |         
314 |         if (folder) {
315 |           return `
316 |             tell application "Notes"
317 |               set folderList to folders whose name is "${folder}"
318 |               if length of folderList > 0 then
319 |                 set targetFolder to item 1 of folderList
320 |                 set matchingNotes to {}
321 |                 set allNotes to notes of targetFolder
322 |                 repeat with n in allNotes
323 |                   if name of n contains "${query}" or body of n contains "${query}" then
324 |                     set end of matchingNotes to n
325 |                   end if
326 |                 end repeat
327 |                 
328 |                 set resultCount to length of matchingNotes
329 |                 if resultCount > ${limit} then set resultCount to ${limit}
330 |                 
331 |                 set jsonResult to "["
332 |                 repeat with i from 1 to resultCount
333 |                   set n to item i of matchingNotes
334 |                   set noteTitle to name of n
335 |                   set noteCreationDate to creation date of n
336 |                   set noteModDate to modification date of n
337 |                   ${includeBody ? 'set noteBody to body of n' : ''}
338 |                   
339 |                   set noteJson to "{\\"title\\": \\""
340 |                   set noteJson to noteJson & noteTitle & "\\""
341 |                   ${includeBody ? 'set noteJson to noteJson & ", \\"body\\": \\"" & noteBody & "\\""' : ''}
342 |                   set noteJson to noteJson & ", \\"creationDate\\": \\"" & noteCreationDate & "\\""
343 |                   set noteJson to noteJson & ", \\"modificationDate\\": \\"" & noteModDate & "\\"}"
344 |                   
345 |                   set jsonResult to jsonResult & noteJson
346 |                   if i < resultCount then set jsonResult to jsonResult & ", "
347 |                 end repeat
348 |                 set jsonResult to jsonResult & "]"
349 |                 
350 |                 return jsonResult
351 |               else
352 |                 return "Folder not found: ${folder}"
353 |               end if
354 |             end tell
355 |           `;
356 |         } else {
357 |           return `
358 |             tell application "Notes"
359 |               set matchingNotes to {}
360 |               set allNotes to notes
361 |               repeat with n in allNotes
362 |                 if name of n contains "${query}" or body of n contains "${query}" then
363 |                   set end of matchingNotes to n
364 |                 end if
365 |               end repeat
366 |               
367 |               set resultCount to length of matchingNotes
368 |               if resultCount > ${limit} then set resultCount to ${limit}
369 |               
370 |               set jsonResult to "["
371 |               repeat with i from 1 to resultCount
372 |                 set n to item i of matchingNotes
373 |                 set noteTitle to name of n
374 |                 set noteCreationDate to creation date of n
375 |                 set noteModDate to modification date of n
376 |                 ${includeBody ? 'set noteBody to body of n' : ''}
377 |                 
378 |                 set noteJson to "{\\"title\\": \\""
379 |                 set noteJson to noteJson & noteTitle & "\\""
380 |                 ${includeBody ? 'set noteJson to noteJson & ", \\"body\\": \\"" & noteBody & "\\""' : ''}
381 |                 set noteJson to noteJson & ", \\"creationDate\\": \\"" & noteCreationDate & "\\""
382 |                 set noteJson to noteJson & ", \\"modificationDate\\": \\"" & noteModDate & "\\"}"
383 |                 
384 |                 set jsonResult to jsonResult & noteJson
385 |                 if i < resultCount then set jsonResult to jsonResult & ", "
386 |               end repeat
387 |               set jsonResult to jsonResult & "]"
388 |               
389 |               return jsonResult
390 |             end tell
391 |           `;
392 |         }
393 |       },
394 |       schema: {
395 |         type: "object",
396 |         properties: {
397 |           query: {
398 |             type: "string",
399 |             description: "Text to search for in notes (title and body)"
400 |           },
401 |           folder: {
402 |             type: "string",
403 |             description: "Optional folder name to search in"
404 |           },
405 |           limit: {
406 |             type: "number",
407 |             description: "Maximum number of results to return (default: 5)"
408 |           },
409 |           includeBody: {
410 |             type: "boolean",
411 |             description: "Whether to include note body in results (default: true)"
412 |           }
413 |         },
414 |         required: ["query"]
415 |       }
416 |     }
417 |   ]
418 | };
```

--------------------------------------------------------------------------------
/src/categories/mail.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ScriptCategory } from "../types/index.js";
  2 | 
  3 | /**
  4 |  * Mail-related scripts.
  5 |  * * create_email: Create a new email in Mail.app with specified recipient, subject, and body
  6 |  * * list_emails: List emails from a specified mailbox in Mail.app
  7 |  * * get_email: Get a specific email by ID or search criteria from Mail.app
  8 |  */
  9 | export const mailCategory: ScriptCategory = {
 10 |   name: "mail",
 11 |   description: "Mail operations",
 12 |   scripts: [
 13 |     {
 14 |       name: "create_email",
 15 |       description: "Create a new email in Mail.app",
 16 |       schema: {
 17 |         type: "object",
 18 |         properties: {
 19 |           recipient: {
 20 |             type: "string",
 21 |             description: "Email recipient",
 22 |           },
 23 |           subject: {
 24 |             type: "string",
 25 |             description: "Email subject",
 26 |           },
 27 |           body: {
 28 |             type: "string",
 29 |             description: "Email body",
 30 |           },
 31 |         },
 32 |         required: ["recipient", "subject", "body"],
 33 |       },
 34 |       script: (args) => `
 35 |         set recipient to "${args.recipient}"
 36 |         set subject to "${args.subject}"
 37 |         set body to "${args.body}"
 38 | 
 39 |         -- URL encode subject and body
 40 |         set encodedSubject to my urlEncode(subject)
 41 |         set encodedBody to my urlEncode(body)
 42 | 
 43 |         -- Construct the mailto URL
 44 |         set mailtoURL to "mailto:" & recipient & "?subject=" & encodedSubject & "&body=" & encodedBody
 45 | 
 46 |         -- Use Apple Mail's 'mailto' command to create the email
 47 |         tell application "Mail"
 48 |           mailto mailtoURL
 49 |           activate
 50 |         end tell
 51 | 
 52 |         -- Handler to URL-encode text
 53 |         on urlEncode(theText)
 54 |           set theEncodedText to ""
 55 |           set theChars to every character of theText
 56 |           repeat with aChar in theChars
 57 |             set charCode to ASCII number aChar
 58 |             if charCode = 32 then
 59 |               set theEncodedText to theEncodedText & "%20" -- Space
 60 |             else if (charCode ≥ 48 and charCode ≤ 57) or (charCode ≥ 65 and charCode ≤ 90) or (charCode ≥ 97 and charCode ≤ 122) or charCode = 45 or charCode = 46 or charCode = 95 or charCode = 126 then
 61 |               -- Allowed characters: A-Z, a-z, 0-9, -, ., _, ~
 62 |               set theEncodedText to theEncodedText & aChar
 63 |             else
 64 |               -- Convert to %HH format
 65 |               set hexCode to do shell script "printf '%02X' " & charCode
 66 |               set theEncodedText to theEncodedText & "%" & hexCode
 67 |             end if
 68 |           end repeat
 69 |           return theEncodedText
 70 |         end urlEncode
 71 |       `,
 72 |     },
 73 |     {
 74 |       name: "list_emails",
 75 |       description: "List emails from a specified mailbox in Mail.app",
 76 |       schema: {
 77 |         type: "object",
 78 |         properties: {
 79 |           mailbox: {
 80 |             type: "string",
 81 |             description: "Name of the mailbox to list emails from (e.g., 'Inbox', 'Sent')",
 82 |             default: "Inbox"
 83 |           },
 84 |           account: {
 85 |             type: "string",
 86 |             description: "Name of the account to search in (e.g., 'iCloud', 'Gmail', 'Exchange'). If not specified, searches all accounts with preference for iCloud.",
 87 |             default: "iCloud"
 88 |           },
 89 |           count: {
 90 |             type: "number",
 91 |             description: "Maximum number of emails to retrieve",
 92 |             default: 10
 93 |           },
 94 |           unreadOnly: {
 95 |             type: "boolean",
 96 |             description: "Only show unread emails if true"
 97 |           }
 98 |         }
 99 |       },
100 |       script: (args) => `
101 |         set mailboxName to "${args.mailbox || 'Inbox'}"
102 |         set accountName to "${args.account || 'iCloud'}"
103 |         set messageCount to ${args.count || 10}
104 |         set showUnreadOnly to ${args.unreadOnly ? 'true' : 'false'}
105 |         set searchAllAccounts to ${!args.account ? 'true' : 'false'}
106 |         
107 |         tell application "Mail"
108 |           -- Get all messages if no specific mailbox is found
109 |           set foundMailbox to false
110 |           set emailMessages to {}
111 |           set targetAccount to missing value
112 |           
113 |           -- First try to find the specified account
114 |           if not searchAllAccounts then
115 |             try
116 |               set allAccounts to every account
117 |               repeat with acct in allAccounts
118 |                 if name of acct is accountName then
119 |                   set targetAccount to acct
120 |                   exit repeat
121 |                 end if
122 |               end repeat
123 |             end try
124 |             
125 |             -- If account not found, set to search all accounts
126 |             if targetAccount is missing value then
127 |               set searchAllAccounts to true
128 |             end if
129 |           end if
130 |           
131 |           -- If specific account is found, search in that account
132 |           if not searchAllAccounts and targetAccount is not missing value then
133 |             try
134 |               set acctMailboxes to every mailbox of targetAccount
135 |               repeat with m in acctMailboxes
136 |                 if name of m is mailboxName then
137 |                   set targetMailbox to m
138 |                   set foundMailbox to true
139 |                   
140 |                   -- Get messages from the found mailbox
141 |                   if showUnreadOnly then
142 |                     set emailMessages to (messages of targetMailbox whose read status is false)
143 |                   else
144 |                     set emailMessages to (messages of targetMailbox)
145 |                   end if
146 |                   
147 |                   exit repeat
148 |                 end if
149 |               end repeat
150 |               
151 |               -- If mailbox not found in specified account, try to get inbox
152 |               if not foundMailbox then
153 |                 try
154 |                   set inboxMailbox to inbox of targetAccount
155 |                   set targetMailbox to inboxMailbox
156 |                   set foundMailbox to true
157 |                   
158 |                   if showUnreadOnly then
159 |                     set emailMessages to (messages of targetMailbox whose read status is false)
160 |                   else
161 |                     set emailMessages to (messages of targetMailbox)
162 |                   end if
163 |                 end try
164 |               end if
165 |             end try
166 |           else
167 |             -- Search all accounts, with preference for iCloud
168 |             set iCloudAccount to missing value
169 |             set allAccounts to every account
170 |             
171 |             -- First look for iCloud account
172 |             repeat with acct in allAccounts
173 |               if name of acct is "iCloud" then
174 |                 set iCloudAccount to acct
175 |                 exit repeat
176 |               end if
177 |             end repeat
178 |             
179 |             -- Try to find the mailbox directly
180 |             try
181 |               set allMailboxes to every mailbox
182 |               repeat with m in allMailboxes
183 |                 if name of m is mailboxName then
184 |                   set targetMailbox to m
185 |                   set foundMailbox to true
186 |                   
187 |                   -- Get messages from the found mailbox
188 |                   if showUnreadOnly then
189 |                     set emailMessages to (messages of targetMailbox whose read status is false)
190 |                   else
191 |                     set emailMessages to (messages of targetMailbox)
192 |                   end if
193 |                   
194 |                   exit repeat
195 |                 end if
196 |               end repeat
197 |             end try
198 |             
199 |             -- If not found directly, try to find it in each account (prioritize iCloud)
200 |             if not foundMailbox and iCloudAccount is not missing value then
201 |               try
202 |                 set acctMailboxes to every mailbox of iCloudAccount
203 |                 repeat with m in acctMailboxes
204 |                   if name of m is mailboxName then
205 |                     set targetMailbox to m
206 |                     set foundMailbox to true
207 |                     
208 |                     -- Get messages from the found mailbox
209 |                     if showUnreadOnly then
210 |                       set emailMessages to (messages of targetMailbox whose read status is false)
211 |                     else
212 |                       set emailMessages to (messages of targetMailbox)
213 |                     end if
214 |                     
215 |                     exit repeat
216 |                   end if
217 |                 end repeat
218 |               end try
219 |             end if
220 |             
221 |             -- If still not found in iCloud, check other accounts
222 |             if not foundMailbox then
223 |               repeat with acct in allAccounts
224 |                 if acct is not iCloudAccount then
225 |                   try
226 |                     set acctMailboxes to every mailbox of acct
227 |                     repeat with m in acctMailboxes
228 |                       if name of m is mailboxName then
229 |                         set targetMailbox to m
230 |                         set foundMailbox to true
231 |                         
232 |                         -- Get messages from the found mailbox
233 |                         if showUnreadOnly then
234 |                           set emailMessages to (messages of targetMailbox whose read status is false)
235 |                         else
236 |                           set emailMessages to (messages of targetMailbox)
237 |                         end if
238 |                         
239 |                         exit repeat
240 |                       end if
241 |                     end repeat
242 |                     
243 |                     if foundMailbox then exit repeat
244 |                   end try
245 |                 end if
246 |               end repeat
247 |             end if
248 |           end if
249 |           
250 |           -- If still not found, get messages from all inboxes
251 |           if not foundMailbox then
252 |             set emailMessages to {}
253 |             set allAccounts to every account
254 |             set accountsChecked to 0
255 |             
256 |             -- First check iCloud if available
257 |             repeat with acct in allAccounts
258 |               if name of acct is "iCloud" then
259 |                 try
260 |                   -- Try to get the inbox for iCloud
261 |                   set inboxMailbox to inbox of acct
262 |                   
263 |                   -- Add messages from this inbox
264 |                   if showUnreadOnly then
265 |                     set acctMessages to (messages of inboxMailbox whose read status is false)
266 |                   else
267 |                     set acctMessages to (messages of inboxMailbox)
268 |                   end if
269 |                   
270 |                   set emailMessages to emailMessages & acctMessages
271 |                   set accountsChecked to accountsChecked + 1
272 |                 end try
273 |                 exit repeat
274 |               end if
275 |             end repeat
276 |             
277 |             -- Then check other accounts if needed
278 |             if accountsChecked is 0 then
279 |               repeat with acct in allAccounts
280 |                 try
281 |                   -- Try to get the inbox for this account
282 |                   set inboxMailbox to inbox of acct
283 |                   
284 |                   -- Add messages from this inbox
285 |                   if showUnreadOnly then
286 |                     set acctMessages to (messages of inboxMailbox whose read status is false)
287 |                   else
288 |                     set acctMessages to (messages of inboxMailbox)
289 |                   end if
290 |                   
291 |                   set emailMessages to emailMessages & acctMessages
292 |                 end try
293 |               end repeat
294 |             end if
295 |             
296 |             -- Sort combined messages by date (newest first)
297 |             set emailMessages to my sortMessagesByDate(emailMessages)
298 |             set mailboxName to "All Inboxes"
299 |           end if
300 |           
301 |           -- Limit the number of messages
302 |           if (count of emailMessages) > messageCount then
303 |             set emailMessages to items 1 thru messageCount of emailMessages
304 |           end if
305 |           
306 |           -- Format the results
307 |           set accountInfo to ""
308 |           if not searchAllAccounts and targetAccount is not missing value then
309 |             set accountInfo to " (" & accountName & ")"
310 |           end if
311 |           
312 |           set emailList to "Recent emails in " & mailboxName & accountInfo & ":" & return & return
313 |           
314 |           if (count of emailMessages) is 0 then
315 |             set emailList to emailList & "No messages found."
316 |           else
317 |             repeat with theMessage in emailMessages
318 |               try
319 |                 set msgSubject to subject of theMessage
320 |                 set msgSender to sender of theMessage
321 |                 set msgDate to date received of theMessage
322 |                 set msgRead to read status of theMessage
323 |                 
324 |                 -- Try to get account name for this message
325 |                 set msgAccount to ""
326 |                 try
327 |                   set msgMailbox to mailbox of theMessage
328 |                   set msgAcct to account of msgMailbox
329 |                   set msgAccount to " [" & name of msgAcct & "]"
330 |                 end try
331 |                 
332 |                 set emailList to emailList & "From: " & msgSender & return
333 |                 set emailList to emailList & "Subject: " & msgSubject & return
334 |                 set emailList to emailList & "Date: " & msgDate & msgAccount & return
335 |                 set emailList to emailList & "Read: " & msgRead & return & return
336 |               on error errMsg
337 |                 set emailList to emailList & "Error processing message: " & errMsg & return & return
338 |               end try
339 |             end repeat
340 |           end if
341 |           
342 |           return emailList
343 |         end tell
344 |         
345 |         -- Helper function to sort messages by date
346 |         on sortMessagesByDate(messageList)
347 |           tell application "Mail"
348 |             set sortedMessages to {}
349 |             
350 |             -- Simple bubble sort by date received (newest first)
351 |             repeat with i from 1 to count of messageList
352 |               set currentMsg to item i of messageList
353 |               set currentDate to date received of currentMsg
354 |               set inserted to false
355 |               
356 |               if (count of sortedMessages) is 0 then
357 |                 set sortedMessages to {currentMsg}
358 |               else
359 |                 repeat with j from 1 to count of sortedMessages
360 |                   set compareMsg to item j of sortedMessages
361 |                   set compareDate to date received of compareMsg
362 |                   
363 |                   if currentDate > compareDate then
364 |                     if j is 1 then
365 |                       set sortedMessages to {currentMsg} & sortedMessages
366 |                     else
367 |                       set sortedMessages to (items 1 thru (j - 1) of sortedMessages) & currentMsg & (items j thru (count of sortedMessages) of sortedMessages)
368 |                     end if
369 |                     set inserted to true
370 |                     exit repeat
371 |                   end if
372 |                 end repeat
373 |                 
374 |                 if not inserted then
375 |                   set sortedMessages to sortedMessages & {currentMsg}
376 |                 end if
377 |               end if
378 |             end repeat
379 |             
380 |             return sortedMessages
381 |           end tell
382 |         end sortMessagesByDate
383 |       `,
384 |     },
385 |     {
386 |       name: "get_email",
387 |       description: "Get a specific email by search criteria from Mail.app",
388 |       schema: {
389 |         type: "object",
390 |         properties: {
391 |           mailbox: {
392 |             type: "string",
393 |             description: "Name of the mailbox to search in (e.g., 'Inbox', 'Sent')",
394 |             default: "Inbox"
395 |           },
396 |           account: {
397 |             type: "string",
398 |             description: "Name of the account to search in (e.g., 'iCloud', 'Gmail', 'Exchange'). If not specified, searches all accounts with preference for iCloud.",
399 |             default: "iCloud"
400 |           },
401 |           subject: {
402 |             type: "string",
403 |             description: "Subject text to search for (partial match)"
404 |           },
405 |           sender: {
406 |             type: "string",
407 |             description: "Sender email or name to search for (partial match)"
408 |           },
409 |           dateReceived: {
410 |             type: "string",
411 |             description: "Date received to search for (format: YYYY-MM-DD)"
412 |           },
413 |           unreadOnly: {
414 |             type: "boolean",
415 |             description: "Only search unread emails if true"
416 |           },
417 |           includeBody: {
418 |             type: "boolean",
419 |             description: "Include email body in the result if true",
420 |             default: false
421 |           }
422 |         },
423 |         required: []
424 |       },
425 |       script: (args) => `
426 |         set mailboxName to "${args.mailbox || 'Inbox'}"
427 |         set accountName to "${args.account || 'iCloud'}"
428 |         set searchSubject to "${args.subject || ''}"
429 |         set searchSender to "${args.sender || ''}"
430 |         set searchDate to "${args.dateReceived || ''}"
431 |         set showUnreadOnly to ${args.unreadOnly ? 'true' : 'false'}
432 |         set includeBody to ${args.includeBody ? 'true' : 'false'}
433 |         set searchAllAccounts to ${!args.account ? 'true' : 'false'}
434 |         
435 |         tell application "Mail"
436 |           -- Get all messages if no specific mailbox is found
437 |           set foundMailbox to false
438 |           set emailMessages to {}
439 |           set targetAccount to missing value
440 |           
441 |           -- First try to find the specified account
442 |           if not searchAllAccounts then
443 |             try
444 |               set allAccounts to every account
445 |               repeat with acct in allAccounts
446 |                 if name of acct is accountName then
447 |                   set targetAccount to acct
448 |                   exit repeat
449 |                 end if
450 |               end repeat
451 |             end try
452 |             
453 |             -- If account not found, set to search all accounts
454 |             if targetAccount is missing value then
455 |               set searchAllAccounts to true
456 |             end if
457 |           end if
458 |           
459 |           -- If specific account is found, search in that account
460 |           if not searchAllAccounts and targetAccount is not missing value then
461 |             try
462 |               set acctMailboxes to every mailbox of targetAccount
463 |               repeat with m in acctMailboxes
464 |                 if name of m is mailboxName then
465 |                   set targetMailbox to m
466 |                   set foundMailbox to true
467 |                   
468 |                   -- Get messages from the found mailbox
469 |                   if showUnreadOnly then
470 |                     set emailMessages to (messages of targetMailbox whose read status is false)
471 |                   else
472 |                     set emailMessages to (messages of targetMailbox)
473 |                   end if
474 |                   
475 |                   exit repeat
476 |                 end if
477 |               end repeat
478 |             end try
479 |           else
480 |             -- Search all accounts, with preference for iCloud
481 |             set iCloudAccount to missing value
482 |             set allAccounts to every account
483 |             
484 |             -- First look for iCloud account
485 |             repeat with acct in allAccounts
486 |               if name of acct is "iCloud" then
487 |                 set iCloudAccount to acct
488 |                 exit repeat
489 |               end if
490 |             end repeat
491 |             
492 |             -- Try to find the mailbox directly
493 |             try
494 |               set allMailboxes to every mailbox
495 |               repeat with m in allMailboxes
496 |                 if name of m is mailboxName then
497 |                   set targetMailbox to m
498 |                   set foundMailbox to true
499 |                   
500 |                   -- Get messages from the found mailbox
501 |                   if showUnreadOnly then
502 |                     set emailMessages to (messages of targetMailbox whose read status is false)
503 |                   else
504 |                     set emailMessages to (messages of targetMailbox)
505 |                   end if
506 |                   
507 |                   exit repeat
508 |                 end if
509 |               end repeat
510 |             end try
511 |           end if
512 |           
513 |           -- Filter messages based on search criteria
514 |           set filteredMessages to {}
515 |           
516 |           repeat with theMessage in emailMessages
517 |             try
518 |               set matchesSubject to true
519 |               set matchesSender to true
520 |               set matchesDate to true
521 |               
522 |               -- Check subject if specified
523 |               if searchSubject is not "" then
524 |                 set msgSubject to subject of theMessage
525 |                 if msgSubject does not contain searchSubject then
526 |                   set matchesSubject to false
527 |                 end if
528 |               end if
529 |               
530 |               -- Check sender if specified
531 |               if searchSender is not "" then
532 |                 set msgSender to sender of theMessage
533 |                 if msgSender does not contain searchSender then
534 |                   set matchesSender to false
535 |                 end if
536 |               end if
537 |               
538 |               -- Check date if specified
539 |               if searchDate is not "" then
540 |                 set msgDate to date received of theMessage
541 |                 set msgDateString to (year of msgDate as string) & "-" & my padNumber(month of msgDate as integer) & "-" & my padNumber(day of msgDate as integer)
542 |                 if msgDateString is not searchDate then
543 |                   set matchesDate to false
544 |                 end if
545 |               end if
546 |               
547 |               -- Add to filtered list if all criteria match
548 |               if matchesSubject and matchesSender and matchesDate then
549 |                 set end of filteredMessages to theMessage
550 |               end if
551 |             end try
552 |           end repeat
553 |           
554 |           -- Format the results
555 |           set emailList to "Search results:" & return & return
556 |           
557 |           if (count of filteredMessages) is 0 then
558 |             set emailList to emailList & "No matching emails found."
559 |           else
560 |             repeat with theMessage in filteredMessages
561 |               try
562 |                 set msgSubject to subject of theMessage
563 |                 set msgSender to sender of theMessage
564 |                 set msgDate to date received of theMessage
565 |                 set msgRead to read status of theMessage
566 |                 
567 |                 -- Try to get account name for this message
568 |                 set msgAccount to ""
569 |                 try
570 |                   set msgMailbox to mailbox of theMessage
571 |                   set msgAcct to account of msgMailbox
572 |                   set msgAccount to " [" & name of msgAcct & "]"
573 |                 end try
574 |                 
575 |                 set emailList to emailList & "From: " & msgSender & return
576 |                 set emailList to emailList & "Subject: " & msgSubject & return
577 |                 set emailList to emailList & "Date: " & msgDate & msgAccount & return
578 |                 set emailList to emailList & "Read: " & msgRead & return
579 |                 
580 |                 -- Include body if requested
581 |                 if includeBody then
582 |                   set msgContent to content of theMessage
583 |                   set emailList to emailList & "Content: " & return & msgContent & return
584 |                 end if
585 |                 
586 |                 set emailList to emailList & return
587 |               on error errMsg
588 |                 set emailList to emailList & "Error processing message: " & errMsg & return & return
589 |               end try
590 |             end repeat
591 |           end if
592 |           
593 |           return emailList
594 |         end tell
595 |         
596 |         -- Helper function to pad numbers with leading zero if needed
597 |         on padNumber(num)
598 |           if num < 10 then
599 |             return "0" & num
600 |           else
601 |             return num as string
602 |           end if
603 |         end padNumber
604 |       `,
605 |     },
606 |   ],
607 | };
608 | 
```