Skip to main content
Glama
carterlasalle

mac-messages-mcp

tool_fuzzy_search_messages

Search macOS Messages with fuzzy matching to find relevant conversations from recent hours using adjustable similarity thresholds.

Instructions

Fuzzy search for messages containing the search_term within the last N hours.
Returns messages that match the search term with a similarity score.

Args:
    search_term: The text to search for in messages.
    hours: How many hours back to search (default 24). Must be positive.
    threshold: Similarity threshold for matching (0.0 to 1.0, default 0.6). Lower is more lenient.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
search_termYes
hoursNo
thresholdNo

Implementation Reference

  • MCP tool handler and registration for 'tool_fuzzy_search_messages'. Performs input validation on hours and threshold, logs the search, calls the core fuzzy_search_messages helper, and handles exceptions.
    @mcp.tool()
    def tool_fuzzy_search_messages(
        ctx: Context, search_term: str, hours: int = 24, threshold: float = 0.6
    ) -> str:
        """
        Fuzzy search for messages containing the search_term within the last N hours.
        Returns messages that match the search term with a similarity score.
    
        Args:
            search_term: The text to search for in messages.
            hours: How many hours back to search (default 24). Must be positive.
            threshold: Similarity threshold for matching (0.0 to 1.0, default 0.6). Lower is more lenient.
        """
        if not (0.0 <= threshold <= 1.0):
            return "Error: Threshold must be between 0.0 and 1.0."
        if hours <= 0:
            return "Error: Hours must be a positive integer."
    
        logger.info(
            f"Tool: Fuzzy searching messages for '{search_term}' in last {hours} hours with threshold {threshold}"
        )
        try:
            result = fuzzy_search_messages(
                search_term=search_term, hours=hours, threshold=threshold
            )
            return result
        except Exception as e:
            logger.error(f"Error in tool_fuzzy_search_messages: {e}", exc_info=True)
            return f"An unexpected error occurred during fuzzy message search: {str(e)}"
  • Core implementation of fuzzy message search. Queries recent messages from chat.db (last N hours, limited to 500), extracts text from text or attributedBody fields, cleans text, uses thefuzz.fuzz.WRatio for similarity scoring against search_term above threshold, formats results with timestamps, scores, sender, and group chat info.
    def fuzzy_search_messages(
        search_term: str,
        hours: int = 24,
        threshold: float = 0.6,  # Default threshold adjusted for thefuzz
    ) -> str:
        """
        Fuzzy search for messages containing the search_term within the last N hours.
    
        Args:
            search_term: The string to search for in message content.
            hours: Number of hours to look back (default: 24).
            threshold: Minimum similarity score (0.0-1.0) to consider a match (default: 0.6 for WRatio).
                       A lower threshold allows for more lenient matching.
    
        Returns:
            Formatted string with matching messages and their scores, or an error/no results message.
        """
        # Input validation
        if not search_term or not search_term.strip():
            return "Error: Search term cannot be empty."
        
        if hours < 0:
            return "Error: Hours cannot be negative. Please provide a positive number."
        
        # Prevent integer overflow - limit to reasonable maximum (10 years)
        MAX_HOURS = 10 * 365 * 24  # 87,600 hours
        if hours > MAX_HOURS:
            return f"Error: Hours value too large. Maximum allowed is {MAX_HOURS} hours (10 years)."
        
        if not (0.0 <= threshold <= 1.0):
            return "Error: Threshold must be between 0.0 and 1.0."
        
        # Calculate the timestamp for X hours ago
        current_time = datetime.now(timezone.utc)
        hours_ago_dt = current_time - timedelta(hours=hours)
        apple_epoch = datetime(2001, 1, 1, tzinfo=timezone.utc)
        seconds_since_apple_epoch = (hours_ago_dt - apple_epoch).total_seconds()
        
        # Convert to nanoseconds (Apple's format)
        nanoseconds_since_apple_epoch = int(seconds_since_apple_epoch * 1_000_000_000)
        timestamp_str = str(nanoseconds_since_apple_epoch)
    
        # Build the SQL query to get all messages in the time window
        # Limiting to 500 messages to avoid performance issues with very large message histories.
        query = """
        SELECT
            m.ROWID,
            m.date,
            m.text,
            m.attributedBody,
            m.is_from_me,
            m.handle_id,
            m.cache_roomnames
        FROM
            message m
        WHERE
            CAST(m.date AS TEXT) > ?
        ORDER BY m.date DESC
        LIMIT 500
        """
        params = (timestamp_str,)
        raw_messages = query_messages_db(query, params)
    
        if not raw_messages:
            return f"No messages found in the last {hours} hours to search."
        if "error" in raw_messages[0]:
            return f"Error accessing messages: {raw_messages[0]['error']}"
    
        message_candidates = []
        for msg_dict in raw_messages:
            body = msg_dict.get("text") or extract_body_from_attributed(
                msg_dict.get("attributedBody")
            )
            if body and body.strip():
                message_candidates.append((body, msg_dict))
    
        if not message_candidates:
            return f"No message content found to search in the last {hours} hours."
    
        # --- New fuzzy matching logic using thefuzz ---
        cleaned_search_term = clean_name(search_term).lower()
        # thefuzz scores are 0-100. Scale the input threshold (0.0-1.0).
        scaled_threshold = threshold * 100
    
        matched_messages_with_scores = []
        for original_message_text, msg_dict_value in message_candidates:
            # We use the original_message_text for matching, which might contain HTML entities etc.
            # clean_name will handle basic cleaning like emoji removal.
            cleaned_candidate_text = clean_name(original_message_text).lower()
    
            # Using WRatio for a good balance of matching strategies.
            score_from_thefuzz = fuzz.WRatio(cleaned_search_term, cleaned_candidate_text)
    
            if score_from_thefuzz >= scaled_threshold:
                # Store score as 0.0-1.0 for consistency with how threshold is defined
                matched_messages_with_scores.append(
                    (original_message_text, msg_dict_value, score_from_thefuzz / 100.0)
                )
        matched_messages_with_scores.sort(
            key=lambda x: x[2], reverse=True
        )  # Sort by score desc
    
        if not matched_messages_with_scores:
            return f"No messages found matching '{search_term}' with a threshold of {threshold} in the last {hours} hours."
    
        chat_mapping = get_chat_mapping()
        formatted_results = []
        for _matched_text, msg_dict, score in matched_messages_with_scores:
            original_body = (
                msg_dict.get("text")
                or extract_body_from_attributed(msg_dict.get("attributedBody"))
                or "[No displayable content]"
            )
    
            apple_offset = (
                978307200  # Seconds between Unix epoch and Apple epoch (2001-01-01)
            )
            msg_timestamp_ns = int(msg_dict["date"])
            # Ensure timestamp is in seconds for fromtimestamp
            msg_timestamp_s = (
                msg_timestamp_ns / 1_000_000_000
                if len(str(msg_timestamp_ns)) > 10
                else msg_timestamp_ns
            )
            date_val = datetime.fromtimestamp(
                msg_timestamp_s + apple_offset, tz=timezone.utc
            )
            date_str = date_val.astimezone().strftime("%Y-%m-%d %H:%M:%S")
    
            direction = (
                "You" if msg_dict["is_from_me"] else get_contact_name(msg_dict["handle_id"])
            )
            group_chat_name = (
                chat_mapping.get(msg_dict.get("cache_roomnames"))
                if msg_dict.get("cache_roomnames")
                else None
            )
            message_prefix = f"[{date_str}] (Score: {score:.2f})" + (
                f" [{group_chat_name}]" if group_chat_name else ""
            )
            formatted_results.append(f"{message_prefix} {direction}: {original_body}")
    
        return (
            f"Found {len(matched_messages_with_scores)} messages matching '{search_term}':\n"
            + "\n".join(formatted_results)
        )

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/carterlasalle/mac_messages_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server