Skip to main content
Glama
anipotts

imessage-mcp

by anipotts

double_texts

Analyze iMessage conversations to identify double-texting patterns and unanswered message sequences, showing frequency, longest bursts, and contact comparisons for messaging behavior insights.

Instructions

Detect double-texting and unanswered message patterns. Finds when you (or a contact) sent multiple consecutive messages without a reply. Shows frequency, longest bursts, and who does it more. Omit contact for a global ranking of who you double-text the most.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
contactNoContact handle or name (omit for global double-text ranking)
min_consecutiveNoMinimum consecutive messages to count (default: 2)
date_fromNoStart date (ISO)
date_toNoEnd date (ISO)
limitNoMax burst results (default 20)

Implementation Reference

  • Main handler function that implements the double_texts tool logic. Handles two modes: global ranking (showing who the user double-texts most) and contact-specific mode (showing detailed double-text patterns for a specific contact). Uses island-and-gaps SQL technique to detect consecutive message bursts.
    async (params) => {
      const db = getDb();
      const limit = clamp(params.limit ?? 20, 1, MAX_LIMIT);
      const minConsecutive = params.min_consecutive ?? 2;
    
      if (!params.contact) {
        // Global ranking: who does the user double-text most?
        const conditions = [
          "(m.text IS NOT NULL OR m.attributedBody IS NOT NULL)",
          "m.associated_message_type = 0",
        ];
        const bindings: Record<string, any> = { min_consecutive: minConsecutive, limit };
    
        // Apply spam filter for global queries
        conditions.push(repliedToCondition());
    
        if (params.date_from) {
          conditions.push(`${DATE_EXPR} >= @date_from`);
          bindings.date_from = params.date_from;
        }
        if (params.date_to) {
          conditions.push(`${DATE_EXPR} <= @date_to`);
          bindings.date_to = params.date_to;
        }
    
        const where = conditions.join(" AND ");
    
        const ranking = db.prepare(`
          WITH msgs AS (
            SELECT m.is_from_me, ${DATE_EXPR} as date, h.id as handle,
              ROW_NUMBER() OVER (PARTITION BY h.id ORDER BY m.date) as rn,
              ROW_NUMBER() OVER (PARTITION BY h.id, m.is_from_me ORDER BY m.date) as part_rn
            FROM message m
            JOIN handle h ON m.handle_id = h.ROWID
            WHERE ${where}
          ),
          runs AS (
            SELECT handle, is_from_me, COUNT(*) as consecutive
            FROM msgs GROUP BY handle, is_from_me, rn - part_rn
            HAVING consecutive >= @min_consecutive
          )
          SELECT handle,
            SUM(CASE WHEN is_from_me = 1 THEN 1 ELSE 0 END) as your_double_texts,
            SUM(CASE WHEN is_from_me = 0 THEN 1 ELSE 0 END) as their_double_texts,
            MAX(consecutive) as max_burst
          FROM runs
          GROUP BY handle
          ORDER BY your_double_texts DESC
          LIMIT @limit
        `).all(bindings) as any[];
    
        const enriched = ranking.map((row: any) => {
          const c = lookupContact(row.handle);
          return {
            handle: row.handle,
            name: c.name,
            your_double_texts: row.your_double_texts,
            their_double_texts: row.their_double_texts,
            max_burst: row.max_burst,
          };
        });
    
        return {
          content: [{
            type: "text",
            text: JSON.stringify({ min_consecutive: minConsecutive, ranking: enriched }, null, 2),
          }],
        };
      }
    
      // Contact-specific mode
      const conditions = [
        "h.id LIKE @contact",
        "(m.text IS NOT NULL OR m.attributedBody IS NOT NULL)",
        "m.associated_message_type = 0",
      ];
      const bindings: Record<string, any> = {
        contact: `%${params.contact}%`,
        min_consecutive: minConsecutive,
        limit,
      };
    
      if (params.date_from) {
        conditions.push(`${DATE_EXPR} >= @date_from`);
        bindings.date_from = params.date_from;
      }
      if (params.date_to) {
        conditions.push(`${DATE_EXPR} <= @date_to`);
        bindings.date_to = params.date_to;
      }
    
      const where = conditions.join(" AND ");
    
      // Find consecutive message bursts using island-and-gaps
      const bursts = db.prepare(`
        WITH msgs AS (
          SELECT m.is_from_me, ${DATE_EXPR} as date,
            ROW_NUMBER() OVER (ORDER BY m.date) as rn,
            ROW_NUMBER() OVER (PARTITION BY m.is_from_me ORDER BY m.date) as part_rn
          FROM message m
          JOIN handle h ON m.handle_id = h.ROWID
          WHERE ${where}
        )
        SELECT
          is_from_me,
          COUNT(*) as consecutive,
          MIN(date) as first_msg,
          MAX(date) as last_msg
        FROM msgs
        GROUP BY is_from_me, rn - part_rn
        HAVING consecutive >= @min_consecutive
        ORDER BY consecutive DESC
        LIMIT @limit
      `).all(bindings) as any[];
    
      // Summary stats
      const summary = db.prepare(`
        WITH msgs AS (
          SELECT m.is_from_me, ${DATE_EXPR} as date,
            ROW_NUMBER() OVER (ORDER BY m.date) as rn,
            ROW_NUMBER() OVER (PARTITION BY m.is_from_me ORDER BY m.date) as part_rn
          FROM message m
          JOIN handle h ON m.handle_id = h.ROWID
          WHERE ${where}
        ),
        runs AS (
          SELECT is_from_me, COUNT(*) as consecutive
          FROM msgs GROUP BY is_from_me, rn - part_rn
          HAVING consecutive >= @min_consecutive
        )
        SELECT
          is_from_me,
          COUNT(*) as times,
          ROUND(AVG(consecutive), 1) as avg_burst,
          MAX(consecutive) as max_burst
        FROM runs
        Group BY is_from_me
      `).all(bindings) as any[];
    
      const contact = lookupContact(params.contact);
    
      const formattedSummary: Record<string, any> = {};
      for (const s of summary as any[]) {
        formattedSummary[s.is_from_me ? "you" : contact.name] = {
          times: s.times,
          avg_burst: s.avg_burst,
          max_burst: s.max_burst,
        };
      }
    
      return {
        content: [{
          type: "text",
          text: JSON.stringify({
            contact: contact.name,
            min_consecutive: minConsecutive,
            summary: formattedSummary,
            top_bursts: bursts.map((b: any) => ({
              from: b.is_from_me ? "you" : contact.name,
              consecutive_messages: b.consecutive,
              first_msg: b.first_msg,
              last_msg: b.last_msg,
            })),
          }, null, 2),
        }],
      };
    },
  • Tool registration with name, description, input schema (using zod validation), and handler hints for the double_texts tool.
    // -- double_texts --
    server.tool(
      "double_texts",
      "Detect double-texting and unanswered message patterns. Finds when you (or a contact) sent multiple consecutive messages without a reply. Shows frequency, longest bursts, and who does it more. Omit contact for a global ranking of who you double-text the most.",
      {
        contact: z.string().optional().describe("Contact handle or name (omit for global double-text ranking)"),
        min_consecutive: z.number().optional().describe("Minimum consecutive messages to count (default: 2)"),
        date_from: isoDateSchema.optional().describe("Start date (ISO)"),
        date_to: isoDateSchema.optional().describe("End date (ISO)"),
        limit: z.number().optional().describe("Max burst results (default 20)"),
      },
      { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
  • Zod schema for ISO date strings (YYYY-MM-DD format) used by the double_texts tool for date_from and date_to parameters.
    /** 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");
  • src/index.ts:22-22 (registration)
    Import of registerPatternTools function which registers the double_texts tool along with other pattern tools (who_initiates, streaks, conversation_gaps, forgotten_contacts).
    import { registerPatternTools } from "./tools/patterns.js";

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