Skip to main content
Glama
jxnl
by jxnl

messages

Send, read, schedule, and check unread messages in the Apple Messages app through the MCP server.

Instructions

Interact with Apple Messages app - send, read, schedule messages and check unread messages

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
operationYesOperation to perform: 'send', 'read', 'schedule', or 'unread'
phoneNumberNoPhone number to send message to (required for send, read, and schedule operations)
messageNoMessage to send (required for send and schedule operations)
limitNoNumber of messages to read (optional, for read and unread operations)
scheduledTimeNoISO string of when to send the message (required for schedule operation)

Implementation Reference

  • Defines the MCP Tool schema for the 'messages' tool, including input validation for operations: send, read, schedule, unread.
    const MESSAGES_TOOL: Tool = {
      name: "messages",
      description: "Interact with Apple Messages app - send, read, schedule messages and check unread messages",
      inputSchema: {
        type: "object",
        properties: {
          operation: {
            type: "string",
            description: "Operation to perform: 'send', 'read', 'schedule', or 'unread'",
            enum: ["send", "read", "schedule", "unread"]
          },
          phoneNumber: {
            type: "string",
            description: "Phone number to send message to (required for send, read, and schedule operations)"
          },
          message: {
            type: "string",
            description: "Message to send (required for send and schedule operations)"
          },
          limit: {
            type: "number",
            description: "Number of messages to read (optional, for read and unread operations)"
          },
          scheduledTime: {
            type: "string",
            description: "ISO string of when to send the message (required for schedule operation)"
          }
        },
        required: ["operation"]
      }
    };
  • tools.ts:308-310 (registration)
    Registers the 'messages' tool (MESSAGES_TOOL) in the array of tools exported for use in the MCP server.
    const tools = [CONTACTS_TOOL, NOTES_TOOL, MESSAGES_TOOL, MAIL_TOOL, REMINDERS_TOOL, WEB_SEARCH_TOOL, CALENDAR_TOOL, MAPS_TOOL];
    
    export default tools;
  • Helper function to send a message via Apple Messages app using AppleScript.
    async function sendMessage(phoneNumber: string, message: string) {
        const escapedMessage = message.replace(/"/g, '\\"');
        const result = await runAppleScript(`
    tell application "Messages"
        set targetService to 1st service whose service type = iMessage
        set targetBuddy to buddy "${phoneNumber}"
        send "${escapedMessage}" to targetBuddy
    end tell`);
        return result;
    }
  • Helper function to read recent messages from a specific phone number by querying the Messages chat.db SQLite database.
    async function readMessages(phoneNumber: string, limit: number = 10): Promise<Message[]> {
        try {
            // Check database access with retries
            const hasAccess = await retryOperation(checkMessagesDBAccess);
            if (!hasAccess) {
                return [];
            }
    
            // Get all possible formats of the phone number
            const phoneFormats = normalizePhoneNumber(phoneNumber);
            console.error("Trying phone formats:", phoneFormats);
            
            // Create SQL IN clause with all phone number formats
            const phoneList = phoneFormats.map(p => `'${p.replace(/'/g, "''")}'`).join(',');
            
            const query = `
                SELECT 
                    m.ROWID as message_id,
                    CASE 
                        WHEN m.text IS NOT NULL AND m.text != '' THEN m.text
                        WHEN m.attributedBody IS NOT NULL THEN hex(m.attributedBody)
                        ELSE NULL
                    END as content,
                    datetime(m.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as date,
                    h.id as sender,
                    m.is_from_me,
                    m.is_audio_message,
                    m.cache_has_attachments,
                    m.subject,
                    CASE 
                        WHEN m.text IS NOT NULL AND m.text != '' THEN 0
                        WHEN m.attributedBody IS NOT NULL THEN 1
                        ELSE 2
                    END as content_type
                FROM message m 
                INNER JOIN handle h ON h.ROWID = m.handle_id 
                WHERE h.id IN (${phoneList})
                    AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL OR m.cache_has_attachments = 1)
                    AND m.is_from_me IS NOT NULL  -- Ensure it's a real message
                    AND m.item_type = 0  -- Regular messages only
                    AND m.is_audio_message = 0  -- Skip audio messages
                ORDER BY m.date DESC 
                LIMIT ${limit}
            `;
    
            // Execute query with retries
            const { stdout } = await retryOperation(() => 
                execAsync(`sqlite3 -json "${process.env.HOME}/Library/Messages/chat.db" "${query}"`)
            );
            
            if (!stdout.trim()) {
                console.error("No messages found in database for the given phone number");
                return [];
            }
    
            const messages = JSON.parse(stdout) as (Message & {
                message_id: number;
                is_audio_message: number;
                cache_has_attachments: number;
                subject: string | null;
                content_type: number;
            })[];
    
            // Process messages with potential parallel attachment fetching
            const processedMessages = await Promise.all(
                messages
                    .filter(msg => msg.content !== null || msg.cache_has_attachments === 1)
                    .map(async msg => {
                        let content = msg.content || '';
                        let url: string | undefined;
                        
                        // If it's an attributedBody (content_type = 1), decode it
                        if (msg.content_type === 1) {
                            const decoded = decodeAttributedBody(content);
                            content = decoded.text;
                            url = decoded.url;
                        } else {
                            // Check for URLs in regular text messages
                            const urlMatch = content.match(/(https?:\/\/[^\s]+)/);
                            if (urlMatch) {
                                url = urlMatch[1];
                            }
                        }
                        
                        // Get attachments if any
                        let attachments: string[] = [];
                        if (msg.cache_has_attachments) {
                            attachments = await getAttachmentPaths(msg.message_id);
                        }
                        
                        // Add subject if present
                        if (msg.subject) {
                            content = `Subject: ${msg.subject}\n${content}`;
                        }
                        
                        // Format the message object
                        const formattedMsg: Message = {
                            content: content || '[No text content]',
                            date: new Date(msg.date).toISOString(),
                            sender: msg.sender,
                            is_from_me: Boolean(msg.is_from_me)
                        };
    
                        // Add attachments if any
                        if (attachments.length > 0) {
                            formattedMsg.attachments = attachments;
                            formattedMsg.content += '\n[Attachments: ' + attachments.length + ']';
                        }
    
                        // Add URL if present
                        if (url) {
                            formattedMsg.url = url;
                            formattedMsg.content += '\n[URL: ' + url + ']';
                        }
    
                        return formattedMsg;
                    })
            );
    
            return processedMessages;
        } catch (error) {
            console.error('Error reading messages:', error);
            if (error instanceof Error) {
                console.error('Error details:', error.message);
                console.error('Stack trace:', error.stack);
            }
            return [];
        }
    }
  • Helper function to retrieve unread incoming messages from the Messages database.
    async function getUnreadMessages(limit: number = 10): Promise<Message[]> {
        try {
            // Check database access with retries
            const hasAccess = await retryOperation(checkMessagesDBAccess);
            if (!hasAccess) {
                return [];
            }
    
            const query = `
                SELECT 
                    m.ROWID as message_id,
                    CASE 
                        WHEN m.text IS NOT NULL AND m.text != '' THEN m.text
                        WHEN m.attributedBody IS NOT NULL THEN hex(m.attributedBody)
                        ELSE NULL
                    END as content,
                    datetime(m.date/1000000000 + strftime('%s', '2001-01-01'), 'unixepoch', 'localtime') as date,
                    h.id as sender,
                    m.is_from_me,
                    m.is_audio_message,
                    m.cache_has_attachments,
                    m.subject,
                    CASE 
                        WHEN m.text IS NOT NULL AND m.text != '' THEN 0
                        WHEN m.attributedBody IS NOT NULL THEN 1
                        ELSE 2
                    END as content_type
                FROM message m 
                INNER JOIN handle h ON h.ROWID = m.handle_id 
                WHERE m.is_from_me = 0  -- Only messages from others
                    AND m.is_read = 0   -- Only unread messages
                    AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL OR m.cache_has_attachments = 1)
                    AND m.is_audio_message = 0  -- Skip audio messages
                    AND m.item_type = 0  -- Regular messages only
                ORDER BY m.date DESC 
                LIMIT ${limit}
            `;
    
            // Execute query with retries
            const { stdout } = await retryOperation(() => 
                execAsync(`sqlite3 -json "${process.env.HOME}/Library/Messages/chat.db" "${query}"`)
            );
            
            if (!stdout.trim()) {
                console.error("No unread messages found");
                return [];
            }
    
            const messages = JSON.parse(stdout) as (Message & {
                message_id: number;
                is_audio_message: number;
                cache_has_attachments: number;
                subject: string | null;
                content_type: number;
            })[];
    
            // Process messages with potential parallel attachment fetching
            const processedMessages = await Promise.all(
                messages
                    .filter(msg => msg.content !== null || msg.cache_has_attachments === 1)
                    .map(async msg => {
                        let content = msg.content || '';
                        let url: string | undefined;
                        
                        // If it's an attributedBody (content_type = 1), decode it
                        if (msg.content_type === 1) {
                            const decoded = decodeAttributedBody(content);
                            content = decoded.text;
                            url = decoded.url;
                        } else {
                            // Check for URLs in regular text messages
                            const urlMatch = content.match(/(https?:\/\/[^\s]+)/);
                            if (urlMatch) {
                                url = urlMatch[1];
                            }
                        }
                        
                        // Get attachments if any
                        let attachments: string[] = [];
                        if (msg.cache_has_attachments) {
                            attachments = await getAttachmentPaths(msg.message_id);
                        }
                        
                        // Add subject if present
                        if (msg.subject) {
                            content = `Subject: ${msg.subject}\n${content}`;
                        }
                        
                        // Format the message object
                        const formattedMsg: Message = {
                            content: content || '[No text content]',
                            date: new Date(msg.date).toISOString(),
                            sender: msg.sender,
                            is_from_me: Boolean(msg.is_from_me)
                        };
    
                        // Add attachments if any
                        if (attachments.length > 0) {
                            formattedMsg.attachments = attachments;
                            formattedMsg.content += '\n[Attachments: ' + attachments.length + ']';
                        }
    
                        // Add URL if present
                        if (url) {
                            formattedMsg.url = url;
                            formattedMsg.content += '\n[URL: ' + url + ']';
                        }
    
                        return formattedMsg;
                    })
            );
    
            return processedMessages;
        } catch (error) {
            console.error('Error reading unread messages:', error);
            if (error instanceof Error) {
                console.error('Error details:', error.message);
                console.error('Stack trace:', error.stack);
            }
            return [];
        }
    }
  • Helper function to schedule a message to be sent at a future time, plus the export of all message-related functions.
    async function scheduleMessage(phoneNumber: string, message: string, scheduledTime: Date) {
        // Store the scheduled message details
        const scheduledMessages = new Map();
        
        // Calculate delay in milliseconds
        const delay = scheduledTime.getTime() - Date.now();
        
        if (delay < 0) {
            throw new Error('Cannot schedule message in the past');
        }
        
        // Schedule the message
        const timeoutId = setTimeout(async () => {
            try {
                await sendMessage(phoneNumber, message);
                scheduledMessages.delete(timeoutId);
            } catch (error) {
                console.error('Failed to send scheduled message:', error);
            }
        }, delay);
        
        // Store the scheduled message details for reference
        scheduledMessages.set(timeoutId, {
            phoneNumber,
            message,
            scheduledTime,
            timeoutId
        });
        
        return {
            id: timeoutId,
            scheduledTime,
            message,
            phoneNumber
        };
    }
Behavior2/5

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

No annotations are provided, so the description carries the full burden of behavioral disclosure. It mentions operations like 'send' and 'schedule', implying mutations, but doesn't state permissions needed, rate limits, or what 'check unread messages' entails (e.g., returns count or list). This leaves significant gaps in understanding the tool's behavior and constraints.

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 concise and front-loaded, using a single sentence to list key operations. Every word contributes to understanding the tool's scope. However, it could be slightly more structured by separating operations or adding brief context, but it avoids waste and is efficiently phrased.

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?

Given the tool's complexity (5 parameters, multiple operations including mutations) and lack of annotations and output schema, the description is incomplete. It doesn't cover behavioral aspects like error handling, return values, or operational constraints. For a multi-operation tool with no structured support, more detail is needed to guide effective use.

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 already documents all 5 parameters with descriptions and enums. The description adds minimal value beyond listing operations, as it doesn't explain parameter interactions or provide additional context. For example, it doesn't clarify that 'phoneNumber' is required for specific operations beyond what the schema states. Baseline 3 is appropriate given high schema coverage.

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 Messages app - send, read, schedule messages and check unread messages.' It specifies the verb ('interact with') and resource ('Apple Messages app'), listing four distinct operations. However, it doesn't differentiate from sibling tools like 'contacts' or 'mail' beyond mentioning the specific app, which is good but not fully distinguishing.

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 this tool versus alternatives. It lists operations but doesn't mention prerequisites, context, or exclusions. For example, it doesn't clarify if 'send' requires specific permissions or if 'read' is for recent messages only. With sibling tools like 'mail' and 'contacts', no differentiation is offered, leaving usage ambiguous.

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

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/jxnl/apple-mcp'

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