get_contact
Retrieve detailed iMessage contact analytics including message statistics, conversation patterns, and recent interactions from local macOS databases.
Instructions
Deep info on a specific contact: tier, message stats, yearly breakdown, and recent messages.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| contact | Yes | Contact handle (phone/email) or name fragment |
Implementation Reference
- src/tools/contacts.ts:82-147 (handler)Complete implementation of get_contact tool: registration with MCP server, schema definition (contact parameter), and handler logic that retrieves contact info, message stats, yearly breakdown, and recent messages from the iMessage database.
server.tool( "get_contact", "Deep info on a specific contact: tier, message stats, yearly breakdown, and recent messages.", { contact: z.string().describe("Contact handle (phone/email) or name fragment"), }, { readOnlyHint: true, destructiveHint: false, openWorldHint: false }, async (params) => { const db = getDb(); const handle = params.contact; // Find matching handles const handles = db.prepare(` SELECT DISTINCT h.id FROM handle h WHERE h.id LIKE @pattern `).all({ pattern: `%${handle}%` }) as any[]; if (handles.length === 0) { return { content: [{ type: "text", text: `No contact found matching "${handle}"` }] }; } const results: any[] = []; for (const { id } of handles) { const contact = lookupContact(id); // Overall stats 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, AVG(LENGTH(m.text)) as avg_length, MIN(${DATE_EXPR}) as first_message, MAX(${DATE_EXPR}) as last_message FROM message m JOIN handle h ON m.handle_id = h.ROWID WHERE h.id = @id AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL) ${MSG_FILTER} `).get({ id }) as any; // Yearly breakdown const yearly = db.prepare(` SELECT strftime('%Y', ${DATE_EXPR}) as year, COUNT(*) as messages, 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 = @id AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL) ${MSG_FILTER} GROUP BY year ORDER BY year `).all({ id }); results.push({ handle: id, name: contact.name, tier: contact.tier, stats, yearly_breakdown: yearly, }); } return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }], }; }, ); - src/contacts.ts:146-157 (helper)lookupContact helper function that resolves contact information from macOS AddressBook, returning name and tier (known/unknown) for a given handle (phone number or email).
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/index.ts:52-52 (registration)Registration call that activates all contact tools including get_contact by invoking registerContactTools from the tools module.
registerContactTools(server); - src/db.ts:17-20 (helper)Database constants DATE_EXPR and MSG_FILTER used in get_contact SQL queries for timestamp conversion and message filtering.
export const DATE_EXPR = `datetime(m.date/1000000000 + ${APPLE_EPOCH_OFFSET}, 'unixepoch', 'localtime')`; // Filter out tapbacks and object-replacement-character-only messages export const MSG_FILTER = `AND m.associated_message_type = 0 AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL) AND COALESCE(m.text, '') <> '\ufffc' AND COALESCE(m.text, '') NOT LIKE '\ufffc\ufffc%'`;