Skip to main content
Glama
anipotts

imessage-mcp

by anipotts

first_last_message

Retrieve the initial and final messages exchanged with a contact in iMessage for sentimental lookups and conversation history review.

Instructions

The very first and very last message ever exchanged with a contact. People use this for sentimental lookups like 'what was the first text I sent my partner?' or 'what was the last thing my grandparent texted me?'

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
contactYesContact handle (phone/email) or name

Implementation Reference

  • Main handler function for first_last_message tool. Queries the database for the first and last messages exchanged with a contact, retrieves message statistics (total, sent, received), processes message text using safeText and getMessageText helpers, looks up contact information, and returns a JSON response with contact info, statistics, and the first/last messages with their text, date, and sender.
    async (params) => {
      const db = getDb();
      const pattern = `%${params.contact}%`;
    
      const first = db.prepare(`
        SELECT m.text, m.attributedBody, m.is_from_me, ${DATE_EXPR} as date, h.id as handle
        FROM message m JOIN handle h ON m.handle_id = h.ROWID
        WHERE h.id LIKE @contact ${MSG_FILTER}
        ORDER BY m.date ASC LIMIT 1
      `).get({ contact: pattern }) as any;
    
      if (!first) {
        return { content: [{ type: "text", text: `No messages found for "${params.contact}"` }] };
      }
    
      const last = db.prepare(`
        SELECT m.text, m.attributedBody, m.is_from_me, ${DATE_EXPR} as date, h.id as handle
        FROM message m JOIN handle h ON m.handle_id = h.ROWID
        WHERE h.id LIKE @contact ${MSG_FILTER}
        ORDER BY m.date DESC LIMIT 1
      `).get({ contact: pattern }) as any;
    
      const stats = db.prepare(`
        SELECT COUNT(*) as total,
          SUM(CASE WHEN m.is_from_me = 1 THEN 1 ELSE 0 END) as sent,
          SUM(CASE WHEN m.is_from_me = 0 THEN 1 ELSE 0 END) as received
        FROM message m JOIN handle h ON m.handle_id = h.ROWID
        WHERE h.id LIKE @contact ${MSG_FILTER}
      `).get({ contact: pattern }) as any;
    
      first.text = safeText(getMessageText(first));
      delete first.attributedBody;
      last.text = safeText(getMessageText(last));
      delete last.attributedBody;
    
      const contact = lookupContact(first.handle);
    
      return {
        content: [{
          type: "text",
          text: JSON.stringify({
            contact: { handle: first.handle, name: contact.name },
            total_messages: stats.total,
            sent: stats.sent,
            received: stats.received,
            first_message: {
              text: first.text,
              date: first.date,
              from: first.is_from_me ? "you" : contact.name,
            },
            last_message: {
              text: last.text,
              date: last.date,
              from: last.is_from_me ? "you" : contact.name,
            },
          }, null, 2),
        }],
      };
    },
  • Tool registration for first_last_message. Defines the tool name, description, input schema (contact parameter as a string), and hints (readOnlyHint, destructiveHint, openWorldHint) for the MCP server.
    server.tool(
      "first_last_message",
      "The very first and very last message ever exchanged with a contact. People use this for sentimental lookups like 'what was the first text I sent my partner?' or 'what was the last thing my grandparent texted me?'",
      {
        contact: z.string().describe("Contact handle (phone/email) or name"),
      },
      { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
  • src/index.ts:61-61 (registration)
    Registration of the memory tools module (which includes first_last_message) with the MCP server in the main server initialization.
    registerMemoryTools(server);
  • lookupContact helper function used by first_last_message to resolve contact handles (phone numbers/emails) to human-readable names from the macOS AddressBook database.
    export function lookupContact(handle: string): Contact {
      if (!handle) return { id: "", name: "(unknown)", tier: "unknown" };
      const cleaned = handle.trim();
    
      // Resolve from macOS AddressBook
      const name = resolveFromAddressBook(cleaned);
      if (name) {
        return { id: cleaned, name, tier: "known" };
      }
    
      return { id: cleaned, name: cleaned, tier: "unknown" };
    }
  • getMessageText helper function used to extract text from message records, falling back to extracting text from the attributedBody binary blob when the text column is null or contains only object replacement characters.
    export function getMessageText(row: any): string | null {
      if (row.text && row.text !== "\ufffc" && !row.text.startsWith("\ufffc\ufffc")) {
        return row.text;
      }
      if (row.attributedBody) {
        return extractTextFromAttributedBody(row.attributedBody);
      }
      return null;
    }

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/anipotts/imessage-mcp'

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