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[];
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. It mentions 'intelligent analysis' but doesn't specify what that entails (e.g., AI processing, accuracy, or limitations). It lacks details on output format (e.g., structured data vs. raw text), error handling, or performance characteristics (e.g., processing time). For a tool with no annotations, this is a significant gap in transparency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core functionality. It avoids redundancy and wastes no words, making it easy to parse. However, it could be slightly more structured by separating key points (e.g., detection vs. analysis), but this is minor.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (intelligent analysis tool with no annotations and no output schema), the description is incomplete. It doesn't explain what the output looks like (e.g., list of meetings with details), behavioral traits, or how it interacts with sibling tools. For a tool that performs extraction and analysis, more context is needed to guide effective use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all three parameters (time_expression, timezone, min_duration_minutes) with descriptions. The tool description adds no additional parameter semantics beyond what's in the schema, such as examples or constraints. With high schema coverage, the baseline score of 3 is appropriate, as the description doesn't compensate but doesn't need to.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Automatically detect and extract meetings/conversations from lifelogs with intelligent analysis of participants, topics, action items, and key information.' It specifies the verb ('detect and extract'), resource ('meetings/conversations from lifelogs'), and scope of analysis. However, it doesn't explicitly differentiate from sibling tools like 'limitless_search_conversations_about' or 'limitless_get_detailed_analysis', which might have overlapping functionality.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites (e.g., needing lifelogs to exist), exclusions, or comparisons to siblings like 'limitless_list_lifelogs_by_date' (which might list logs) or 'limitless_extract_action_items' (which might focus on specific aspects). Usage is implied but not explicitly stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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