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
| Name | Required | Description | Default |
|---|---|---|---|
| contact | Yes | Contact handle (phone/email) or name |
Implementation Reference
- src/tools/memories.ts:134-192 (handler)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), }], }; }, - src/tools/memories.ts:127-133 (registration)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); - src/contacts.ts:146-157 (helper)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" }; } - src/db.ts:182-190 (helper)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; }