#
tokens: 21743/50000 20/20 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
node_modules/
build/
*.log
.env*
/dist
.DS_Store
helps/*
```

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

```markdown
# applescript-mcp MCP Server

A Model Context Protocol server that enables LLM applications to interact with macOS through AppleScript.
This server provides a standardized interface for AI applications to control system functions, manage files, handle notifications, and more.

[![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)

<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>

## Features

- 🗓️ Calendar management (events, reminders)
- 📋 Clipboard operations
- 🔍 Finder integration
- 🔔 System notifications
- ⚙️ System controls (volume, dark mode, apps)
- 📟 iTerm terminal integration
- 📬 Mail (create new email, list emails, get email)
- 🔄 Shortcuts automation
- 💬 Messages (list chats, get messages, search messages, send a message)
- 🗒️ Notes (create formatted notes, list notes, search notes)
- 📄 Pages (create documents)

### Planned Features

- 🧭 Safari (open in Safari, save page content, get selected page/tab)
- ✅ Reminders (create, get)

## Prerequisites

- macOS 10.15 or later
- Node.js 18 or later

## Available Categories

### Calendar

| Command | Description           | Parameters                                          |
| ------- | --------------------- | --------------------------------------------------- |
| `add`   | Create calendar event | `title`, `startDate`, `endDate`, `calendar` (optional) |
| `list`  | List today's events   | None                                                |

#### Examples

```
// Create a new calendar event
Create a calendar event titled "Team Meeting" starting tomorrow at 2pm for 1 hour

// List today's events
What events do I have scheduled for today?
```

### Clipboard

| Command           | Description            | Parameters |
| ----------------- | ---------------------- | ---------- |
| `set_clipboard`   | Copy to clipboard      | `content`  |
| `get_clipboard`   | Get clipboard contents | None       |
| `clear_clipboard` | Clear clipboard        | None       |

#### Examples

```
// Copy text to clipboard
Copy "Remember to buy groceries" to my clipboard

// Get clipboard contents
What's currently in my clipboard?

// Clear clipboard
Clear my clipboard
```

### Finder

| Command              | Description        | Parameters                     |
| -------------------- | ------------------ | ------------------------------ |
| `get_selected_files` | Get selected files | None                           |
| `search_files`       | Search for files   | `query`, `location` (optional) |
| `quick_look`         | Preview file       | `path`                         |

#### Examples

```
// Get selected files in Finder
What files do I currently have selected in Finder?

// Search for files
Find all PDF files in my Documents folder

// Preview a file
Show me a preview of ~/Documents/report.pdf
```

### Notifications

> Note: Sending notification requires that you enable notifications in System Settings > Notifications > Script Editor.

| Command                 | Description       | Parameters                             |
| ----------------------- | ----------------- | -------------------------------------- |
| `send_notification`     | Show notification | `title`, `message`, `sound` (optional) |
| `toggle_do_not_disturb` | Toggle DND mode   | None                                   |

#### Examples

```
// Send a notification
Send me a notification with the title "Reminder" and message "Time to take a break"

// Toggle Do Not Disturb
Turn on Do Not Disturb mode
```

### System

| Command             | Description       | Parameters                 |
| ------------------- | ----------------- | -------------------------- |
| `volume`            | Set system volume | `level` (0-100)            |
| `get_frontmost_app` | Get active app    | None                       |
| `launch_app`        | Open application  | `name`                     |
| `quit_app`          | Close application | `name`, `force` (optional) |
| `toggle_dark_mode`  | Toggle dark mode  | None                       |

#### Examples

```
// Set system volume
Set my Mac's volume to 50%

// Get active application
What app am I currently using?

// Launch an application
Open Safari

// Quit an application
Close Spotify

// Toggle dark mode
Switch to dark mode
```

### iTerm

| Command           | Description     | Parameters                        |
| ----------------- | --------------- | --------------------------------- |
| `paste_clipboard` | Paste to iTerm  | None                              |
| `run`             | Execute command | `command`, `newWindow` (optional) |

#### Examples

```
// Paste clipboard to iTerm
Paste my clipboard contents into iTerm

// Run a command in iTerm
Run "ls -la" in iTerm

// Run a command in a new iTerm window
Run "top" in a new iTerm window
```

### Shortcuts

| Command          | Description                                | Parameters                                           |
| ---------------- | ------------------------------------------ | ---------------------------------------------------- |
| `run_shortcut`   | Run a shortcut                             | `name`, `input` (optional)                           |
| `list_shortcuts` | List all available shortcuts               | `limit` (optional)                                   |

#### Examples

```
// List available shortcuts
List all my available shortcuts

// List with limit
Show me my top 5 shortcuts

// Run a shortcut
Run my "Daily Note in Bear" shortcut

// Run a shortcut with input
Run my "Add to-do" shortcut with input "Buy groceries"
```

### Mail

| Command       | Description                      | Parameters                                                |
| ------------- | -------------------------------- | --------------------------------------------------------- |
| `create_email`| Create a new email in Mail.app   | `recipient`, `subject`, `body`                            |
| `list_emails` | List emails from a mailbox       | `mailbox` (optional), `count` (optional), `unreadOnly` (optional) |
| `get_email`   | Get a specific email by search   | `subject` (optional), `sender` (optional), `dateReceived` (optional), `mailbox` (optional), `account` (optional), `unreadOnly` (optional), `includeBody` (optional) |

#### Examples

```
// Create a new email
Compose an email to [email protected] with subject "Meeting Tomorrow" and body "Hi John, Can we meet tomorrow at 2pm?"

// List emails
Show me my 10 most recent unread emails

// Get a specific email
Find the email from [email protected] about "Project Update"
```

### Messages

| Command           | Description                                  | Parameters                                                |
| ----------------- | -------------------------------------------- | --------------------------------------------------------- |
| `list_chats`      | List available iMessage and SMS chats        | `includeParticipantDetails` (optional, default: false)    |
| `get_messages`    | Get messages from the Messages app           | `limit` (optional, default: 100)                          |
| `search_messages` | Search for messages containing specific text | `searchText`, `sender` (optional), `chatId` (optional), `limit` (optional, default: 50), `daysBack` (optional, default: 30) |
| `compose_message` | Open Messages app with pre-filled message or auto-send   | `recipient` (required), `body` (optional), `auto` (optional, default: false) |

#### Examples

```
// List available chats
Show me my recent message conversations

// Get recent messages
Show me my last 20 messages

// Search messages
Find messages containing "dinner plans" from John in the last week

// Compose a message
Send a message to 555-123-4567 saying "I'll be there in 10 minutes"
```

### Notes

| Command           | Description                                  | Parameters                                                |
| ----------------- | -------------------------------------------- | --------------------------------------------------------- |
| `create`          | Create a note with markdown-like formatting  | `title`, `content`, `format` (optional with formatting options) |
| `createRawHtml`   | Create a note with direct HTML content       | `title`, `html`                                           |
| `list`            | List notes, optionally from a specific folder| `folder` (optional)                                       |
| `get`             | Get a specific note by title                 | `title`, `folder` (optional)                              |
| `search`          | Search for notes containing specific text    | `query`, `folder` (optional), `limit` (optional, default: 5), `includeBody` (optional, default: true) |

#### Examples

```
// Create a new note with markdown formatting
Create a note titled "Meeting Minutes" with content "# Discussion Points\n- Project timeline\n- Budget review\n- Next steps" and format headings and lists

// Create a note with HTML
Create a note titled "Formatted Report" with HTML content "<h1>Quarterly Report</h1><p>Sales increased by <strong>15%</strong></p>"

// List notes
Show me all my notes in the "Work" folder

// Get a specific note
Show me my note titled "Shopping List"

// Search notes
Find notes containing "recipe" in my "Cooking" folder
```

### Pages

| Command            | Description                                  | Parameters                                                |
| ----------------- | -------------------------------------------- | --------------------------------------------------------- |
| `create_document` | Create a new Pages document with plain text  | `content`                                                 |

#### Examples

```
// Create a new Pages document
Create a Pages document with the content "Project Proposal\n\nThis document outlines the scope and timeline for the upcoming project."
```

## Architecture

The applescript-mcp server is built using TypeScript and follows a modular architecture:

### Core Components

1. **AppleScriptFramework** (`framework.ts`): The main server class that:
   - Manages MCP protocol communication
   - Handles tool registration and execution
   - Provides logging functionality
   - Executes AppleScript commands

2. **Categories** (`src/categories/*.ts`): Modular script collections organized by functionality:
   - Each category contains related scripts (e.g., calendar, system, notes)
   - Categories are registered with the framework in `index.ts`

3. **Types** (`src/types/index.ts`): TypeScript interfaces defining:
   - `ScriptDefinition`: Structure for individual scripts
   - `ScriptCategory`: Collection of related scripts
   - `LogLevel`: Standard logging levels
   - `FrameworkOptions`: Configuration options

### Execution Flow

1. Client sends a tool request via MCP protocol
2. Server identifies the appropriate category and script
3. Script content is generated (static or dynamically via function)
4. AppleScript is executed via macOS `osascript` command
5. Results are returned to the client

### Logging System

The framework includes a comprehensive logging system that:
- Logs to both stderr and MCP logging protocol
- Supports multiple severity levels (debug, info, warning, error, etc.)
- Provides detailed execution information for troubleshooting

## Development

### Setup

```bash
# Install dependencies
npm install

# Build the server
npm run build

# Launch MCP Inspector
# See: https://modelcontextprotocol.io/docs/tools/inspector
npx @modelcontextprotocol/inspector node path/to/server/index.js args...
```

### Adding New Functionality

#### 1. Create Category File

Create `src/categories/newcategory.ts`:

```typescript
import { ScriptCategory } from "../types/index.js";

export const newCategory: ScriptCategory = {
  name: "category_name",
  description: "Category description",
  scripts: [
    // Scripts will go here
  ],
};
```

#### 2. Add Scripts

```typescript
{
  name: "script_name",
  description: "What the script does",
  schema: {
    type: "object",
    properties: {
      paramName: {
        type: "string",
        description: "Parameter description"
      }
    },
    required: ["paramName"]
  },
  script: (args) => `
    tell application "App"
      // AppleScript code using ${args.paramName}
    end tell
  `
}
```

#### 3. Register Category

Update `src/index.ts`:

```typescript
import { newCategory } from "./categories/newcategory.js";
// ...
server.addCategory(newCategory);
```

### Advanced Script Development

For more complex scripts, you can:

1. **Use dynamic script generation**:
   ```typescript
   script: (args) => {
     // Process arguments and build script dynamically
     let scriptContent = `tell application "App"\n`;
     
     if (args.condition) {
       scriptContent += `  // Conditional logic\n`;
     }
     
     scriptContent += `end tell`;
     return scriptContent;
   }
   ```

2. **Process complex data**:
   ```typescript
   // Example from Notes category
   function generateNoteHtml(args: any): string {
     // Process markdown-like syntax into HTML
     let processedContent = content;
     
     if (format.headings) {
       processedContent = processedContent.replace(/^# (.+)$/gm, '<h1>$1</h1>');
       // ...
     }
     
     return processedContent;
   }
   ```

## Debugging

### Using MCP Inspector

The MCP Inspector provides a web interface for testing and debugging your server:

```bash
npm run inspector
```

### Logging

Enable debug logging by setting the environment variable:

```bash
DEBUG=applescript-mcp* npm start
```

### Example configuration
After running `npm run build` add the following to your `mcp.json` file:

```json
{
  "mcpServers": {
    "applescript-mcp-server": {
      "command": "node",
      "args": ["/path/to/applescript-mcp/dist/index.js"]
    }
  }
}
```

### Common Issues

- **Permission Errors**: Check System Preferences > Security & Privacy > Privacy > Automation
- **Script Failures**: Test scripts directly in Script Editor.app before integration
- **Communication Issues**: Check stdio streams aren't being redirected
- **Database Access**: Some features (like Messages) require Full Disk Access permission

## Resources

- [AppleScript Language Guide](https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html)
- [MCP Protocol Documentation](https://modelcontextprotocol.io)
- [Issue Tracker](https://github.com/joshrutkowski/applescript-mcp/issues)

## Contributing

1. Fork the repository
2. Create a feature branch
3. Commit your changes
4. Push to the branch
5. Create a Pull Request

## License

MIT License - see [LICENSE](LICENSE) for details

```

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

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

```

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

```json
{
  "name": "applescript-mcp",
  "version": "1.0.4",
  "description": "AppleScript MCP Framework",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "ts-node --esm src/index.ts"
  },
  "keywords": [
    "mcp",
    "applescript",
    "macos"
  ],
  "author": "Josh Rutkowski",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.1.1"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "ts-node": "^10.9.0"
  }
}

```

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

```yaml
# 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
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs

name: Node.js CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: macos-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x]
        # Using macOS runners since this is an AppleScript project

    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
    - run: npm ci
    - run: npm run build

```

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

```typescript
import { ScriptCategory } from "../types/index.js";

/**
 * Pages-related scripts.
 * * create_document: Create a new Pages document with plain text content
 */
export const pagesCategory: ScriptCategory = {
  name: "pages",
  description: "Pages document operations",
  scripts: [
    {
      name: "create_document",
      description: "Create a new Pages document with plain text content (no formatting)",
      schema: {
        type: "object",
        properties: {
          content: {
            type: "string",
            description: "The plain text content to add to the document (no formatting)"
          }
        },
        required: ["content"]
      },
      script: (args) => `
        try
          tell application "Pages"
            -- Create new document
            set newDoc to make new document
            
            set the body text of newDoc to "${args.content.replace(/"/g, '\\"')}"
            activate
            return "Document created successfully with plain text content"
          end tell
        on error errMsg
          return "Failed to create document: " & errMsg
        end try
      `
    }
  ]
};

```

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

```typescript
export interface ScriptDefinition {
  /**
   * The name of the script.
   */
  name: string;

  /**
   * A brief description of what the script does.
   */
  description: string;

  /**
   * The script content, which can be a string or a function that returns a string.
   */
  script: string | ((args: any) => string);

  /**
   * Optional schema defining the structure of the script's input parameters.
   */
  schema?: {
    type: "object";
    properties: Record<string, any>;

    /**
     * Optional list of required properties in the schema.
     */
    required?: string[];
  };
}

export interface ScriptCategory {
  /**
   * The name of the script category.
   */
  name: string;

  /**
   * A brief description of the script category.
   */
  description: string;

  /**
   * A list of scripts that belong to this category.
   */
  scripts: ScriptDefinition[];
}

/**
 * Standard log levels for the framework's logging system.
 * Follows the RFC 5424 syslog severity levels.
 */
export type LogLevel = 
  | "emergency" // System is unusable
  | "alert"     // Action must be taken immediately
  | "critical"  // Critical conditions
  | "error"     // Error conditions
  | "warning"   // Warning conditions
  | "notice"    // Normal but significant condition
  | "info"      // Informational messages
  | "debug";    // Debug-level messages

export interface FrameworkOptions {
  /**
   * Optional name of the framework.
   */
  name?: string;

  /**
   * Optional version of the framework.
   */
  version?: string;

  /**
   * Optional flag to enable or disable debug mode.
   */
  debug?: boolean;
}

```

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

```typescript
// src/categories/notifications.ts
import { ScriptCategory } from "../types/index.js";

/**
 * Notification-related scripts.
 * * toggle_do_not_disturb: Toggle Do Not Disturb mode. NOTE: Requires keyboard shortcut to be set up in System Preferences.
 * * send_notification: Send a system notification
 */
export const notificationsCategory: ScriptCategory = {
  name: "notifications",
  description: "Notification management",
  scripts: [
    {
      name: "toggle_do_not_disturb",
      description: "Toggle Do Not Disturb mode using keyboard shortcut",
      script: `
        try
          tell application "System Events"
            keystroke "z" using {control down, option down, command down}
          end tell
          return "Toggled Do Not Disturb mode"
        on error errMsg
          return "Failed to toggle Do Not Disturb: " & errMsg
        end try
      `,
    },
    {
      name: "send_notification",
      description: "Send a system notification",
      schema: {
        type: "object",
        properties: {
          title: {
            type: "string",
            description: "Notification title",
          },
          message: {
            type: "string",
            description: "Notification message",
          },
          sound: {
            type: "boolean",
            description: "Play sound with notification",
            default: true,
          },
        },
        required: ["title", "message"],
      },
      script: (args) => `
        display notification "${args.message}" with title "${args.title}" ${args.sound ? 'sound name "default"' : ""}
      `,
    },
  ],
};

```

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

```typescript
import { ScriptCategory } from "../types/index.js";

/**
 * iTerm-related scripts.
 * * paste_clipboard: Pastes the clipboard to an iTerm window
 * * run: Run a command in iTerm
 */
export const itermCategory: ScriptCategory = {
  name: "iterm",
  description: "iTerm terminal operations",
  scripts: [
    {
      name: "paste_clipboard",
      description: "Paste clipboard content into iTerm",
      script: `
        tell application "System Events" to keystroke "c" using {command down}
        delay 0.1
        tell application "iTerm"
          set w to current window
          tell w's current session to write text (the clipboard)
          activate
        end tell
      `,
    },
    {
      name: "run",
      description: "Run a command in iTerm",
      schema: {
        type: "object",
        properties: {
          command: {
            type: "string",
            description: "Command to run in iTerm",
          },
          newWindow: {
            type: "boolean",
            description: "Whether to open in a new window (default: false)",
            default: false,
          },
        },
        required: ["command"],
      },
      script: (args) => `
        tell application "iTerm"
          ${
            args.newWindow
              ? `
            set newWindow to (create window with default profile)
            tell current session of newWindow
          `
              : `
            set w to current window
            tell w's current session
          `
          }
            write text "${args.command}"
            activate
          end tell
        end tell
      `,
    },
  ],
};

```

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

```yaml
name: Create Release

on:
  push:
    tags:
      - 'v*' # Trigger on version tags (v1.0.0, v1.2.3, etc.)

permissions:
  contents: write 
  pull-requests: write
  repository-projects: write

jobs:
  build:
    runs-on: macos-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'https://registry.npmjs.org'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Build package
        run: npm run build
        
      - name: Get version from tag
        id: get_version
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
        
      - name: Create package archive
        run: |
          mkdir -p dist-package
          cp -r dist package.json README.md LICENSE dist-package/
          cd dist-package
          npm pack
          mv *.tgz ../applescript-mcp-${{ steps.get_version.outputs.VERSION }}.tgz
          
      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        with:
          name: Release ${{ github.ref_name }}
          draft: false
          prerelease: false
          body: |
            Release of applescript-mcp version ${{ steps.get_version.outputs.VERSION }}
            
            ## Changes
            
            <!-- Add your release notes here -->
          files: |
            ./applescript-mcp-${{ steps.get_version.outputs.VERSION }}.tgz
          
      # Uncomment the following step if you want to publish to npm
      # - name: Publish to npm
      #   run: npm publish
      #   env:
      #     NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

```

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

```typescript
import { AppleScriptFramework } from "./framework.js";
import { systemCategory } from "./categories/system.js";
import { calendarCategory } from "./categories/calendar.js";
import { finderCategory } from "./categories/finder.js";
import { clipboardCategory } from "./categories/clipboard.js";
import { notificationsCategory } from "./categories/notifications.js";
import { itermCategory } from "./categories/iterm.js";
import { mailCategory } from "./categories/mail.js";
import { pagesCategory } from "./categories/pages.js";
import { shortcutsCategory } from "./categories/shortcuts.js";
import { messagesCategory } from "./categories/messages.js";
import { notesCategory } from "./categories/notes.js";

const server = new AppleScriptFramework({
  name: "applescript-server",
  version: "1.0.4",
  debug: false,
});

// Log startup information using stderr (server isn't connected yet)
console.error(`[INFO] Starting AppleScript MCP server - PID: ${process.pid}`);

// Add all categories
console.error("[INFO] Registering categories...");
server.addCategory(systemCategory);
server.addCategory(calendarCategory);
server.addCategory(finderCategory);
server.addCategory(clipboardCategory);
server.addCategory(notificationsCategory);
server.addCategory(itermCategory);
server.addCategory(mailCategory);
server.addCategory(pagesCategory);
server.addCategory(shortcutsCategory);
server.addCategory(messagesCategory);
server.addCategory(notesCategory);
console.error(`[INFO] Registered ${11} categories successfully`);

// Start the server
console.error("[INFO] Starting server...");
server.run()
  .then(() => {
    console.error("[NOTICE] Server started successfully");
  })
  .catch(error => {
    const errorMessage = error instanceof Error ? error.message : String(error);
    console.error(`[EMERGENCY] Failed to start server: ${errorMessage}`);
    console.error(error);
  });

```

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

```typescript
import { ScriptCategory } from "../types/index.js";

/**
 * Shortcuts-related scripts.
 * * run_shortcut: Run a shortcut with optional input
 * * list_shortcuts: List available shortcuts
 */
export const shortcutsCategory: ScriptCategory = {
  name: "shortcuts",
  description: "Shortcuts operations",
  scripts: [
    {
      name: "run_shortcut",
      description: "Run a shortcut with optional input. Uses Shortcuts Events to run in background without opening the app.",
      schema: {
        type: "object",
        properties: {
          name: {
            type: "string",
            description: "Name of the shortcut to run",
          },
          input: {
            type: "string",
            description: "Optional input to provide to the shortcut",
          },
        },
        required: ["name"],
      },
      script: (args) => `
        try
          tell application "Shortcuts Events"
            ${args.input ? 
              `run shortcut "${args.name}" with input "${args.input}"` :
              `run shortcut "${args.name}"`
            }
          end tell
          return "Shortcut '${args.name}' executed successfully"
        on error errMsg
          return "Failed to run shortcut: " & errMsg
        end try
      `,
    },
    {
      name: "list_shortcuts",
      description: "List all available shortcuts with optional limit",
      schema: {
        type: "object",
        properties: {
          limit: {
            type: "number",
            description: "Optional limit on the number of shortcuts to return",
          },
        },
      },
      script: (args) => `
        try
          tell application "Shortcuts"
            set shortcutNames to name of every shortcut
            
            ${args.limit ? `
            -- Apply limit if specified
            if (count of shortcutNames) > ${args.limit} then
              set shortcutNames to items 1 through ${args.limit} of shortcutNames
            end if
            ` : ``}
          end tell
          
          -- Convert to JSON string manually
          set jsonOutput to "{"
          set jsonOutput to jsonOutput & "\\"status\\": \\"success\\","
          set jsonOutput to jsonOutput & "\\"shortcuts\\": ["
          
          repeat with i from 1 to count of shortcutNames
            set currentName to item i of shortcutNames
            set jsonOutput to jsonOutput & "{\\"name\\": \\"" & currentName & "\\"}"
            if i < count of shortcutNames then
              set jsonOutput to jsonOutput & ", "
            end if
          end repeat
          
          set jsonOutput to jsonOutput & "]}"
          return jsonOutput
          
        on error errMsg
          return "{\\"status\\": \\"error\\", \\"message\\": \\"" & errMsg & "\\"}"
        end try
      `,
    },
  ],
};

```

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

```typescript
// src/categories/clipboard.ts
import { ScriptCategory } from "../types/index.js";

/**
 * Clipboard-related scripts.
 * * get_clipboard: Returns the current clipboard content
 * * set_clipboard: Sets the clipboard to a specified value
 * * clear_clipboard: Resets the clipboard content
 */
export const clipboardCategory: ScriptCategory = {
  name: "clipboard",
  description: "Clipboard management operations",
  scripts: [
    {
      name: "get_clipboard",
      description: "Get current clipboard content",
      schema: {
        type: "object",
        properties: {
          type: {
            type: "string",
            enum: ["text", "file_paths"],
            description: "Type of clipboard content to get",
            default: "text",
          },
        },
      },
      script: (args) => {
        if (args?.type === "file_paths") {
          return `
            tell application "System Events"
              try
                set theClipboard to the clipboard
                if theClipboard starts with "file://" then
                  set AppleScript's text item delimiters to linefeed
                  set filePaths to {}
                  repeat with aPath in paragraphs of (the clipboard as string)
                    if aPath starts with "file://" then
                      set end of filePaths to (POSIX path of (aPath as alias))
                    end if
                  end repeat
                  return filePaths as string
                else
                  return "No file paths in clipboard"
                end if
              on error errMsg
                return "Failed to get clipboard: " & errMsg
              end try
            end tell
          `;
        } else {
          return `
            tell application "System Events"
              try
                return (the clipboard as text)
              on error errMsg
                return "Failed to get clipboard: " & errMsg
              end try
            end tell
          `;
        }
      },
    },
    {
      name: "set_clipboard",
      description: "Set clipboard content",
      schema: {
        type: "object",
        properties: {
          content: {
            type: "string",
            description: "Content to copy to clipboard",
          },
        },
        required: ["content"],
      },
      script: (args) => `
        try
          set the clipboard to "${args.content}"
          return "Clipboard content set successfully"
        on error errMsg
          return "Failed to set clipboard: " & errMsg
        end try
      `,
    },
    {
      name: "clear_clipboard",
      description: "Clear clipboard content",
      script: `
        try
          set the clipboard to ""
          return "Clipboard cleared successfully"
        on error errMsg
          return "Failed to clear clipboard: " & errMsg
        end try
      `,
    },
  ],
};

```

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

```typescript
import { ScriptCategory } from "../types/index.js";

/**
 * System-related scripts.
 * * volume: Set system volume
 * * get_frontmost_app: Get the name of the frontmost application
 * * launch_app: Launch an application
 * * quit_app: Quit an application
 * * toggle_dark_mode: Toggle system dark mode
 */
export const systemCategory: ScriptCategory = {
  name: "system",
  description: "System control and information",
  scripts: [
    {
      name: "volume",
      description: "Set system volume",
      schema: {
        type: "object",
        properties: {
          level: {
            type: "number",
            minimum: 0,
            maximum: 100,
          },
        },
        required: ["level"],
      },
      script: (args) => `set volume ${Math.round((args.level / 100) * 7)}`,
    },
    {
      name: "get_frontmost_app",
      description: "Get the name of the frontmost application",
      script:
        'tell application "System Events" to get name of first process whose frontmost is true',
    },
    {
      name: "launch_app",
      description: "Launch an application",
      schema: {
        type: "object",
        properties: {
          name: {
            type: "string",
            description: "Application name",
          },
        },
        required: ["name"],
      },
      script: (args) => `
            try
              tell application "${args.name}"
                activate
              end tell
              return "Application ${args.name} launched successfully"
            on error errMsg
              return "Failed to launch application: " & errMsg
            end try
          `,
    },
    {
      name: "quit_app",
      description: "Quit an application",
      schema: {
        type: "object",
        properties: {
          name: {
            type: "string",
            description: "Application name",
          },
          force: {
            type: "boolean",
            description: "Force quit if true",
            default: false,
          },
        },
        required: ["name"],
      },
      script: (args) => `
            try
              tell application "${args.name}"
                ${args.force ? "quit saving no" : "quit"}
              end tell
              return "Application ${args.name} quit successfully"
            on error errMsg
              return "Failed to quit application: " & errMsg
            end try
          `,
    },
    {
      name: "toggle_dark_mode",
      description: "Toggle system dark mode",
      script: `
            tell application "System Events"
              tell appearance preferences
                set dark mode to not dark mode
                return "Dark mode is now " & (dark mode as text)
              end tell
            end tell
          `,
    },
    {
      name: "get_battery_status",
      description: "Get battery level and charging status",
      script: `
            try
              set powerSource to do shell script "pmset -g batt"
              return powerSource
            on error errMsg
              return "Failed to get battery status: " & errMsg
            end try
          `,
    },
  ],
};

```

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

```typescript
// src/categories/finder.ts
import { ScriptCategory } from "../types/index.js";

/**
 * Finder-related scripts.
 * * get_selected_files: Get currently selected files in Finder
 * * search_files: Search for files by name
 * * quick_look_file: Preview a file using Quick Look
 *
 */
export const finderCategory: ScriptCategory = {
  name: "finder",
  description: "Finder and file operations",
  scripts: [
    {
      name: "get_selected_files",
      description: "Get currently selected files in Finder",
      script: `
        tell application "Finder"
          try
            set selectedItems to selection
            if selectedItems is {} then
              return "No items selected"
            end if

            set itemPaths to ""
            repeat with theItem in selectedItems
              set itemPaths to itemPaths & (POSIX path of (theItem as alias)) & linefeed
            end repeat

            return itemPaths
          on error errMsg
            return "Failed to get selected files: " & errMsg
          end try
        end tell
      `,
    },
    {
      name: "search_files",
      description: "Search for files by name",
      schema: {
        type: "object",
        properties: {
          query: {
            type: "string",
            description: "Search term",
          },
          location: {
            type: "string",
            description: "Search location (default: home folder)",
            default: "~",
          },
        },
        required: ["query"],
      },
      script: (args) => `
        set searchPath to "/Users/joshrutkowski/Downloads"
        tell application "Finder"
          try
            set theFolder to POSIX file searchPath as alias
            set theFiles to every file of folder theFolder whose name contains "${args.query}"
            set resultList to ""
            repeat with aFile in theFiles
              set resultList to resultList & (POSIX path of (aFile as alias)) & return
            end repeat
            if resultList is "" then
              return "No files found matching '${args.query}'"
            end if
            return resultList
          on error errMsg
            return "Failed to search files: " & errMsg
          end try
        end tell
      `,
    },
    {
      name: "quick_look_file",
      description: "Preview a file using Quick Look",
      schema: {
        type: "object",
        properties: {
          path: {
            type: "string",
            description: "File path to preview",
          },
        },
        required: ["path"],
      },
      script: (args) => `
        try
          set filePath to POSIX file "${args.path}"
          tell application "Finder"
            activate
            select filePath
            tell application "System Events"
              -- Press Space to trigger Quick Look
              delay 0.5 -- Small delay to ensure Finder is ready
              key code 49 -- Space key
            end tell
          end tell
          return "Quick Look preview opened for ${args.path}"
        on error errMsg
          return "Failed to open Quick Look: " & errMsg
        end try
      `,
    },
  ],
};

```

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

```typescript
import { ScriptCategory } from "../types/index.js";

/**
 * Calendar-related scripts.
 * * add: adds a new event to Calendar
 * * list: List events for today
 */
export const calendarCategory: ScriptCategory = {
  name: "calendar",
  description: "Calendar operations",
  scripts: [
    {
      name: "add",
      description: "Add a new event to Calendar",
      schema: {
        type: "object",
        properties: {
          title: {
            type: "string",
            description: "Event title",
          },
          startDate: {
            type: "string",
            description: "Start date and time (YYYY-MM-DD HH:MM:SS)",
          },
          endDate: {
            type: "string",
            description: "End date and time (YYYY-MM-DD HH:MM:SS)",
          },
          calendar: {
            type: "string",
            description: "Calendar name (optional)",
            default: "Calendar",
          },
        },
        required: ["title", "startDate", "endDate"],
      },
      script: (args) => `
        tell application "Calendar"
          set theStartDate to current date
          set hours of theStartDate to ${args.startDate.slice(11, 13)}
          set minutes of theStartDate to ${args.startDate.slice(14, 16)}
          set seconds of theStartDate to ${args.startDate.slice(17, 19)}

          set theEndDate to theStartDate + (1 * hours)
          set hours of theEndDate to ${args.endDate.slice(11, 13)}
          set minutes of theEndDate to ${args.endDate.slice(14, 16)}
          set seconds of theEndDate to ${args.endDate.slice(17, 19)}

          tell calendar "${args.calendar || "Calendar"}"
            make new event with properties {summary:"${args.title}", start date:theStartDate, end date:theEndDate}
          end tell
        end tell
      `,
    },
    {
      name: "list",
      description: "List all events for today",
      script: `
      tell application "Calendar"
          set todayStart to (current date)
          set time of todayStart to 0
          set todayEnd to todayStart + 1 * days
          set eventList to {}
          repeat with calendarAccount in calendars
              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)
          end repeat
          set output to ""
          repeat with anEvent in eventList
              set eventStartDate to start date of anEvent
              set eventEndDate to end date of anEvent

              -- Format the time parts
              set startHours to hours of eventStartDate
              set startMinutes to minutes of eventStartDate
              set endHours to hours of eventEndDate
              set endMinutes to minutes of eventEndDate

              set output to output & "Event: " & summary of anEvent & "\n"
              set output to output & "Start: " & startHours & ":" & text -2 thru -1 of ("0" & startMinutes) & "\n"
              set output to output & "End: " & endHours & ":" & text -2 thru -1 of ("0" & endMinutes) & "\n"
              set output to output & "-------------------\n"
          end repeat
          return output
      end tell
      `,
    },
  ],
};

```

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

```typescript
import { ScriptCategory } from "../types/index.js";

/**
 * iMessage related scripts
 */
export const messagesCategory: ScriptCategory = {
  name: "messages",
  description: "iMessage operations",
  scripts: [
    {
      name: "list_chats",
      description: "List available iMessage and SMS chats",
      schema: {
        type: "object",
        properties: {
          includeParticipantDetails: {
            type: "boolean",
            description: "Include detailed participant information",
            default: false
          }
        }
      },
      script: (args) => `
        tell application "Messages"
          set chatList to {}
          repeat with aChat in chats
            set chatName to name of aChat
            if chatName is missing value then
              set chatName to ""
              -- Try to get the contact name for individual chats
              try
                set theParticipants to participants of aChat
                if (count of theParticipants) is 1 then
                  set theParticipant to item 1 of theParticipants
                  set chatName to name of theParticipant
                end if
              end try
            end if
            
            set chatInfo to {id:id of aChat, name:chatName, isGroupChat:(id of aChat contains "+")}
            
            ${args.includeParticipantDetails ? `
            -- Add participant details if requested
            set participantList to {}
            repeat with aParticipant in participants of aChat
              set participantInfo to {id:id of aParticipant, handle:handle of aParticipant}
              try
                set participantInfo to participantInfo & {name:name of aParticipant}
              end try
              copy participantInfo to end of participantList
            end repeat
            set chatInfo to chatInfo & {participant:participantList}
            ` : ''}
            
            copy chatInfo to end of chatList
          end repeat
          return chatList
        end tell
      `
    },
    {
      name: "get_messages",
      description: "Get messages from the Messages app",
      schema: {
        type: "object",
        properties: {
          limit: {
            type: "number",
            description: "Maximum number of messages to retrieve",
            default: 100
          }
        }
      },
      script: (args) => `
 on run
	-- Path to the Messages database
	set dbPath to (do shell script "echo ~/Library/Messages/chat.db")
	
	-- Create a temporary SQL file for our query
	set tempFile to (do shell script "mktemp /tmp/imessage_query.XXXXXX")
	
	-- Write SQL query to temp file
	do shell script "cat > " & quoted form of tempFile & " << 'EOF'
SELECT
    datetime(message.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as message_date,
    handle.id as sender,
    message.text as message_text,
    chat.display_name as chat_name
FROM
    message
    LEFT JOIN handle ON message.handle_id = handle.ROWID
    LEFT JOIN chat_message_join ON message.ROWID = chat_message_join.message_id
    LEFT JOIN chat ON chat_message_join.chat_id = chat.ROWID
ORDER BY
    message.date DESC
LIMIT ${args.limit};
EOF"
	
	-- Execute the query
	set queryResult to do shell script "sqlite3 " & quoted form of dbPath & " < " & quoted form of tempFile
	
	-- Clean up temp file
	do shell script "rm " & quoted form of tempFile
	
	-- Process and display results
	set resultList to paragraphs of queryResult
	set messageData to {}
	
	repeat with messageLine in resultList
		set messageData to messageData & messageLine
	end repeat
	
	return messageData
end run
      `
    },
    {
      name: "search_messages",
      description: "Search for messages containing specific text or from a specific sender",
      schema: {
        type: "object",
        properties: {
          searchText: {
            type: "string",
            description: "Text to search for in messages",
            default: ""
          },
          sender: {
            type: "string",
            description: "Search for messages from a specific sender (phone number or email)",
            default: ""
          },
          chatId: {
            type: "string",
            description: "Limit search to a specific chat ID",
            default: ""
          },
          limit: {
            type: "number",
            description: "Maximum number of messages to retrieve",
            default: 50
          },
          daysBack: {
            type: "number",
            description: "Limit search to messages from the last N days",
            default: 30
          }
        },
        required: ["searchText"]
      },
      script: (args) => `
on run
	-- Path to the Messages database
	set dbPath to (do shell script "echo ~/Library/Messages/chat.db")
	
	-- Create a temporary SQL file for our query
	set tempFile to (do shell script "mktemp /tmp/imessage_search.XXXXXX")
	
	-- Build WHERE clause based on provided parameters
	set whereClause to ""
	
	${args.searchText ? `
	-- Add search text condition if provided
	set whereClause to whereClause & "message.text LIKE '%${args.searchText.replace(/'/g, "''")}%' "
	` : ''}
	
	${args.sender ? `
	-- Add sender condition if provided
	if length of whereClause > 0 then
		set whereClause to whereClause & "AND "
	end if
	set whereClause to whereClause & "handle.id LIKE '%${args.sender.replace(/'/g, "''")}%' "
	` : ''}
	
	${args.chatId ? `
	-- Add chat ID condition if provided
	if length of whereClause > 0 then
		set whereClause to whereClause & "AND "
	end if
	set whereClause to whereClause & "chat.chat_identifier = '${args.chatId.replace(/'/g, "''")}' "
	` : ''}
	
	${args.daysBack ? `
	-- Add date range condition
	if length of whereClause > 0 then
		set whereClause to whereClause & "AND "
	end if
	set whereClause to whereClause & "message.date > (strftime('%s', 'now', '-${args.daysBack} days') - strftime('%s', '2001-01-01')) * 1000000000 "
	` : ''}
	
	-- If no search parameters were provided, add a default condition to avoid returning all messages
	if length of whereClause = 0 then
		set whereClause to "1=1 "
	end if
	
	-- Write SQL query to temp file
	do shell script "cat > " & quoted form of tempFile & " << 'EOF'
SELECT
    datetime(message.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as message_date,
    handle.id as sender,
    message.text as message_text,
    chat.display_name as chat_name,
    chat.chat_identifier as chat_id
FROM
    message
    LEFT JOIN handle ON message.handle_id = handle.ROWID
    LEFT JOIN chat_message_join ON message.ROWID = chat_message_join.message_id
    LEFT JOIN chat ON chat_message_join.chat_id = chat.ROWID
WHERE
    " & whereClause & "
ORDER BY
    message.date DESC
LIMIT ${args.limit};
EOF"
	
	-- Execute the query
	set queryResult to do shell script "sqlite3 " & quoted form of dbPath & " < " & quoted form of tempFile
	
	-- Clean up temp file
	do shell script "rm " & quoted form of tempFile
	
	-- Process and display results
	set resultList to paragraphs of queryResult
	set messageData to {}
	
	repeat with messageLine in resultList
		set messageData to messageData & messageLine
	end repeat
	
	return messageData
end run
      `
    },
    {
      name: "compose_message",
      description: "Open Messages app with a pre-filled message to a recipient or automatically send a message",
      schema: {
        type: "object",
        properties: {
          recipient: {
            type: "string",
            description: "Phone number or email of the recipient"
          },
          body: {
            type: "string",
            description: "Message body text",
            default: ""
          },
          auto: {
            type: "boolean",
            description: "Automatically send the message without user confirmation",
            default: false
          }
        },
        required: ["recipient"]
      },
      script: (args) => `
on run
  -- Get the recipient and message body
  set recipient to "${args.recipient}"
  set messageBody to "${args.body || ''}"
  set autoSend to ${args.auto === true ? "true" : "false"}
  
  if autoSend then
    -- Automatically send the message using AppleScript
    tell application "Messages"
      -- Get the service (iMessage or SMS)
      set targetService to 1st service whose service type = iMessage
      
      -- Send the message
      set targetBuddy to buddy "${args.recipient}" of targetService
      send "${args.body || ''}" to targetBuddy
      
      return "Message sent to " & "${args.recipient}"
    end tell
  else
    -- Just open Messages app with pre-filled content
    -- Create the SMS URL with proper URL encoding
    set smsURL to "sms:" & recipient
    
    if messageBody is not equal to "" then
      -- Use percent encoding for spaces instead of plus signs
      set encodedBody to ""
      repeat with i from 1 to count of characters of messageBody
        set c to character i of messageBody
        if c is space then
          set encodedBody to encodedBody & "%20"
        else
          set encodedBody to encodedBody & c
        end if
      end repeat
      
      set smsURL to smsURL & "&body=" & encodedBody
    end if
    
    -- Open the URL with the default handler (Messages app)
    do shell script "open " & quoted form of smsURL
    
    return "Opening Messages app with recipient: " & recipient
  end if
end run
      `
    }
  ]
};

```

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

```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
  ErrorCode,
  McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { exec } from "child_process";
import { promisify } from "util";
import os from "os";
import {
  ScriptCategory,
  ScriptDefinition,
  FrameworkOptions,
  LogLevel,
} from "./types/index.js";

const execAsync = promisify(exec);

// Get system information for logging
const systemInfo = {
  platform: os.platform(),
  release: os.release(),
  hostname: os.hostname(),
  arch: os.arch(),
  nodeVersion: process.version
};



export class AppleScriptFramework {
  private server: Server;
  private categories: ScriptCategory[] = [];
  private _initInfo: Record<string, any> = {};
  private _isConnected: boolean = false;
  private _pendingCategories: Array<Record<string, any>> = [];

  /**
   * Constructs an instance of AppleScriptFramework.
   * @param options - Configuration options for the framework.
   */
  constructor(options: FrameworkOptions = {}) {
    const serverName = options.name || "applescript-server";
    const serverVersion = options.version || "1.0.0";
    
    this.server = new Server(
      {
        name: serverName,
        version: serverVersion,
      },
      {
        capabilities: {
          tools: {},
          logging: {}, // Enable logging capability
        },
      },
    );

    if (options.debug) {
      this.enableDebugLogging();
    }
    
    // Log server initialization with stderr (server isn't connected yet)
    console.error(`[INFO] AppleScript MCP server initialized - ${serverName} v${serverVersion}`);
    
    // Store initialization info for later logging after connection
    this._initInfo = {
      name: serverName,
      version: serverVersion,
      debug: !!options.debug,
      system: systemInfo
    };
  }

  /**
   * Enables debug logging for the server.
   * Sets up error handlers and configures detailed logging.
   */
  private enableDebugLogging(): void {
    console.error("[INFO] Debug logging enabled");
    
    this.server.onerror = (error) => {
      const errorMessage = error instanceof Error ? error.message : String(error);
      console.error("[MCP Error]", error);
      
      // Only use MCP logging if connected
      if (this._isConnected) {
        this.log("error", "MCP server error", { error: errorMessage });
      }
    };
    
    // Set up additional debug event handlers if needed
    this.server.oninitialized = () => {
      this._isConnected = true;
      console.error("[DEBUG] Connection initialized");
      
      // We'll log initialization info in the run method after connection is fully established
      console.error("[DEBUG] Connection initialized");
    };
    
    this.server.onclose = () => {
      this._isConnected = false;
      console.error("[DEBUG] Connection closed");
      
      // No MCP logging here since we're disconnected
    };
  }

  /**
   * Adds a new script category to the framework.
   * @param category - The script category to add.
   */
  addCategory(category: ScriptCategory): void {
    this.categories.push(category);
    
    // Use console logging since this is called before server connection
    console.error(`[DEBUG] Added category: ${category.name} (${category.scripts.length} scripts)`);
    
    // Store category info to log via MCP after connection
    if (!this._pendingCategories) {
      this._pendingCategories = [];
    }
    this._pendingCategories.push({
      categoryName: category.name,
      scriptCount: category.scripts.length,
      description: category.description
    });
  }

  /**
   * Logs a message with the specified severity level.
   * Uses the MCP server's logging system to record events if available.
   * Always logs to console for visibility.
   * 
   * @param level - The severity level of the log message following RFC 5424 syslog levels
   * @param message - The message to log
   * @param data - Optional additional data to include with the log message
   * 
   * @example
   * // Log a debug message
   * framework.log("debug", "Processing request", { requestId: "123" });
   * 
   * @example
   * // Log an error
   * framework.log("error", "Failed to execute script", { scriptName: "calendar_add" });
   */
  log(level: LogLevel, message: string, data?: Record<string, any>): void {
    // Format for console output
    const timestamp = new Date().toISOString();
    const dataStr = data ? ` ${JSON.stringify(data)}` : '';
    
    // Always log to stderr for visibility
    console.error(`[${timestamp}] [${level.toUpperCase()}] ${message}${dataStr}`);
    
    // Only try to use MCP logging if we're connected
    if (this._isConnected) {
      try {
        this.server.sendLoggingMessage({
          level: level,
          message: message,
          data: data || {},
        });
      } catch (error) {
        // Silently ignore logging errors - we've already logged to console
      }
    }
  }

  /**
   * Executes an AppleScript and returns the result.
   * @param script - The AppleScript to execute.
   * @returns The result of the script execution.
   * @throws Will throw an error if the script execution fails.
   */
  private async executeScript(script: string): Promise<string> {
    // Log script execution (truncate long scripts for readability)
    const scriptPreview = script.length > 100 ? script.substring(0, 100) + "..." : script;
    this.log("debug", "Executing AppleScript", { scriptPreview });
    
    try {
      const startTime = Date.now();
      const { stdout } = await execAsync(
        `osascript -e '${script.replace(/'/g, "'\"'\"'")}'`,
      );
      const executionTime = Date.now() - startTime;
      
      this.log("debug", "AppleScript executed successfully", { 
        executionTimeMs: executionTime,
        outputLength: stdout.length
      });
      
      return stdout.trim();
    } catch (error) {
      // Properly type check the error object
      let errorMessage = "Unknown error occurred";
      if (error && typeof error === "object") {
        if ("message" in error && typeof error.message === "string") {
          errorMessage = error.message;
        } else if (error instanceof Error) {
          errorMessage = error.message;
        }
      } else if (typeof error === "string") {
        errorMessage = error;
      }
      
      this.log("error", "AppleScript execution failed", { 
        error: errorMessage,
        scriptPreview
      });
      
      throw new Error(`AppleScript execution failed: ${errorMessage}`);
    }
  }

  /**
   * Sets up request handlers for the server.
   */
  private setupHandlers(): void {
    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: this.categories.flatMap((category) =>
        category.scripts.map((script) => ({
          name: `${category.name}_${script.name}`, // Changed from dot to underscore
          description: `[${category.description}] ${script.description}`,
          inputSchema: script.schema || {
            type: "object",
            properties: {},
          },
        })),
      ),
    }));

    // Handle tool execution
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const toolName = request.params.name;
      this.log("info", "Tool execution requested", { 
        tool: toolName,
        hasArguments: !!request.params.arguments
      });
      
      try {
        // Split on underscore instead of dot
        const [categoryName, ...scriptNameParts] =
          toolName.split("_");
        const scriptName = scriptNameParts.join("_"); // Rejoin in case script name has underscores

        const category = this.categories.find((c) => c.name === categoryName);
        if (!category) {
          this.log("warning", "Category not found", { categoryName });
          throw new McpError(
            ErrorCode.MethodNotFound,
            `Category not found: ${categoryName}`,
          );
        }

        const script = category.scripts.find((s) => s.name === scriptName);
        if (!script) {
          this.log("warning", "Script not found", { 
            categoryName, 
            scriptName 
          });
          throw new McpError(
            ErrorCode.MethodNotFound,
            `Script not found: ${scriptName}`,
          );
        }

        this.log("debug", "Generating script content", { 
          categoryName, 
          scriptName,
          isFunction: typeof script.script === "function"
        });
        
        const scriptContent =
          typeof script.script === "function"
            ? script.script(request.params.arguments)
            : script.script;

        const result = await this.executeScript(scriptContent);
        
        this.log("info", "Tool execution completed successfully", { 
          tool: toolName,
          resultLength: result.length
        });

        return {
          content: [
            {
              type: "text",
              text: result,
            },
          ],
        };
      } catch (error) {
        if (error instanceof McpError) {
          this.log("error", "MCP error during tool execution", { 
            tool: toolName,
            errorCode: error.code,
            errorMessage: error.message
          });
          throw error;
        }

        let errorMessage = "Unknown error occurred";
        if (error && typeof error === "object") {
          if ("message" in error && typeof error.message === "string") {
            errorMessage = error.message;
          } else if (error instanceof Error) {
            errorMessage = error.message;
          }
        } else if (typeof error === "string") {
          errorMessage = error;
        }

        this.log("error", "Error during tool execution", { 
          tool: toolName,
          errorMessage
        });

        return {
          content: [
            {
              type: "text",
              text: `Error: ${errorMessage}`,
            },
          ],
          isError: true,
        };
      }
    });
  }

  /**
   * Runs the AppleScript framework server.
   */
  async run(): Promise<void> {
    console.error("[INFO] Setting up request handlers");
    this.setupHandlers();
    
    console.error("[INFO] Initializing StdioServerTransport");
    const transport = new StdioServerTransport();
    
    try {
      console.error("[INFO] Connecting server to transport");
      await this.server.connect(transport);
      this._isConnected = true;
      
      // Log server running status using console only
      const totalScripts = this.categories.reduce((count, category) => count + category.scripts.length, 0);
      console.error(`[NOTICE] AppleScript MCP server running with ${this.categories.length} categories and ${totalScripts} scripts`);
      
      console.error("AppleScript MCP server running");
    } catch (error) {
      let errorMessage = "Unknown error occurred";
      if (error && typeof error === "object" && error instanceof Error) {
        errorMessage = error.message;
      }
      
      console.error("Failed to start AppleScript MCP server:", errorMessage);
      throw error;
    }
  }
}

```

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

```typescript
import { ScriptCategory } from "../types/index.js";

/**
 * Generates HTML content for a note based on user input
 * @param args The arguments containing content specifications
 * @returns HTML string for the note
 */
function generateNoteHtml(args: any): string {
  const {
    title = "New Note",
    content = "",
    format = {
      headings: false,
      bold: false,
      italic: false,
      underline: false,
      links: false,
      lists: false
    }
  } = args;

  // Process content based on format options
  let processedContent = content;
  
  // If content contains markdown-like syntax and formatting is enabled, convert it
  if (format.headings) {
    // Convert # Heading to <h1>Heading</h1>, ## Heading to <h2>Heading</h2>, etc.
    processedContent = processedContent.replace(/^# (.+)$/gm, '<h1>$1</h1>');
    processedContent = processedContent.replace(/^## (.+)$/gm, '<h2>$1</h2>');
    processedContent = processedContent.replace(/^### (.+)$/gm, '<h3>$1</h3>');
  }
  
  if (format.bold) {
    // Convert **text** or __text__ to <b>text</b>
    processedContent = processedContent.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
    processedContent = processedContent.replace(/__(.+?)__/g, '<b>$1</b>');
  }
  
  if (format.italic) {
    // Convert *text* or _text_ to <i>text</i>
    processedContent = processedContent.replace(/\*(.+?)\*/g, '<i>$1</i>');
    processedContent = processedContent.replace(/_(.+?)_/g, '<i>$1</i>');
  }
  
  if (format.underline) {
    // Convert ~text~ to <u>text</u>
    processedContent = processedContent.replace(/~(.+?)~/g, '<u>$1</u>');
  }
  
  if (format.links) {
    // Convert [text](url) to <a href="url">text</a>
    processedContent = processedContent.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
  }
  
  if (format.lists) {
    // Handle unordered lists
    // Look for lines starting with - or * and convert to <li> items
    const listItems = processedContent.match(/^[*-] (.+)$/gm);
    if (listItems) {
      let listHtml = '<ul>';
      for (const item of listItems) {
        const content = item.replace(/^[*-] /, '');
        listHtml += `<li>${content}</li>`;
      }
      listHtml += '</ul>';
      
      // Replace the original list items with the HTML list
      for (const item of listItems) {
        processedContent = processedContent.replace(item, '');
      }
      processedContent = processedContent.replace(/\n+/g, '\n') + listHtml;
    }
    
    // Handle ordered lists (1. Item)
    const orderedItems = processedContent.match(/^\d+\. (.+)$/gm);
    if (orderedItems) {
      let listHtml = '<ol>';
      for (const item of orderedItems) {
        const content = item.replace(/^\d+\. /, '');
        listHtml += `<li>${content}</li>`;
      }
      listHtml += '</ol>';
      
      // Replace the original list items with the HTML list
      for (const item of orderedItems) {
        processedContent = processedContent.replace(item, '');
      }
      processedContent = processedContent.replace(/\n+/g, '\n') + listHtml;
    }
  }
  
  // Wrap paragraphs in <p> tags if they aren't already wrapped in HTML tags
  const paragraphs = processedContent.split('\n\n');
  processedContent = paragraphs
    .map((p: string) => {
      if (p.trim() && !p.trim().startsWith('<')) {
        return `<p>${p}</p>`;
      }
      return p;
    })
    .join('\n');
  
  return processedContent;
}

export const notesCategory: ScriptCategory = {
  name: "notes",
  description: "Apple Notes operations",
  scripts: [
    {
      name: "create",
      description: "Create a new note with optional formatting",
      script: (args) => {
        const { title = "New Note", content = "", format = {} } = args;
        const htmlContent = generateNoteHtml(args);
        
        return `
          tell application "Notes"
            make new note with properties {body:"${htmlContent}", name:"${title}"}
          end tell
        `;
      },
      schema: {
        type: "object",
        properties: {
          title: {
            type: "string",
            description: "Title of the note"
          },
          content: {
            type: "string",
            description: "Content of the note, can include markdown-like syntax for formatting"
          },
          format: {
            type: "object",
            description: "Formatting options for the note content",
            properties: {
              headings: {
                type: "boolean",
                description: "Enable heading formatting (# Heading)"
              },
              bold: {
                type: "boolean",
                description: "Enable bold formatting (**text**)"
              },
              italic: {
                type: "boolean",
                description: "Enable italic formatting (*text*)"
              },
              underline: {
                type: "boolean",
                description: "Enable underline formatting (~text~)"
              },
              links: {
                type: "boolean",
                description: "Enable link formatting ([text](url))"
              },
              lists: {
                type: "boolean",
                description: "Enable list formatting (- item or 1. item)"
              }
            }
          }
        },
        required: ["title", "content"]
      }
    },
    {
      name: "createRawHtml",
      description: "Create a new note with direct HTML content",
      script: (args) => {
        const { title = "New Note", html = "" } = args;
        
        return `
          tell application "Notes"
            make new note with properties {body:"${html.replace(/"/g, '\\"')}", name:"${title}"}
          end tell
        `;
      },
      schema: {
        type: "object",
        properties: {
          title: {
            type: "string",
            description: "Title of the note"
          },
          html: {
            type: "string",
            description: "Raw HTML content for the note"
          }
        },
        required: ["title", "html"]
      }
    },
    {
      name: "list",
      description: "List all notes or notes in a specific folder",
      script: (args) => {
        const { folder = "" } = args;
        
        if (folder) {
          return `
            tell application "Notes"
              set folderList to folders whose name is "${folder}"
              if length of folderList > 0 then
                set targetFolder to item 1 of folderList
                set noteNames to name of notes of targetFolder
                return noteNames as string
              else
                return "Folder not found: ${folder}"
              end if
            end tell
          `;
        } else {
          return `
            tell application "Notes"
              set noteNames to name of notes
              return noteNames as string
            end tell
          `;
        }
      },
      schema: {
        type: "object",
        properties: {
          folder: {
            type: "string",
            description: "Optional folder name to list notes from"
          }
        }
      }
    },
    {
      name: "get",
      description: "Get a specific note by title",
      script: (args) => {
        const { title, folder = "" } = args;
        
        if (folder) {
          return `
            tell application "Notes"
              set folderList to folders whose name is "${folder}"
              if length of folderList > 0 then
                set targetFolder to item 1 of folderList
                set matchingNotes to notes of targetFolder whose name is "${title}"
                if length of matchingNotes > 0 then
                  set n to item 1 of matchingNotes
                  set noteTitle to name of n
                  set noteBody to body of n
                  set noteCreationDate to creation date of n
                  set noteModDate to modification date of n
                  
                  set jsonResult to "{\\"title\\": \\""
                  set jsonResult to jsonResult & noteTitle & "\\""
                  set jsonResult to jsonResult & ", \\"body\\": \\"" & noteBody & "\\""
                  set jsonResult to jsonResult & ", \\"creationDate\\": \\"" & noteCreationDate & "\\""
                  set jsonResult to jsonResult & ", \\"modificationDate\\": \\"" & noteModDate & "\\"}"
                  
                  return jsonResult
                else
                  return "Note not found: ${title}"
                end if
              else
                return "Folder not found: ${folder}"
              end if
            end tell
          `;
        } else {
          return `
            tell application "Notes"
              set matchingNotes to notes whose name is "${title}"
              if length of matchingNotes > 0 then
                set n to item 1 of matchingNotes
                set noteTitle to name of n
                set noteBody to body of n
                set noteCreationDate to creation date of n
                set noteModDate to modification date of n
                
                set jsonResult to "{\\"title\\": \\""
                set jsonResult to jsonResult & noteTitle & "\\""
                set jsonResult to jsonResult & ", \\"body\\": \\"" & noteBody & "\\""
                set jsonResult to jsonResult & ", \\"creationDate\\": \\"" & noteCreationDate & "\\""
                set jsonResult to jsonResult & ", \\"modificationDate\\": \\"" & noteModDate & "\\"}"
                
                return jsonResult
              else
                return "Note not found: ${title}"
              end if
            end tell
          `;
        }
      },
      schema: {
        type: "object",
        properties: {
          title: {
            type: "string",
            description: "Title of the note to retrieve"
          },
          folder: {
            type: "string",
            description: "Optional folder name to search in"
          }
        },
        required: ["title"]
      }
    },
    {
      name: "search",
      description: "Search for notes containing specific text",
      script: (args) => {
        const { query, folder = "", limit = 5, includeBody = true } = args;
        
        if (folder) {
          return `
            tell application "Notes"
              set folderList to folders whose name is "${folder}"
              if length of folderList > 0 then
                set targetFolder to item 1 of folderList
                set matchingNotes to {}
                set allNotes to notes of targetFolder
                repeat with n in allNotes
                  if name of n contains "${query}" or body of n contains "${query}" then
                    set end of matchingNotes to n
                  end if
                end repeat
                
                set resultCount to length of matchingNotes
                if resultCount > ${limit} then set resultCount to ${limit}
                
                set jsonResult to "["
                repeat with i from 1 to resultCount
                  set n to item i of matchingNotes
                  set noteTitle to name of n
                  set noteCreationDate to creation date of n
                  set noteModDate to modification date of n
                  ${includeBody ? 'set noteBody to body of n' : ''}
                  
                  set noteJson to "{\\"title\\": \\""
                  set noteJson to noteJson & noteTitle & "\\""
                  ${includeBody ? 'set noteJson to noteJson & ", \\"body\\": \\"" & noteBody & "\\""' : ''}
                  set noteJson to noteJson & ", \\"creationDate\\": \\"" & noteCreationDate & "\\""
                  set noteJson to noteJson & ", \\"modificationDate\\": \\"" & noteModDate & "\\"}"
                  
                  set jsonResult to jsonResult & noteJson
                  if i < resultCount then set jsonResult to jsonResult & ", "
                end repeat
                set jsonResult to jsonResult & "]"
                
                return jsonResult
              else
                return "Folder not found: ${folder}"
              end if
            end tell
          `;
        } else {
          return `
            tell application "Notes"
              set matchingNotes to {}
              set allNotes to notes
              repeat with n in allNotes
                if name of n contains "${query}" or body of n contains "${query}" then
                  set end of matchingNotes to n
                end if
              end repeat
              
              set resultCount to length of matchingNotes
              if resultCount > ${limit} then set resultCount to ${limit}
              
              set jsonResult to "["
              repeat with i from 1 to resultCount
                set n to item i of matchingNotes
                set noteTitle to name of n
                set noteCreationDate to creation date of n
                set noteModDate to modification date of n
                ${includeBody ? 'set noteBody to body of n' : ''}
                
                set noteJson to "{\\"title\\": \\""
                set noteJson to noteJson & noteTitle & "\\""
                ${includeBody ? 'set noteJson to noteJson & ", \\"body\\": \\"" & noteBody & "\\""' : ''}
                set noteJson to noteJson & ", \\"creationDate\\": \\"" & noteCreationDate & "\\""
                set noteJson to noteJson & ", \\"modificationDate\\": \\"" & noteModDate & "\\"}"
                
                set jsonResult to jsonResult & noteJson
                if i < resultCount then set jsonResult to jsonResult & ", "
              end repeat
              set jsonResult to jsonResult & "]"
              
              return jsonResult
            end tell
          `;
        }
      },
      schema: {
        type: "object",
        properties: {
          query: {
            type: "string",
            description: "Text to search for in notes (title and body)"
          },
          folder: {
            type: "string",
            description: "Optional folder name to search in"
          },
          limit: {
            type: "number",
            description: "Maximum number of results to return (default: 5)"
          },
          includeBody: {
            type: "boolean",
            description: "Whether to include note body in results (default: true)"
          }
        },
        required: ["query"]
      }
    }
  ]
};
```

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

```typescript
import { ScriptCategory } from "../types/index.js";

/**
 * Mail-related scripts.
 * * create_email: Create a new email in Mail.app with specified recipient, subject, and body
 * * list_emails: List emails from a specified mailbox in Mail.app
 * * get_email: Get a specific email by ID or search criteria from Mail.app
 */
export const mailCategory: ScriptCategory = {
  name: "mail",
  description: "Mail operations",
  scripts: [
    {
      name: "create_email",
      description: "Create a new email in Mail.app",
      schema: {
        type: "object",
        properties: {
          recipient: {
            type: "string",
            description: "Email recipient",
          },
          subject: {
            type: "string",
            description: "Email subject",
          },
          body: {
            type: "string",
            description: "Email body",
          },
        },
        required: ["recipient", "subject", "body"],
      },
      script: (args) => `
        set recipient to "${args.recipient}"
        set subject to "${args.subject}"
        set body to "${args.body}"

        -- URL encode subject and body
        set encodedSubject to my urlEncode(subject)
        set encodedBody to my urlEncode(body)

        -- Construct the mailto URL
        set mailtoURL to "mailto:" & recipient & "?subject=" & encodedSubject & "&body=" & encodedBody

        -- Use Apple Mail's 'mailto' command to create the email
        tell application "Mail"
          mailto mailtoURL
          activate
        end tell

        -- Handler to URL-encode text
        on urlEncode(theText)
          set theEncodedText to ""
          set theChars to every character of theText
          repeat with aChar in theChars
            set charCode to ASCII number aChar
            if charCode = 32 then
              set theEncodedText to theEncodedText & "%20" -- Space
            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
              -- Allowed characters: A-Z, a-z, 0-9, -, ., _, ~
              set theEncodedText to theEncodedText & aChar
            else
              -- Convert to %HH format
              set hexCode to do shell script "printf '%02X' " & charCode
              set theEncodedText to theEncodedText & "%" & hexCode
            end if
          end repeat
          return theEncodedText
        end urlEncode
      `,
    },
    {
      name: "list_emails",
      description: "List emails from a specified mailbox in Mail.app",
      schema: {
        type: "object",
        properties: {
          mailbox: {
            type: "string",
            description: "Name of the mailbox to list emails from (e.g., 'Inbox', 'Sent')",
            default: "Inbox"
          },
          account: {
            type: "string",
            description: "Name of the account to search in (e.g., 'iCloud', 'Gmail', 'Exchange'). If not specified, searches all accounts with preference for iCloud.",
            default: "iCloud"
          },
          count: {
            type: "number",
            description: "Maximum number of emails to retrieve",
            default: 10
          },
          unreadOnly: {
            type: "boolean",
            description: "Only show unread emails if true"
          }
        }
      },
      script: (args) => `
        set mailboxName to "${args.mailbox || 'Inbox'}"
        set accountName to "${args.account || 'iCloud'}"
        set messageCount to ${args.count || 10}
        set showUnreadOnly to ${args.unreadOnly ? 'true' : 'false'}
        set searchAllAccounts to ${!args.account ? 'true' : 'false'}
        
        tell application "Mail"
          -- Get all messages if no specific mailbox is found
          set foundMailbox to false
          set emailMessages to {}
          set targetAccount to missing value
          
          -- First try to find the specified account
          if not searchAllAccounts then
            try
              set allAccounts to every account
              repeat with acct in allAccounts
                if name of acct is accountName then
                  set targetAccount to acct
                  exit repeat
                end if
              end repeat
            end try
            
            -- If account not found, set to search all accounts
            if targetAccount is missing value then
              set searchAllAccounts to true
            end if
          end if
          
          -- If specific account is found, search in that account
          if not searchAllAccounts and targetAccount is not missing value then
            try
              set acctMailboxes to every mailbox of targetAccount
              repeat with m in acctMailboxes
                if name of m is mailboxName then
                  set targetMailbox to m
                  set foundMailbox to true
                  
                  -- Get messages from the found mailbox
                  if showUnreadOnly then
                    set emailMessages to (messages of targetMailbox whose read status is false)
                  else
                    set emailMessages to (messages of targetMailbox)
                  end if
                  
                  exit repeat
                end if
              end repeat
              
              -- If mailbox not found in specified account, try to get inbox
              if not foundMailbox then
                try
                  set inboxMailbox to inbox of targetAccount
                  set targetMailbox to inboxMailbox
                  set foundMailbox to true
                  
                  if showUnreadOnly then
                    set emailMessages to (messages of targetMailbox whose read status is false)
                  else
                    set emailMessages to (messages of targetMailbox)
                  end if
                end try
              end if
            end try
          else
            -- Search all accounts, with preference for iCloud
            set iCloudAccount to missing value
            set allAccounts to every account
            
            -- First look for iCloud account
            repeat with acct in allAccounts
              if name of acct is "iCloud" then
                set iCloudAccount to acct
                exit repeat
              end if
            end repeat
            
            -- Try to find the mailbox directly
            try
              set allMailboxes to every mailbox
              repeat with m in allMailboxes
                if name of m is mailboxName then
                  set targetMailbox to m
                  set foundMailbox to true
                  
                  -- Get messages from the found mailbox
                  if showUnreadOnly then
                    set emailMessages to (messages of targetMailbox whose read status is false)
                  else
                    set emailMessages to (messages of targetMailbox)
                  end if
                  
                  exit repeat
                end if
              end repeat
            end try
            
            -- If not found directly, try to find it in each account (prioritize iCloud)
            if not foundMailbox and iCloudAccount is not missing value then
              try
                set acctMailboxes to every mailbox of iCloudAccount
                repeat with m in acctMailboxes
                  if name of m is mailboxName then
                    set targetMailbox to m
                    set foundMailbox to true
                    
                    -- Get messages from the found mailbox
                    if showUnreadOnly then
                      set emailMessages to (messages of targetMailbox whose read status is false)
                    else
                      set emailMessages to (messages of targetMailbox)
                    end if
                    
                    exit repeat
                  end if
                end repeat
              end try
            end if
            
            -- If still not found in iCloud, check other accounts
            if not foundMailbox then
              repeat with acct in allAccounts
                if acct is not iCloudAccount then
                  try
                    set acctMailboxes to every mailbox of acct
                    repeat with m in acctMailboxes
                      if name of m is mailboxName then
                        set targetMailbox to m
                        set foundMailbox to true
                        
                        -- Get messages from the found mailbox
                        if showUnreadOnly then
                          set emailMessages to (messages of targetMailbox whose read status is false)
                        else
                          set emailMessages to (messages of targetMailbox)
                        end if
                        
                        exit repeat
                      end if
                    end repeat
                    
                    if foundMailbox then exit repeat
                  end try
                end if
              end repeat
            end if
          end if
          
          -- If still not found, get messages from all inboxes
          if not foundMailbox then
            set emailMessages to {}
            set allAccounts to every account
            set accountsChecked to 0
            
            -- First check iCloud if available
            repeat with acct in allAccounts
              if name of acct is "iCloud" then
                try
                  -- Try to get the inbox for iCloud
                  set inboxMailbox to inbox of acct
                  
                  -- Add messages from this inbox
                  if showUnreadOnly then
                    set acctMessages to (messages of inboxMailbox whose read status is false)
                  else
                    set acctMessages to (messages of inboxMailbox)
                  end if
                  
                  set emailMessages to emailMessages & acctMessages
                  set accountsChecked to accountsChecked + 1
                end try
                exit repeat
              end if
            end repeat
            
            -- Then check other accounts if needed
            if accountsChecked is 0 then
              repeat with acct in allAccounts
                try
                  -- Try to get the inbox for this account
                  set inboxMailbox to inbox of acct
                  
                  -- Add messages from this inbox
                  if showUnreadOnly then
                    set acctMessages to (messages of inboxMailbox whose read status is false)
                  else
                    set acctMessages to (messages of inboxMailbox)
                  end if
                  
                  set emailMessages to emailMessages & acctMessages
                end try
              end repeat
            end if
            
            -- Sort combined messages by date (newest first)
            set emailMessages to my sortMessagesByDate(emailMessages)
            set mailboxName to "All Inboxes"
          end if
          
          -- Limit the number of messages
          if (count of emailMessages) > messageCount then
            set emailMessages to items 1 thru messageCount of emailMessages
          end if
          
          -- Format the results
          set accountInfo to ""
          if not searchAllAccounts and targetAccount is not missing value then
            set accountInfo to " (" & accountName & ")"
          end if
          
          set emailList to "Recent emails in " & mailboxName & accountInfo & ":" & return & return
          
          if (count of emailMessages) is 0 then
            set emailList to emailList & "No messages found."
          else
            repeat with theMessage in emailMessages
              try
                set msgSubject to subject of theMessage
                set msgSender to sender of theMessage
                set msgDate to date received of theMessage
                set msgRead to read status of theMessage
                
                -- Try to get account name for this message
                set msgAccount to ""
                try
                  set msgMailbox to mailbox of theMessage
                  set msgAcct to account of msgMailbox
                  set msgAccount to " [" & name of msgAcct & "]"
                end try
                
                set emailList to emailList & "From: " & msgSender & return
                set emailList to emailList & "Subject: " & msgSubject & return
                set emailList to emailList & "Date: " & msgDate & msgAccount & return
                set emailList to emailList & "Read: " & msgRead & return & return
              on error errMsg
                set emailList to emailList & "Error processing message: " & errMsg & return & return
              end try
            end repeat
          end if
          
          return emailList
        end tell
        
        -- Helper function to sort messages by date
        on sortMessagesByDate(messageList)
          tell application "Mail"
            set sortedMessages to {}
            
            -- Simple bubble sort by date received (newest first)
            repeat with i from 1 to count of messageList
              set currentMsg to item i of messageList
              set currentDate to date received of currentMsg
              set inserted to false
              
              if (count of sortedMessages) is 0 then
                set sortedMessages to {currentMsg}
              else
                repeat with j from 1 to count of sortedMessages
                  set compareMsg to item j of sortedMessages
                  set compareDate to date received of compareMsg
                  
                  if currentDate > compareDate then
                    if j is 1 then
                      set sortedMessages to {currentMsg} & sortedMessages
                    else
                      set sortedMessages to (items 1 thru (j - 1) of sortedMessages) & currentMsg & (items j thru (count of sortedMessages) of sortedMessages)
                    end if
                    set inserted to true
                    exit repeat
                  end if
                end repeat
                
                if not inserted then
                  set sortedMessages to sortedMessages & {currentMsg}
                end if
              end if
            end repeat
            
            return sortedMessages
          end tell
        end sortMessagesByDate
      `,
    },
    {
      name: "get_email",
      description: "Get a specific email by search criteria from Mail.app",
      schema: {
        type: "object",
        properties: {
          mailbox: {
            type: "string",
            description: "Name of the mailbox to search in (e.g., 'Inbox', 'Sent')",
            default: "Inbox"
          },
          account: {
            type: "string",
            description: "Name of the account to search in (e.g., 'iCloud', 'Gmail', 'Exchange'). If not specified, searches all accounts with preference for iCloud.",
            default: "iCloud"
          },
          subject: {
            type: "string",
            description: "Subject text to search for (partial match)"
          },
          sender: {
            type: "string",
            description: "Sender email or name to search for (partial match)"
          },
          dateReceived: {
            type: "string",
            description: "Date received to search for (format: YYYY-MM-DD)"
          },
          unreadOnly: {
            type: "boolean",
            description: "Only search unread emails if true"
          },
          includeBody: {
            type: "boolean",
            description: "Include email body in the result if true",
            default: false
          }
        },
        required: []
      },
      script: (args) => `
        set mailboxName to "${args.mailbox || 'Inbox'}"
        set accountName to "${args.account || 'iCloud'}"
        set searchSubject to "${args.subject || ''}"
        set searchSender to "${args.sender || ''}"
        set searchDate to "${args.dateReceived || ''}"
        set showUnreadOnly to ${args.unreadOnly ? 'true' : 'false'}
        set includeBody to ${args.includeBody ? 'true' : 'false'}
        set searchAllAccounts to ${!args.account ? 'true' : 'false'}
        
        tell application "Mail"
          -- Get all messages if no specific mailbox is found
          set foundMailbox to false
          set emailMessages to {}
          set targetAccount to missing value
          
          -- First try to find the specified account
          if not searchAllAccounts then
            try
              set allAccounts to every account
              repeat with acct in allAccounts
                if name of acct is accountName then
                  set targetAccount to acct
                  exit repeat
                end if
              end repeat
            end try
            
            -- If account not found, set to search all accounts
            if targetAccount is missing value then
              set searchAllAccounts to true
            end if
          end if
          
          -- If specific account is found, search in that account
          if not searchAllAccounts and targetAccount is not missing value then
            try
              set acctMailboxes to every mailbox of targetAccount
              repeat with m in acctMailboxes
                if name of m is mailboxName then
                  set targetMailbox to m
                  set foundMailbox to true
                  
                  -- Get messages from the found mailbox
                  if showUnreadOnly then
                    set emailMessages to (messages of targetMailbox whose read status is false)
                  else
                    set emailMessages to (messages of targetMailbox)
                  end if
                  
                  exit repeat
                end if
              end repeat
            end try
          else
            -- Search all accounts, with preference for iCloud
            set iCloudAccount to missing value
            set allAccounts to every account
            
            -- First look for iCloud account
            repeat with acct in allAccounts
              if name of acct is "iCloud" then
                set iCloudAccount to acct
                exit repeat
              end if
            end repeat
            
            -- Try to find the mailbox directly
            try
              set allMailboxes to every mailbox
              repeat with m in allMailboxes
                if name of m is mailboxName then
                  set targetMailbox to m
                  set foundMailbox to true
                  
                  -- Get messages from the found mailbox
                  if showUnreadOnly then
                    set emailMessages to (messages of targetMailbox whose read status is false)
                  else
                    set emailMessages to (messages of targetMailbox)
                  end if
                  
                  exit repeat
                end if
              end repeat
            end try
          end if
          
          -- Filter messages based on search criteria
          set filteredMessages to {}
          
          repeat with theMessage in emailMessages
            try
              set matchesSubject to true
              set matchesSender to true
              set matchesDate to true
              
              -- Check subject if specified
              if searchSubject is not "" then
                set msgSubject to subject of theMessage
                if msgSubject does not contain searchSubject then
                  set matchesSubject to false
                end if
              end if
              
              -- Check sender if specified
              if searchSender is not "" then
                set msgSender to sender of theMessage
                if msgSender does not contain searchSender then
                  set matchesSender to false
                end if
              end if
              
              -- Check date if specified
              if searchDate is not "" then
                set msgDate to date received of theMessage
                set msgDateString to (year of msgDate as string) & "-" & my padNumber(month of msgDate as integer) & "-" & my padNumber(day of msgDate as integer)
                if msgDateString is not searchDate then
                  set matchesDate to false
                end if
              end if
              
              -- Add to filtered list if all criteria match
              if matchesSubject and matchesSender and matchesDate then
                set end of filteredMessages to theMessage
              end if
            end try
          end repeat
          
          -- Format the results
          set emailList to "Search results:" & return & return
          
          if (count of filteredMessages) is 0 then
            set emailList to emailList & "No matching emails found."
          else
            repeat with theMessage in filteredMessages
              try
                set msgSubject to subject of theMessage
                set msgSender to sender of theMessage
                set msgDate to date received of theMessage
                set msgRead to read status of theMessage
                
                -- Try to get account name for this message
                set msgAccount to ""
                try
                  set msgMailbox to mailbox of theMessage
                  set msgAcct to account of msgMailbox
                  set msgAccount to " [" & name of msgAcct & "]"
                end try
                
                set emailList to emailList & "From: " & msgSender & return
                set emailList to emailList & "Subject: " & msgSubject & return
                set emailList to emailList & "Date: " & msgDate & msgAccount & return
                set emailList to emailList & "Read: " & msgRead & return
                
                -- Include body if requested
                if includeBody then
                  set msgContent to content of theMessage
                  set emailList to emailList & "Content: " & return & msgContent & return
                end if
                
                set emailList to emailList & return
              on error errMsg
                set emailList to emailList & "Error processing message: " & errMsg & return & return
              end try
            end repeat
          end if
          
          return emailList
        end tell
        
        -- Helper function to pad numbers with leading zero if needed
        on padNumber(num)
          if num < 10 then
            return "0" & num
          else
            return num as string
          end if
        end padNumber
      `,
    },
  ],
};

```