Skip to main content
Glama

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

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