Skip to main content
Glama
anipotts

imessage-mcp

by anipotts

contact_stats

Analyze iMessage conversation patterns for a specific contact by tracking message volumes, response times, and yearly trends to understand communication dynamics.

Instructions

Deep per-contact analytics: message volumes, response time estimates, conversation patterns, and yearly trends.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
contactYesContact handle or name fragment
date_fromNoStart date
date_toNoEnd date

Implementation Reference

  • Complete implementation of the contact_stats tool - registers the tool with MCP server and defines the handler function that retrieves per-contact analytics including message volumes, response time estimates, conversation patterns, monthly trends, and hourly distributions.
    server.tool(
      "contact_stats",
      "Deep per-contact analytics: message volumes, response time estimates, conversation patterns, and yearly trends.",
      {
        contact: z.string().describe("Contact handle or name fragment"),
        date_from: isoDateSchema.optional().describe("Start date"),
        date_to: isoDateSchema.optional().describe("End date"),
      },
      { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
      async (params) => {
        const db = getDb();
    
        const dateFilter = [];
        const bindings: Record<string, any> = {};
        bindings.contact = `%${params.contact}%`;
    
        if (params.date_from) {
          dateFilter.push(`${DATE_EXPR} >= @date_from`);
          bindings.date_from = params.date_from;
        }
        if (params.date_to) {
          dateFilter.push(`${DATE_EXPR} <= @date_to`);
          bindings.date_to = params.date_to;
        }
    
        const extraWhere = dateFilter.length > 0 ? "AND " + dateFilter.join(" AND ") : "";
    
        // Basic stats
        const stats = db.prepare(`
          SELECT
            h.id as handle,
            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,
            ROUND(AVG(LENGTH(m.text)), 1) as avg_length,
            ROUND(AVG(CASE WHEN m.is_from_me = 1 THEN LENGTH(m.text) END), 1) as avg_sent_length,
            ROUND(AVG(CASE WHEN m.is_from_me = 0 THEN LENGTH(m.text) END), 1) as avg_received_length,
            MIN(${DATE_EXPR}) as first_message,
            MAX(${DATE_EXPR}) as last_message,
            SUM(m.cache_has_attachments) as attachment_count
          FROM message m
          JOIN handle h ON m.handle_id = h.ROWID
          WHERE h.id LIKE @contact AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL) ${MSG_FILTER} ${extraWhere}
          GROUP BY h.id
          ORDER BY total DESC
        `).all(bindings) as any[];
    
        if (stats.length === 0) {
          return { content: [{ type: "text", text: `No messages found for "${params.contact}"` }] };
        }
    
        // Monthly trend for top handle
        const topHandle = stats[0].handle;
        const monthly = db.prepare(`
          SELECT
            strftime('%Y-%m', ${DATE_EXPR}) as month,
            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 = @handle AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL) ${MSG_FILTER} ${extraWhere}
          GROUP BY month
          ORDER BY month
        `).all({ ...bindings, handle: topHandle });
    
        // Hour-of-day distribution
        const hourly = db.prepare(`
          SELECT
            CAST(strftime('%H', ${DATE_EXPR}) AS INTEGER) as hour,
            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 = @handle AND (m.text IS NOT NULL OR m.attributedBody IS NOT NULL) ${MSG_FILTER} ${extraWhere}
          GROUP BY hour
          ORDER BY hour
        `).all({ ...bindings, handle: topHandle });
    
        const contact = lookupContact(topHandle);
    
        return {
          content: [{
            type: "text",
            text: JSON.stringify({
              contact: { handle: topHandle, name: contact.name, tier: contact.tier },
              stats: stats[0],
              monthly_trend: monthly,
              hourly_distribution: hourly,
              all_matching_handles: stats.length > 1 ? stats : undefined,
            }, null, 2),
          }],
        };
      },
    );
  • Input schema definition for contact_stats tool - uses Zod to validate contact (string), date_from (optional ISO date), and date_to (optional ISO date) parameters.
    {
      contact: z.string().describe("Contact handle or name fragment"),
      date_from: isoDateSchema.optional().describe("Start date"),
      date_to: isoDateSchema.optional().describe("End date"),
    },
  • isoDateSchema definition - a Zod schema that validates ISO date strings in YYYY-MM-DD format, used for date filtering in contact_stats and other tools.
    /** Zod schema for ISO date strings (YYYY-MM-DD) */
    export const isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Must be ISO format: YYYY-MM-DD");
  • lookupContact helper function - resolves a handle (phone/email) to contact name using macOS AddressBook, falling back to raw handle. Used by contact_stats to enrich results with contact names and tiers.
    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" };
    }
  • getDb function - provides singleton access to the read-only SQLite database connection for iMessage chat.db, used by contact_stats to execute queries.
    export function getDb(): Database.Database {
      if (!_db) {
        _db = new Database(CHAT_DB, { readonly: true, fileMustExist: true });
        _db.pragma("journal_mode = WAL");
        _db.pragma("query_only = ON");
      }
      return _db;
    }

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