Skip to main content
Glama

messages

Manage Apple Messages app interactions: send, read, schedule messages, and check unread messages via specified operations and parameters.

Instructions

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

Input Schema

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

Implementation Reference

  • Main execution handler for the 'messages' tool. Loads utils/message module and dispatches to sendMessage, readMessages, scheduleMessage, or getUnreadMessages based on the operation.
    case "messages": {
    	if (!isMessagesArgs(args)) {
    		throw new Error("Invalid arguments for messages tool");
    	}
    
    	try {
    		const messageModule = await loadModule("message");
    
    		switch (args.operation) {
    			case "send": {
    				if (!args.phoneNumber || !args.message) {
    					throw new Error(
    						"Phone number and message are required for send operation",
    					);
    				}
    				await messageModule.sendMessage(args.phoneNumber, args.message);
    				return {
    					content: [
    						{
    							type: "text",
    							text: `Message sent to ${args.phoneNumber}`,
    						},
    					],
    					isError: false,
    				};
    			}
    
    			case "read": {
    				if (!args.phoneNumber) {
    					throw new Error(
    						"Phone number is required for read operation",
    					);
    				}
    				const messages = await messageModule.readMessages(
    					args.phoneNumber,
    					args.limit,
    				);
    				return {
    					content: [
    						{
    							type: "text",
    							text:
    								messages.length > 0
    									? messages
    											.map(
    												(msg) =>
    													`[${new Date(msg.date).toLocaleString()}] ${msg.is_from_me ? "Me" : msg.sender}: ${msg.content}`,
    											)
    											.join("\n")
    									: "No messages found",
    						},
    					],
    					isError: false,
    				};
    			}
    
    			case "schedule": {
    				if (!args.phoneNumber || !args.message || !args.scheduledTime) {
    					throw new Error(
    						"Phone number, message, and scheduled time are required for schedule operation",
    					);
    				}
    				const scheduledMsg = await messageModule.scheduleMessage(
    					args.phoneNumber,
    					args.message,
    					new Date(args.scheduledTime),
    				);
    				return {
    					content: [
    						{
    							type: "text",
    							text: `Message scheduled to be sent to ${args.phoneNumber} at ${scheduledMsg.scheduledTime}`,
    						},
    					],
    					isError: false,
    				};
    			}
    
    			case "unread": {
    				const messages = await messageModule.getUnreadMessages(
    					args.limit,
    				);
    
    				// Look up contact names for all messages
    				const contactsModule = await loadModule("contacts");
    				const messagesWithNames = await Promise.all(
    					messages.map(async (msg) => {
    						// Only look up names for messages not from me
    						if (!msg.is_from_me) {
    							const contactName =
    								await contactsModule.findContactByPhone(msg.sender);
    							return {
    								...msg,
    								displayName: contactName || msg.sender, // Use contact name if found, otherwise use phone/email
    							};
    						}
    						return {
    							...msg,
    							displayName: "Me",
    						};
    					}),
    				);
    
    				return {
    					content: [
    						{
    							type: "text",
    							text:
    								messagesWithNames.length > 0
    									? `Found ${messagesWithNames.length} unread message(s):\n` +
    										messagesWithNames
    											.map(
    												(msg) =>
    													`[${new Date(msg.date).toLocaleString()}] From ${msg.displayName}:\n${msg.content}`,
    											)
    											.join("\n\n")
    									: "No unread messages found",
    						},
    					],
    					isError: false,
    				};
    			}
    
    			default:
    				throw new Error(`Unknown operation: ${args.operation}`);
    		}
    	} catch (error) {
    		const errorMessage = error instanceof Error ? error.message : String(error);
    		return {
    			content: [
    				{
    					type: "text",
    					text: errorMessage.includes("access") ? errorMessage : `Error with messages operation: ${errorMessage}`,
    				},
    			],
    			isError: true,
    		};
    	}
    }
  • Tool schema definition including name, description, and inputSchema with properties for different operations.
    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"]
      }
    };
  • index.ts:195-197 (registration)
    Registers all tools (including 'messages') by handling ListToolsRequestSchema and returning the tools array from tools.ts.
    server.setRequestHandler(ListToolsRequestSchema, async () => ({
    	tools,
    }));
  • Type guard function for validating input arguments to the 'messages' tool.
    function isMessagesArgs(args: unknown): args is {
    	operation: "send" | "read" | "schedule" | "unread";
    	phoneNumber?: string;
    	message?: string;
    	limit?: number;
    	scheduledTime?: string;
    } {
    	if (typeof args !== "object" || args === null) return false;
    
    	const { operation, phoneNumber, message, limit, scheduledTime } = args as any;
    
    	if (
    		!operation ||
    		!["send", "read", "schedule", "unread"].includes(operation)
    	) {
    		return false;
    	}
    
    	// Validate required fields based on operation
    	switch (operation) {
    		case "send":
    		case "schedule":
    			if (!phoneNumber || !message) return false;
    			if (operation === "schedule" && !scheduledTime) return false;
    			break;
    		case "read":
    			if (!phoneNumber) return false;
    			break;
    		case "unread":
    			// No additional required fields
    			break;
    	}
    
    	// Validate field types if present
    	if (phoneNumber && typeof phoneNumber !== "string") return false;
    	if (message && typeof message !== "string") return false;
    	if (limit && typeof limit !== "number") return false;
    	if (scheduledTime && typeof scheduledTime !== "string") return false;
    
    	return true;
    }
  • Supporting utility functions for Messages app interaction: database access checks, message sending, reading recent/unread messages, scheduling, AppleScript fallbacks, etc.
    import {runAppleScript} from 'run-applescript';
    import { promisify } from 'node:util';
    import { exec } from 'node:child_process';
    import { access } from 'node:fs/promises';
    
    const execAsync = promisify(exec);
    
    // Configuration
    const CONFIG = {
        // Maximum messages to process (to avoid performance issues)
        MAX_MESSAGES: 50,
        // Maximum content length for previews
        MAX_CONTENT_PREVIEW: 300,
        // Timeout for operations
        TIMEOUT_MS: 8000
    };
    
    // Retry configuration
    const MAX_RETRIES = 3;
    const RETRY_DELAY = 1000; // 1 second
    
    async function sleep(ms: number) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    async function retryOperation<T>(operation: () => Promise<T>, retries = MAX_RETRIES, delay = RETRY_DELAY): Promise<T> {
        try {
            return await operation();
        } catch (error) {
            if (retries > 0) {
                console.error(`Operation failed, retrying... (${retries} attempts remaining)`);
                await sleep(delay);
                return retryOperation(operation, retries - 1, delay);
            }
            throw error;
        }
    }
    
    function normalizePhoneNumber(phone: string): string[] {
        // Remove all non-numeric characters except +
        const cleaned = phone.replace(/[^0-9+]/g, '');
        
        // If it's already in the correct format (+1XXXXXXXXXX), return just that
        if (/^\+1\d{10}$/.test(cleaned)) {
            return [cleaned];
        }
        
        // If it starts with 1 and has 11 digits total
        if (/^1\d{10}$/.test(cleaned)) {
            return [`+${cleaned}`];
        }
        
        // If it's 10 digits
        if (/^\d{10}$/.test(cleaned)) {
            return [`+1${cleaned}`];
        }
        
        // If none of the above match, try multiple formats
        const formats = new Set<string>();
        
        if (cleaned.startsWith('+1')) {
            formats.add(cleaned);
        } else if (cleaned.startsWith('1')) {
            formats.add(`+${cleaned}`);
        } else {
            formats.add(`+1${cleaned}`);
        }
        
        return Array.from(formats);
    }
    
    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;
    }
    
    interface Message {
        content: string;
        date: string;
        sender: string;
        is_from_me: boolean;
        attachments?: string[];
        url?: string;
    }
    
    async function checkMessagesDBAccess(): Promise<boolean> {
        try {
            const dbPath = `${process.env.HOME}/Library/Messages/chat.db`;
            await access(dbPath);
            
            // Additional check - try to query the database
            await execAsync(`sqlite3 "${dbPath}" "SELECT 1;"`);
            
            return true;
        } catch (error) {
            console.error(`
    Error: Cannot access Messages database.
    To fix this, please grant Full Disk Access to Terminal/iTerm2:
    1. Open System Preferences
    2. Go to Security & Privacy > Privacy
    3. Select "Full Disk Access" from the left sidebar
    4. Click the lock icon to make changes
    5. Add Terminal.app or iTerm.app to the list
    6. Restart your terminal and try again
    
    Error details: ${error instanceof Error ? error.message : String(error)}
    `);
            return false;
        }
    }
    
    /**
     * Request Messages access and provide instructions if not available
     */
    async function requestMessagesAccess(): Promise<{ hasAccess: boolean; message: string }> {
        try {
            // Check database access first
            const hasDBAccess = await checkMessagesDBAccess();
            if (hasDBAccess) {
                return {
                    hasAccess: true,
                    message: "Messages access is already granted."
                };
            }
    
            // If no database access, check if Messages app is at least accessible
            try {
                await runAppleScript('tell application "Messages" to return name');
                return {
                    hasAccess: false,
                    message: "Messages app is accessible but database access is required. Please:\n1. Open System Settings > Privacy & Security > Full Disk Access\n2. Add your terminal application (Terminal.app or iTerm.app)\n3. Restart your terminal and try again\n4. Note: This is required to read message history from the Messages database"
                };
            } catch (error) {
                return {
                    hasAccess: false,
                    message: "Messages access is required but not granted. Please:\n1. Open System Settings > Privacy & Security > Automation\n2. Find your terminal/app and enable 'Messages'\n3. Also grant Full Disk Access in Privacy & Security > Full Disk Access\n4. Restart your terminal and try again"
                };
            }
        } catch (error) {
            return {
                hasAccess: false,
                message: `Error checking Messages access: ${error instanceof Error ? error.message : String(error)}`
            };
        }
    }
    
    function decodeAttributedBody(hexString: string): { text: string; url?: string } {
        try {
            // Convert hex to buffer
            const buffer = Buffer.from(hexString, 'hex');
            const content = buffer.toString();
            
            // Common patterns in attributedBody
            const patterns = [
                /NSString">(.*?)</,           // Basic NSString pattern
                /NSString">([^<]+)/,          // NSString without closing tag
                /NSNumber">\d+<.*?NSString">(.*?)</,  // NSNumber followed by NSString
                /NSArray">.*?NSString">(.*?)</,       // NSString within NSArray
                /"string":\s*"([^"]+)"/,      // JSON-style string
                /text[^>]*>(.*?)</,           // Generic XML-style text
                /message>(.*?)</              // Generic message content
            ];
            
            // Try each pattern
            let text = '';
            for (const pattern of patterns) {
                const match = content.match(pattern);
                if (match?.[1]) {
                    text = match[1];
                    if (text.length > 5) { // Only use if we got something substantial
                        break;
                    }
                }
            }
            
            // Look for URLs
            const urlPatterns = [
                /(https?:\/\/[^\s<"]+)/,      // Standard URLs
                /NSString">(https?:\/\/[^\s<"]+)/, // URLs in NSString
                /"url":\s*"(https?:\/\/[^"]+)"/, // URLs in JSON format
                /link[^>]*>(https?:\/\/[^<]+)/ // URLs in XML-style tags
            ];
            
            let url: string | undefined;
            for (const pattern of urlPatterns) {
                const match = content.match(pattern);
                if (match?.[1]) {
                    url = match[1];
                    break;
                }
            }
            
            if (!text && !url) {
                // Try to extract any readable text content
                const readableText = content
                    .replace(/streamtyped.*?NSString/g, '') // Remove streamtyped header
                    .replace(/NSAttributedString.*?NSString/g, '') // Remove attributed string metadata
                    .replace(/NSDictionary.*?$/g, '') // Remove dictionary metadata
                    .replace(/\+[A-Za-z]+\s/g, '') // Remove +[identifier] patterns
                    .replace(/NSNumber.*?NSValue.*?\*/g, '') // Remove number/value metadata
                    .replace(/[^\x20-\x7E]/g, ' ') // Replace non-printable chars with space
                    .replace(/\s+/g, ' ')          // Normalize whitespace
                    .trim();
                
                if (readableText.length > 5) {    // Only use if we got something substantial
                    text = readableText;
                } else {
                    return { text: '[Message content not readable]' };
                }
            }
    
            // Clean up the found text
            if (text) {
                text = text
                    .replace(/^[+\s]+/, '') // Remove leading + and spaces
                    .replace(/\s*iI\s*[A-Z]\s*$/, '') // Remove iI K pattern at end
                    .replace(/\s+/g, ' ') // Normalize whitespace
                    .trim();
            }
            
            return { text: text || url || '', url };
        } catch (error) {
            console.error('Error decoding attributedBody:', error);
            return { text: '[Message content not readable]' };
        }
    }
    
    async function getAttachmentPaths(messageId: number): Promise<string[]> {
        try {
            const query = `
                SELECT filename
                FROM attachment
                INNER JOIN message_attachment_join 
                ON attachment.ROWID = message_attachment_join.attachment_id
                WHERE message_attachment_join.message_id = ${messageId}
            `;
            
            const { stdout } = await execAsync(`sqlite3 -json "${process.env.HOME}/Library/Messages/chat.db" "${query}"`);
            
            if (!stdout.trim()) {
                return [];
            }
            
            const attachments = JSON.parse(stdout) as { filename: string }[];
            return attachments.map(a => a.filename).filter(Boolean);
        } catch (error) {
            console.error('Error getting attachments:', error);
            return [];
        }
    }
    
    async function readMessages(phoneNumber: string, limit = 10): Promise<Message[]> {
        try {
            // Enforce maximum limit for performance
            const maxLimit = Math.min(limit, CONFIG.MAX_MESSAGES);
            
            // Check access and get instructions if needed
            const accessResult = await requestMessagesAccess();
            if (!accessResult.hasAccess) {
                throw new Error(accessResult.message);
            }
    
            // 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 ${maxLimit}
            `;
    
            // 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 [];
        }
    }
    
    async function getUnreadMessages(limit = 10): Promise<Message[]> {
        try {
            // Enforce maximum limit for performance
            const maxLimit = Math.min(limit, CONFIG.MAX_MESSAGES);
            
            // Check access and get instructions if needed
            const accessResult = await requestMessagesAccess();
            if (!accessResult.hasAccess) {
                throw new Error(accessResult.message);
            }
    
            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 ${maxLimit}
            `;
    
            // 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 [];
        }
    }
    
    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
        };
    }
    
    /**
     * AppleScript fallback for reading messages (simplified, limited functionality)
     */
    async function readMessagesAppleScript(phoneNumber: string, limit: number): Promise<Message[]> {
        try {
            const script = `
    tell application "Messages"
        return "SUCCESS:messages_not_accessible_via_applescript"
    end tell`;
    
            const result = await runAppleScript(script) as string;
            
            if (result && result.includes('SUCCESS')) {
                // Return empty array with a note that AppleScript doesn't provide full message access
                return [];
            }
            
            return [];
        } catch (error) {
            console.error(`AppleScript fallback failed: ${error instanceof Error ? error.message : String(error)}`);
            return [];
        }
    }
    
    /**
     * AppleScript fallback for getting unread messages (simplified, limited functionality)
     */
    async function getUnreadMessagesAppleScript(limit: number): Promise<Message[]> {
        try {
            const script = `
    tell application "Messages"
        return "SUCCESS:unread_messages_not_accessible_via_applescript"
    end tell`;
    
            const result = await runAppleScript(script) as string;
            
            if (result && result.includes('SUCCESS')) {
                // Return empty array with a note that AppleScript doesn't provide full message access
                return [];
            }
            
            return [];
        } catch (error) {
            console.error(`AppleScript fallback failed: ${error instanceof Error ? error.message : String(error)}`);
            return [];
        }
    }
    
    export default { sendMessage, readMessages, scheduleMessage, getUnreadMessages, requestMessagesAccess };
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/supermemoryai/apple-mcp'

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