Skip to main content
Glama

mail

Manage Apple Mail app operations: read unread emails, search for specific messages, or send new emails using pre-defined accounts and mailboxes.

Instructions

Interact with Apple Mail app - read unread emails, search emails, and send emails

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
accountNoEmail account to use (optional - if not provided, searches across all accounts)
bccNoBCC email address (optional for send operation)
bodyNoEmail body content (required for send operation)
ccNoCC email address (optional for send operation)
limitNoNumber of emails to retrieve (optional, for unread and search operations)
mailboxNoMailbox to use (optional - if not provided, uses inbox or searches across all mailboxes)
operationYesOperation to perform: 'unread', 'search', 'send', 'mailboxes', or 'accounts'
searchTermNoText to search for in emails (required for search operation)
subjectNoEmail subject (required for send operation)
toNoRecipient email address (required for send operation)

Implementation Reference

  • The handleMail function that implements the core logic for the 'mail' tool. It validates arguments, loads the mail module lazily, and dispatches to specific operations like unread, search, send, mailboxes, and accounts.
    export async function handleMail(
      args: MailArgs,
      loadModule: LoadModuleFunction
    ): Promise<ToolResult> {
      try {
        const mailModule = await loadModule('mail');
    
        switch (args.operation) {
          case "unread": {
            let emails;
            if (args.account) {
              console.error(`Getting unread emails for account: ${args.account}`);
              
              try {
                const scriptPath = path.resolve(__dirname, '../../scripts/getUnreadMail.applescript'); // Path to the script file
                
                // Prepare arguments for the script, ensuring proper shell quoting
                const scriptArgs = [args.account];
                if (args.mailbox) {
                  scriptArgs.push(args.mailbox);
                }
                if (args.limit) {
                  // Ensure limit is passed as a string if the script expects it that way
                  scriptArgs.push(String(args.limit));
                }
                const quotedArgs = scriptArgs.map(arg => `'${String(arg).replace(/'/g, "'\\''")}'`).join(' '); // Quote args
    
                // Construct the osascript command
                const command = `osascript '${scriptPath.replace(/'/g, "'\\''")}' ${quotedArgs}`;
                console.error(`Executing command: ${command}`);
    
                // Execute the command using execAsync
                const { stdout, stderr } = await execAsync(command);
    
                if (stderr) {
                  console.error(`AppleScript stderr: ${stderr}`);
                  // Check if stdout also contains an error, prioritize stdout for AppleScript errors
                  if (stdout && stdout.trim().startsWith('Error:')) {
                     throw new Error(stdout.trim());
                  }
                  // Optionally throw based on stderr if stdout is clean, but let's rely on stdout check first
                }
                
                const asResult = stdout.trim(); // Use stdout as the result
                if (asResult && asResult.startsWith('Error:')) {
                  throw new Error(asResult);
                }
                
                const emailData = [];
                // Use regex on the stdout result
                const matches = asResult.match(/\{([^}]+)\}/g); 
                if (matches && matches.length > 0) {
                  for (const match of matches) {
                    try {
                      const props = match.substring(1, match.length - 1).split(',');
                      const email: any = {};
                      
                      props.forEach(prop => {
                        const parts = prop.split(':');
                        if (parts.length >= 2) {
                          const key = parts[0].trim();
                          const value = parts.slice(1).join(':').trim();
                          email[key] = value;
                        }
                      });
                      
                      if (email.subject || email.sender) {
                        emailData.push({
                          subject: email.subject || "No subject",
                          sender: email.sender || "Unknown sender",
                          dateSent: email.date || new Date().toString(),
                          content: email.content || "[Content not available]",
                          isRead: false,
                                  // Use mailboxName from script result
                                  mailbox: `${args.account} - ${email.mailboxName || "Unknown"}` 
                        });
                      }
                    } catch (parseError) {
                      console.error('Error parsing email match:', parseError);
                    }
                  }
                }
                
                emails = emailData;
              } catch (error: any) { // Catch errors from execAsync or parsing
                console.error('Error executing or processing AppleScript:', error);
                 // Check if the error object has stdout/stderr properties (from exec failure)
                 if (error.stderr) {
                   console.error(`Exec stderr: ${error.stderr}`);
                 }
                 if (error.stdout) {
                    console.error(`Exec stdout: ${error.stdout}`);
                    // If stdout contains the AppleScript error message, use that
                    if (error.stdout.trim().startsWith('Error:')) {
                       // Re-throw the specific AppleScript error if found in stdout
                       throw new Error(error.stdout.trim()); 
                    }
                 }
                // Fallback if script execution failed
                console.error('Falling back to general unread mail fetch due to script error.');
                emails = await mailModule.getUnreadMails(args.limit); 
              }
            } else {
              emails = await mailModule.getUnreadMails(args.limit);
            }
            
            return {
              content: [{ 
                type: "text", 
                text: emails.length > 0 ? 
                  `Found ${emails.length} unread email(s)${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}:\n\n` +
                  emails.map((email: any) => 
                    `[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 500)}${email.content.length > 500 ? '...' : ''}`
                  ).join("\n\n") :
                  `No unread emails found${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}`
              }],
              isError: false
            };
          }
    
          case "search": {
            const emails = await mailModule.searchMails(args.searchTerm, args.limit);
            return {
              content: [{ 
                type: "text", 
                text: emails.length > 0 ? 
                  `Found ${emails.length} email(s) for "${args.searchTerm}"${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}:\n\n` +
                  emails.map((email: any) => 
                    `[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? '...' : ''}`
                  ).join("\n\n") :
                  `No emails found for "${args.searchTerm}"${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}`
              }],
              isError: false
            };
          }
    
          case "send": {
            const result = await mailModule.sendMail(args.to, args.subject, args.body, args.cc, args.bcc);
            return {
              // Ensure result is a string, provide default if null/undefined
              content: [{ type: "text", text: result ?? "Mail operation completed." }], 
              isError: false
            };
          }
    
          case "mailboxes": {
            let mailboxes;
            if (args.account) {
              mailboxes = await mailModule.getMailboxesForAccount(args.account);
            } else {
              mailboxes = await mailModule.getMailboxes();
            }
            // Ensure mailboxes is an array before accessing properties
            const mailboxesArray = Array.isArray(mailboxes) ? mailboxes : [];
            return {
              content: [{ 
                type: "text", 
                text: mailboxesArray.length > 0 ? 
                  `Found ${mailboxesArray.length} mailboxes${args.account ? ` for account "${args.account}"` : ''}:\n\n${mailboxesArray.join("\n")}` :
                  `No mailboxes found${args.account ? ` for account "${args.account}"` : ''}.`
              }],
              isError: false
            };
          }
    
          case "accounts": {
            const accounts = await mailModule.getAccounts();
            // Ensure accounts is an array before accessing properties
            const accountsArray = Array.isArray(accounts) ? accounts : [];
            return {
              content: [{ 
                type: "text", 
                text: accountsArray.length > 0 ? 
                  `Found ${accountsArray.length} email accounts:\n\n${accountsArray.join("\n")}` :
                  "No email accounts found. Make sure Mail app is configured."
              }],
              isError: false
            };
          }
    
          default:
            // This should be unreachable due to Zod validation
            throw new Error(`Unknown mail operation: ${(args as any).operation}`);
        }
      } catch (error) {
        return {
          content: [{
            type: "text",
            text: `Error with mail operation: ${error instanceof Error ? error.message : String(error)}`
          }],
          isError: true
        };
      }
    }
  • Zod schema (MailArgsSchema) defining the discriminated union of input arguments based on 'operation' for the mail tool, used for runtime validation.
    export const MailArgsSchema = z.discriminatedUnion("operation", [
      z.object({ operation: z.literal("unread"), account: z.string().optional(), mailbox: z.string().optional(), limit: z.number().optional() }),
      z.object({ operation: z.literal("search"), searchTerm: z.string().min(1), account: z.string().optional(), mailbox: z.string().optional(), limit: z.number().optional() }),
      z.object({ operation: z.literal("send"), to: z.string(), subject: z.string(), body: z.string(), cc: z.string().optional(), bcc: z.string().optional() }),
      z.object({ operation: z.literal("mailboxes"), account: z.string().optional() }),
      z.object({ operation: z.literal("accounts") }),
    ]);
    
    // Define the argument type from the schema
    type MailArgs = z.infer<typeof MailArgsSchema>;
  • index.ts:128-131 (registration)
    Registers the 'mail' tool in the MCP server's CallToolRequestHandler by parsing input with MailArgsSchema and invoking handleMail with loadModule.
    case "mail": {
      const validatedArgs = MailArgsSchema.parse(args);
      return await handleMail(validatedArgs, loadModule);
    }
  • MCP Tool definition (MAIL_TOOL) with name 'mail', description, and JSON inputSchema for tool discovery via ListTools.
    const MAIL_TOOL: Tool = {
      name: "mail",
      description: "Interact with Apple Mail app - read unread emails, search emails, and send emails",
      inputSchema: {
        type: "object",
        properties: {
          operation: {
            type: "string",
            description: "Operation to perform: 'unread', 'search', 'send', 'mailboxes', or 'accounts'",
            enum: ["unread", "search", "send", "mailboxes", "accounts"]
          },
          account: {
            type: "string",
            description: "Email account to use (optional - if not provided, searches across all accounts)"
          },
          mailbox: {
            type: "string",
            description: "Mailbox to use (optional - if not provided, uses inbox or searches across all mailboxes)"
          },
          limit: {
            type: "number",
            description: "Number of emails to retrieve (optional, for unread and search operations)"
          },
          searchTerm: {
            type: "string",
            description: "Text to search for in emails (required for search operation)"
          },
          to: {
            type: "string",
            description: "Recipient email address (required for send operation)"
          },
          subject: {
            type: "string",
            description: "Email subject (required for send operation)"
          },
          body: {
            type: "string",
            description: "Email body content (required for send operation)"
          },
          cc: {
            type: "string",
            description: "CC email address (optional for send operation)"
          },
          bcc: {
            type: "string",
            description: "BCC email address (optional for send operation)"
          }
        },
        required: ["operation"]
      }
    };
  • Default export of the mail utility module providing helper functions like getUnreadMails, searchMails, sendMail, etc., loaded dynamically by the handler.
    export default {
      getUnreadMails,
      searchMails,
      sendMail,
      getMailboxes,
      getAccounts,
      getMailboxesForAccount,
    };
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. It mentions three operations but omits two ('mailboxes' and 'accounts') listed in the schema. It doesn't describe authentication requirements, rate limits, error conditions, or what 'interact' entails beyond basic operations. The description is incomplete for a multi-operation tool with 10 parameters.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is efficiently structured in a single sentence listing three main operations. However, it omits two operations from the schema ('mailboxes' and 'accounts'), making it slightly incomplete despite its brevity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex tool with 10 parameters, 5 operations, and no annotations or output schema, the description is inadequate. It doesn't explain operation differences, return formats, error handling, or prerequisites. The agent would struggle to use this tool correctly without trial and error.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully documents all 10 parameters. The description adds no parameter-specific information beyond what's in the schema. It mentions operations but doesn't clarify which parameters apply to which operations. Baseline 3 is appropriate when schema does all the work.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Interact with Apple Mail app' with specific verbs (read, search, send) and resources (emails). It distinguishes from siblings by focusing on email functionality rather than calendar, contacts, etc., but doesn't differentiate between email operations within the tool itself.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use specific operations (unread vs search vs send) or when to choose this tool over sibling tools. It lists capabilities but offers no decision criteria or context for selection among the five operation types.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wearesage/mcp-apple'

If you have feedback or need assistance with the MCP directory API, please join our Discord server