Skip to main content
Glama
199-mcp

Limitless MCP Server

by 199-mcp

limitless_detect_meetings

Detect and analyze meetings from lifelog recordings to extract participants, topics, and action items using natural time queries.

Instructions

Automatically detect and extract meetings/conversations from lifelogs with intelligent analysis of participants, topics, action items, and key information.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
time_expressionNoNatural time expression like 'today', 'yesterday', 'this week' (defaults to 'today').
timezoneNoIANA timezone for date/time parameters.
min_duration_minutesNoMinimum duration in minutes to consider as a meeting.

Implementation Reference

  • src/server.ts:705-756 (registration)
    MCP tool registration for 'limitless_detect_meetings', including tool name, description, input schema, and handler function.
    server.tool("limitless_detect_meetings",
        "Automatically detect and extract meetings/conversations from lifelogs with intelligent analysis of participants, topics, action items, and key information.",
        MeetingDetectionArgsSchema,
        async (args, _extra) => {
            try {
                const timeExpression = args.time_expression || 'today';
                const parser = new NaturalTimeParser({ timezone: args.timezone });
                const timeRange = parser.parseTimeExpression(timeExpression);
                
                // Fetch all logs with pagination
                const allLogs: Lifelog[] = [];
                let cursor: string | undefined = undefined;
                
                while (true) {
                    const result = await getLifelogsWithPagination(limitlessApiKey, {
                        start: timeRange.start,
                        end: timeRange.end,
                        timezone: timeRange.timezone,
                        includeMarkdown: true,
                        includeHeadings: true,
                        limit: MAX_API_LIMIT,
                        direction: 'asc',
                        cursor: cursor
                    });
                    
                    allLogs.push(...result.lifelogs);
                    
                    if (!result.pagination.nextCursor || result.lifelogs.length < MAX_API_LIMIT) {
                        break;
                    }
                    cursor = result.pagination.nextCursor;
                }
                
                const meetings = MeetingDetector.detectMeetings(allLogs);
                
                // Filter by minimum duration if specified
                const filteredMeetings = meetings.filter(meeting => 
                    meeting.duration >= (args.min_duration_minutes || 5) * 60 * 1000
                );
                
                return createSafeResponse(
                    filteredMeetings,
                    filteredMeetings.length === 0
                        ? `No meetings detected for "${timeExpression}"`
                        : `Found ${filteredMeetings.length} meeting(s) for "${timeExpression}"`
                );
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : String(error);
                return { content: [{ type: "text", text: `Error detecting meetings: ${errorMessage}` }], isError: true };
            }
        }
    );
  • The primary handler function that orchestrates lifelog fetching via pagination, invokes MeetingDetector for analysis, applies duration filtering, and formats the safe response.
    async (args, _extra) => {
        try {
            const timeExpression = args.time_expression || 'today';
            const parser = new NaturalTimeParser({ timezone: args.timezone });
            const timeRange = parser.parseTimeExpression(timeExpression);
            
            // Fetch all logs with pagination
            const allLogs: Lifelog[] = [];
            let cursor: string | undefined = undefined;
            
            while (true) {
                const result = await getLifelogsWithPagination(limitlessApiKey, {
                    start: timeRange.start,
                    end: timeRange.end,
                    timezone: timeRange.timezone,
                    includeMarkdown: true,
                    includeHeadings: true,
                    limit: MAX_API_LIMIT,
                    direction: 'asc',
                    cursor: cursor
                });
                
                allLogs.push(...result.lifelogs);
                
                if (!result.pagination.nextCursor || result.lifelogs.length < MAX_API_LIMIT) {
                    break;
                }
                cursor = result.pagination.nextCursor;
            }
            
            const meetings = MeetingDetector.detectMeetings(allLogs);
            
            // Filter by minimum duration if specified
            const filteredMeetings = meetings.filter(meeting => 
                meeting.duration >= (args.min_duration_minutes || 5) * 60 * 1000
            );
            
            return createSafeResponse(
                filteredMeetings,
                filteredMeetings.length === 0
                    ? `No meetings detected for "${timeExpression}"`
                    : `Found ${filteredMeetings.length} meeting(s) for "${timeExpression}"`
            );
        } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            return { content: [{ type: "text", text: `Error detecting meetings: ${errorMessage}` }], isError: true };
        }
    }
  • Input validation schema using Zod for tool parameters: time_expression (optional), timezone (optional), min_duration_minutes (optional, default 5).
    const MeetingDetectionArgsSchema = {
        time_expression: z.string().optional().describe("Natural time expression like 'today', 'yesterday', 'this week' (defaults to 'today')."),
        timezone: z.string().optional().describe("IANA timezone for date/time parameters."),
        min_duration_minutes: z.number().optional().default(5).describe("Minimum duration in minutes to consider as a meeting."),
    };
  • Core MeetingDetector class with static detectMeetings() method that groups lifelogs by time proximity, analyzes for meeting criteria (multiple speakers, duration), extracts participants/topics/action items/decisions/summary, used by the tool handler.
    export class MeetingDetector {
        
        /**
         * Detect meetings from lifelogs using sophisticated analysis
         */
        static detectMeetings(lifelogs: Lifelog[]): Meeting[] {
            if (!lifelogs.length) return [];
            
            // Group lifelogs by continuous time periods
            const timeGroups = this.groupByTimeProximity(lifelogs);
            
            // Analyze each group for meeting characteristics
            const meetings: Meeting[] = [];
            
            for (const group of timeGroups) {
                const meeting = this.analyzePotentialMeeting(group);
                if (meeting) {
                    meetings.push(meeting);
                }
            }
            
            return meetings.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime());
        }
    
        private static groupByTimeProximity(lifelogs: Lifelog[], maxGapMinutes: number = 15): Lifelog[][] {
            if (!lifelogs.length) return [];
            
            // Sort by start time
            const sorted = [...lifelogs].sort((a, b) => 
                new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
            );
            
            const groups: Lifelog[][] = [];
            let currentGroup: Lifelog[] = [sorted[0]];
            
            for (let i = 1; i < sorted.length; i++) {
                const current = sorted[i];
                const previous = currentGroup[currentGroup.length - 1];
                
                const gap = new Date(current.startTime).getTime() - new Date(previous.endTime).getTime();
                const gapMinutes = gap / (1000 * 60);
                
                if (gapMinutes <= maxGapMinutes) {
                    currentGroup.push(current);
                } else {
                    groups.push(currentGroup);
                    currentGroup = [current];
                }
            }
            
            groups.push(currentGroup);
            return groups;
        }
    
        private static analyzePotentialMeeting(lifelogs: Lifelog[]): Meeting | null {
            if (!lifelogs.length) return null;
            
            // Extract all speakers and content nodes
            const allNodes: LifelogContentNode[] = [];
            const speakers = new Set<string>();
            let hasUserSpeaker = false;
            
            for (const lifelog of lifelogs) {
                if (lifelog.contents) {
                    allNodes.push(...lifelog.contents);
                    
                    for (const node of lifelog.contents) {
                        if (node.speakerName) {
                            speakers.add(node.speakerName);
                        }
                        if (node.speakerIdentifier === "user") {
                            hasUserSpeaker = true;
                        }
                    }
                }
            }
            
            // Meeting criteria: multiple speakers OR user interaction OR significant duration
            const duration = new Date(lifelogs[lifelogs.length - 1].endTime).getTime() - 
                            new Date(lifelogs[0].startTime).getTime();
            const durationMinutes = duration / (1000 * 60);
            
            const isMeeting = speakers.size > 1 || hasUserSpeaker || durationMinutes > 5;
            
            if (!isMeeting) return null;
            
            // Build meeting object
            const participants = this.extractParticipants(allNodes);
            const actionItems = ActionItemExtractor.extractFromNodes(allNodes, lifelogs[0].id);
            const topics = this.extractTopics(allNodes);
            const keyDecisions = this.extractDecisions(allNodes);
            
            return {
                id: `meeting_${lifelogs[0].startTime}_${lifelogs.length}`,
                title: this.generateMeetingTitle(lifelogs, participants),
                startTime: lifelogs[0].startTime,
                endTime: lifelogs[lifelogs.length - 1].endTime,
                duration,
                participants,
                mainTopics: topics,
                actionItems,
                keyDecisions,
                summary: this.generateMeetingSummary(lifelogs, participants, topics),
                lifelogIds: lifelogs.map(l => l.id)
            };
        }
    
        private static extractParticipants(nodes: LifelogContentNode[]): MeetingParticipant[] {
            const participantMap = new Map<string, {
                name: string;
                identifier?: "user" | null;
                duration: number;
                messageCount: number;
            }>();
            
            for (const node of nodes) {
                if (node.speakerName) {
                    const existing = participantMap.get(node.speakerName) || {
                        name: node.speakerName,
                        identifier: node.speakerIdentifier,
                        duration: 0,
                        messageCount: 0
                    };
                    
                    existing.messageCount++;
                    
                    if (node.startOffsetMs !== undefined && node.endOffsetMs !== undefined) {
                        existing.duration += node.endOffsetMs - node.startOffsetMs;
                    }
                    
                    participantMap.set(node.speakerName, existing);
                }
            }
            
            return Array.from(participantMap.values()).map(p => ({
                name: p.name,
                identifier: p.identifier,
                speakingDuration: p.duration,
                messageCount: p.messageCount
            }));
        }
    
        private static extractTopics(nodes: LifelogContentNode[]): string[] {
            const topics: string[] = [];
            
            for (const node of nodes) {
                if (node.type === "heading1" || node.type === "heading2") {
                    if (node.content) {
                        topics.push(node.content);
                    }
                }
            }
            
            return topics.slice(0, 5); // Top 5 topics
        }
    
        private static extractDecisions(nodes: LifelogContentNode[]): string[] {
            const decisions: string[] = [];
            const decisionKeywords = /\b(decided|agreed|concluded|determined|resolved|final decision|we will|going with)\b/i;
            
            for (const node of nodes) {
                if (node.content && decisionKeywords.test(node.content)) {
                    decisions.push(node.content);
                }
            }
            
            return decisions;
        }
    
        private static generateMeetingTitle(lifelogs: Lifelog[], participants: MeetingParticipant[]): string {
            // Use first heading if available
            for (const lifelog of lifelogs) {
                if (lifelog.title && lifelog.title.trim()) {
                    return lifelog.title;
                }
            }
            
            // Generate from participants
            if (participants.length > 1) {
                const others = participants.filter(p => p.identifier !== "user").map(p => p.name);
                if (others.length > 0) {
                    return `Meeting with ${others.slice(0, 2).join(", ")}${others.length > 2 ? ` and ${others.length - 2} others` : ""}`;
                }
            }
            
            // Fallback to time-based title
            const startTime = new Date(lifelogs[0].startTime);
            return `Meeting at ${startTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
        }
    
        private static generateMeetingSummary(lifelogs: Lifelog[], participants: MeetingParticipant[], topics: string[]): string {
            const participantNames = participants.map(p => p.name).join(", ");
            const duration = Math.round((new Date(lifelogs[lifelogs.length - 1].endTime).getTime() - 
                                       new Date(lifelogs[0].startTime).getTime()) / (1000 * 60));
            
            // Extract key technical details, numbers, and specific information
            const allNodes: LifelogContentNode[] = [];
            for (const lifelog of lifelogs) {
                if (lifelog.contents) {
                    allNodes.push(...lifelog.contents);
                }
            }
            const technicalDetails = this.extractTechnicalDetails(lifelogs);
            const numbersAndFigures = this.extractNumbersAndFigures(lifelogs);
            const keyDecisions = this.extractDecisions(allNodes);
            
            let summary = `${duration}-minute meeting with ${participantNames}.`;
            
            if (topics.length > 0) {
                summary += ` Topics covered: ${topics.join(", ")}.`;
            }
            
            if (technicalDetails.length > 0) {
                summary += ` Technical elements discussed: ${technicalDetails.slice(0, 5).join(", ")}.`;
            }
            
            if (numbersAndFigures.length > 0) {
                summary += ` Key figures mentioned: ${numbersAndFigures.slice(0, 8).join(", ")}.`;
            }
            
            if (keyDecisions.length > 0) {
                summary += ` Decisions made: ${keyDecisions.slice(0, 3).join("; ")}.`;
            }
            
            return summary;
        }
    
        private static extractTechnicalDetails(lifelogs: Lifelog[]): string[] {
            const technicalTerms = new Set<string>();
            const technicalPatterns = [
                // Scientific and medical terms
                /\b[A-Z][a-z]+(?:ine|ase|oid|gen|ide|ate|ium|sis|tion|logy|graphy|metry|scopy|therapy|diagnosis)\b/g,
                // Technical abbreviations and acronyms
                /\b[A-Z]{2,6}\b/g,
                // Software/technology terms
                /\b(?:API|SDK|REST|GraphQL|JSON|XML|HTTP|HTTPS|SQL|NoSQL|CI\/CD|DevOps|ML|AI|GPU|CPU|RAM|SSD|IoT|VR|AR|blockchain|cryptocurrency|algorithm|database|server|cloud|kubernetes|docker|microservice)\b/gi,
                // Scientific units and measurements
                /\b\d+(?:\.\d+)?\s*(?:mg|kg|ml|cm|mm|km|hz|ghz|mb|gb|tb|fps|rpm|°[CF]|pH|ppm|mol|atm|bar|pascal|joule|watt|volt|amp|ohm)\b/gi,
                // Chemical formulas
                /\b[A-Z][a-z]?\d*(?:[A-Z][a-z]?\d*)*\b/g,
                // Version numbers and model numbers
                /\bv?\d+\.\d+(?:\.\d+)*\b|\b[A-Z]+\d+[A-Z]*\d*\b/gi
            ];
    
            for (const lifelog of lifelogs) {
                if (lifelog.contents) {
                    for (const node of lifelog.contents) {
                        if (node.content) {
                            for (const pattern of technicalPatterns) {
                                const matches = node.content.match(pattern) || [];
                                matches.forEach(match => {
                                    if (match.length > 2) {
                                        technicalTerms.add(match);
                                    }
                                });
                            }
                        }
                    }
                }
            }
    
            return Array.from(technicalTerms).slice(0, 10);
        }
    
        private static extractNumbersAndFigures(lifelogs: Lifelog[]): string[] {
            const figures = new Set<string>();
            const numberPatterns = [
                // Percentages and ratios
                /\b\d+(?:\.\d+)?%\b|\b\d+:\d+\b|\b\d+\/\d+\b/g,
                // Currency amounts
                /\$\d+(?:,\d{3})*(?:\.\d{2})?\b|\b\d+(?:,\d{3})*(?:\.\d{2})?\s*(?:dollars?|USD|EUR|GBP|million|billion|thousand|k)\b/gi,
                // Large numbers with commas
                /\b\d{1,3}(?:,\d{3})+(?:\.\d+)?\b/g,
                // Time durations with specific units
                /\b\d+(?:\.\d+)?\s*(?:hours?|minutes?|seconds?|days?|weeks?|months?|years?|milliseconds?|microseconds?)\b/gi,
                // Dates with specific formats
                /\b(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},?\s+\d{4}\b/gi,
                // Scientific notation
                /\b\d+(?:\.\d+)?[eE][+-]?\d+\b/g,
                // Specific measurements
                /\b\d+(?:\.\d+)?\s*(?:x|×)\s*\d+(?:\.\d+)?\b/g
            ];
    
            for (const lifelog of lifelogs) {
                if (lifelog.contents) {
                    for (const node of lifelog.contents) {
                        if (node.content) {
                            for (const pattern of numberPatterns) {
                                const matches = node.content.match(pattern) || [];
                                matches.forEach(match => figures.add(match));
                            }
                        }
                    }
                }
            }
    
            return Array.from(figures).slice(0, 15);
        }
    }
  • Type definition for Meeting output structure returned by the detection logic.
    export interface Meeting {
        id: string;
        title: string;
        startTime: string;
        endTime: string;
        duration: number; // in milliseconds
        participants: MeetingParticipant[];
        mainTopics: string[];
        actionItems: ActionItem[];
        keyDecisions: string[];
        summary: string;
        lifelogIds: string[];
    }

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/199-mcp/mcp-limitless'

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