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
| Name | Required | Description | Default |
|---|---|---|---|
| operation | Yes | Operation to perform: 'send', 'read', 'schedule', or 'unread' | |
| phoneNumber | No | Phone number to send message to (required for send, read, and schedule operations) | |
| message | No | Message to send (required for send and schedule operations) | |
| limit | No | Number of messages to read (optional, for read and unread operations) | |
| scheduledTime | No | ISO string of when to send the message (required for schedule operation) |
Implementation Reference
- tools.ts:49-79 (schema)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;
- utils/message.ts:62-71 (helper)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; }
- utils/message.ts:213-341 (helper)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 []; } }
- utils/message.ts:343-464 (helper)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 []; } }
- utils/message.ts:466-501 (helper)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 }; }