Skip to main content
Glama
anipotts

imessage-mcp

by anipotts

forgotten_contacts

Identify contacts you haven't messaged in a long time to help reconnect with dormant relationships from your iMessage history.

Instructions

Find dormant relationships — contacts you used to message but haven't talked to in a long time. Great for reconnecting with people you've lost touch with. By default excludes contacts you've never replied to.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
min_messagesNoMinimum past messages to qualify (default: 10)
inactive_daysNoDays of inactivity to count as 'forgotten' (default: 365)
include_allNoInclude messages from all contacts, even those you've never replied to (default: false)
limitNoMax results (default 20)

Implementation Reference

  • Complete implementation of the forgotten_contacts tool. Registers the tool with MCP server, defines Zod schema for parameters (min_messages, inactive_days, include_all, limit), queries the iMessage database to find dormant contacts who haven't been messaged in the specified days, and returns enriched results with contact names, message counts, and days since last contact.
    server.tool(
      "forgotten_contacts",
      "Find dormant relationships — contacts you used to message but haven't talked to in a long time. Great for reconnecting with people you've lost touch with. By default excludes contacts you've never replied to.",
      {
        min_messages: z.number().optional().describe("Minimum past messages to qualify (default: 10)"),
        inactive_days: z.number().optional().describe("Days of inactivity to count as 'forgotten' (default: 365)"),
        include_all: z.boolean().optional().describe("Include messages from all contacts, even those you've never replied to (default: false)"),
        limit: z.number().optional().describe("Max results (default 20)"),
      },
      { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
      async (params) => {
        const db = getDb();
        const limit = clamp(params.limit ?? 20, 1, MAX_LIMIT);
        const minMessages = params.min_messages ?? 10;
    
        const cutoff = new Date();
        cutoff.setDate(cutoff.getDate() - (params.inactive_days ?? 365));
        const cutoffDate = cutoff.toISOString().slice(0, 10);
    
        const repliedTo = params.include_all ? '' : `AND ${repliedToCondition()}`;
    
        const rows = db.prepare(`
          SELECT
            h.id as handle,
            COUNT(*) as total_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,
            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 (m.text IS NOT NULL OR m.attributedBody IS NOT NULL) ${MSG_FILTER} ${repliedTo}
          GROUP BY h.id
          HAVING total_messages >= @min_messages AND MAX(${DATE_EXPR}) < @cutoff
          ORDER BY total_messages DESC
          LIMIT @limit
        `).all({ min_messages: minMessages, cutoff: cutoffDate, limit }) as any[];
    
        const enriched = rows.map((row: any) => {
          const contact = lookupContact(row.handle);
          const lastDate = new Date(row.last_message);
          const daysSince = Math.floor((Date.now() - lastDate.getTime()) / 86400000);
          return {
            handle: row.handle,
            name: contact.name,
            total_messages: row.total_messages,
            sent: row.sent,
            received: row.received,
            first_message: row.first_message,
            last_message: row.last_message,
            days_since_last: daysSince,
          };
        });
    
        return {
          content: [{
            type: "text",
            text: JSON.stringify({
              inactive_threshold_days: params.inactive_days ?? 365,
              min_messages: minMessages,
              forgotten: enriched,
            }, null, 2),
          }],
        };
      },
    );
  • Zod schema definition for the forgotten_contacts tool parameters. Defines optional parameters: min_messages (default: 10), inactive_days (default: 365), include_all (default: false), and limit (default: 20).
    {
      min_messages: z.number().optional().describe("Minimum past messages to qualify (default: 10)"),
      inactive_days: z.number().optional().describe("Days of inactivity to count as 'forgotten' (default: 365)"),
      include_all: z.boolean().optional().describe("Include messages from all contacts, even those you've never replied to (default: false)"),
      limit: z.number().optional().describe("Max results (default 20)"),
    },
  • Core SQL query and result enrichment logic. Queries the messages table to find contacts with minimum message count whose last message was before the cutoff date. Enriches results with contact names via lookupContact() and calculates days since last message.
    const rows = db.prepare(`
      SELECT
        h.id as handle,
        COUNT(*) as total_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,
        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 (m.text IS NOT NULL OR m.attributedBody IS NOT NULL) ${MSG_FILTER} ${repliedTo}
      GROUP BY h.id
      HAVING total_messages >= @min_messages AND MAX(${DATE_EXPR}) < @cutoff
      ORDER BY total_messages DESC
      LIMIT @limit
    `).all({ min_messages: minMessages, cutoff: cutoffDate, limit }) as any[];
    
    const enriched = rows.map((row: any) => {
      const contact = lookupContact(row.handle);
      const lastDate = new Date(row.last_message);
      const daysSince = Math.floor((Date.now() - lastDate.getTime()) / 86400000);
      return {
        handle: row.handle,
        name: contact.name,
        total_messages: row.total_messages,
        sent: row.sent,
        received: row.received,
        first_message: row.first_message,
        last_message: row.last_message,
        days_since_last: daysSince,
      };
    });
  • Helper function clamp() used by forgotten_contacts to enforce minimum and maximum limits on the results (clamps between 1 and MAX_LIMIT).
    export function clamp(val: number, min: number, max: number): number {
      return Math.max(min, Math.min(max, val));
    }
  • Helper function repliedToCondition() used by forgotten_contacts to filter results to only include contacts the user has replied to (unless include_all parameter is true).
    export function repliedToCondition(): string {
      return `(m.is_from_me = 1 OR h.id IN (
        SELECT DISTINCT h2.id FROM handle h2
        JOIN message m2 ON m2.handle_id = h2.ROWID
        WHERE m2.is_from_me = 1
      ))`;
    }

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