Skip to main content
Glama
199-mcp

Limitless MCP Server

by 199-mcp

limitless_analyze_speaker

Analyze conversations with a specific person to measure speaking time, identify discussion topics, track interaction patterns, and gain relationship insights from recorded audio.

Instructions

Detailed analytics for conversations with a specific person including speaking time, topics, interaction patterns, and relationship insights.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
participant_nameYesName of the person to analyze conversations with.
time_expressionNoTime range like 'this week', 'past month' (defaults to 'past month').
timezoneNoIANA timezone for calculations.

Implementation Reference

  • src/server.ts:824-848 (registration)
    Tool registration for 'limitless_analyze_speaker' with description, input schema, and handler function that calls SpeakerAnalyticsEngine.analyzeConversationWith
    server.tool("limitless_analyze_speaker",
        "Detailed analytics for conversations with a specific person including speaking time, topics, interaction patterns, and relationship insights.",
        SpeakerAnalyticsArgsSchema,
        async (args, _extra) => {
            try {
                let timeRange = undefined;
                if (args.time_expression) {
                    const parser = new NaturalTimeParser({ timezone: args.timezone });
                    timeRange = parser.parseTimeExpression(args.time_expression);
                }
                
                const analytics = await SpeakerAnalyticsEngine.analyzeConversationWith(
                    limitlessApiKey,
                    args.participant_name,
                    timeRange
                );
                
                const resultText = `Speaker analytics for ${args.participant_name}:\n\n${JSON.stringify(analytics, null, 2)}`;
                return { content: [{ type: "text", text: resultText }] };
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : String(error);
                return { content: [{ type: "text", text: `Error analyzing speaker: ${errorMessage}` }], isError: true };
            }
        }
    );
  • Zod schema definition for tool input arguments: participant_name (required), time_expression (optional), timezone (optional).
    const SpeakerAnalyticsArgsSchema = {
        participant_name: z.string().describe("Name of the person to analyze conversations with."),
        time_expression: z.string().optional().describe("Time range like 'this week', 'past month' (defaults to 'past month')."),
        timezone: z.string().optional().describe("IANA timezone for calculations."),
    };
  • Main handler logic in SpeakerAnalyticsEngine.analyzeConversationWith: fetches lifelogs, filters those containing the participant, computes speaking time, conversation count, topics, time distribution, and recent interactions.
     */
    static async analyzeConversationWith(
        apiKey: string,
        participantName: string,
        timeRange?: TimeRange
    ): Promise<SpeakerAnalytics> {
        
        // Fetch relevant lifelogs
        const lifelogParams: any = {
            limit: 1000,
            includeMarkdown: true,
            includeHeadings: true
        };
        
        if (timeRange) {
            lifelogParams.start = timeRange.start;
            lifelogParams.end = timeRange.end;
            lifelogParams.timezone = timeRange.timezone;
        }
        
        const allLifelogs = await getLifelogs(apiKey, lifelogParams);
        
        // Filter lifelogs that contain the participant
        const relevantLifelogs = allLifelogs.filter(lifelog =>
            lifelog.contents?.some(node => 
                node.speakerName === participantName || 
                (node.content && node.content.toLowerCase().includes(participantName.toLowerCase()))
            )
        );
        
        if (relevantLifelogs.length === 0) {
            return this.createEmptyAnalytics(participantName);
        }
        
        // Calculate metrics
        const totalSpeakingTime = this.calculateSpeakingTime(relevantLifelogs, participantName);
        const conversationCount = relevantLifelogs.length;
        const averageConversationLength = totalSpeakingTime / conversationCount;
        const topTopics = this.extractTopicsWithParticipant(relevantLifelogs, participantName);
        const timeDistribution = this.analyzeTimeDistribution(relevantLifelogs, participantName);
        const recentInteractions = this.getRecentInteractions(relevantLifelogs, participantName);
        
        return {
            participant: participantName,
            totalSpeakingTime,
            conversationCount,
            averageConversationLength,
            topTopics,
            timeDistribution,
            recentInteractions
        };
    }
  • SpeakerAnalyticsEngine class containing the core analysis logic and supporting helper methods for participant conversation analytics.
    export class SpeakerAnalyticsEngine {
        
        /**
         * Generate comprehensive analytics for conversations with a specific person
         */
        static async analyzeConversationWith(
            apiKey: string,
            participantName: string,
            timeRange?: TimeRange
        ): Promise<SpeakerAnalytics> {
            
            // Fetch relevant lifelogs
            const lifelogParams: any = {
                limit: 1000,
                includeMarkdown: true,
                includeHeadings: true
            };
            
            if (timeRange) {
                lifelogParams.start = timeRange.start;
                lifelogParams.end = timeRange.end;
                lifelogParams.timezone = timeRange.timezone;
            }
            
            const allLifelogs = await getLifelogs(apiKey, lifelogParams);
            
            // Filter lifelogs that contain the participant
            const relevantLifelogs = allLifelogs.filter(lifelog =>
                lifelog.contents?.some(node => 
                    node.speakerName === participantName || 
                    (node.content && node.content.toLowerCase().includes(participantName.toLowerCase()))
                )
            );
            
            if (relevantLifelogs.length === 0) {
                return this.createEmptyAnalytics(participantName);
            }
            
            // Calculate metrics
            const totalSpeakingTime = this.calculateSpeakingTime(relevantLifelogs, participantName);
            const conversationCount = relevantLifelogs.length;
            const averageConversationLength = totalSpeakingTime / conversationCount;
            const topTopics = this.extractTopicsWithParticipant(relevantLifelogs, participantName);
            const timeDistribution = this.analyzeTimeDistribution(relevantLifelogs, participantName);
            const recentInteractions = this.getRecentInteractions(relevantLifelogs, participantName);
            
            return {
                participant: participantName,
                totalSpeakingTime,
                conversationCount,
                averageConversationLength,
                topTopics,
                timeDistribution,
                recentInteractions
            };
        }
    
        private static createEmptyAnalytics(participantName: string): SpeakerAnalytics {
            return {
                participant: participantName,
                totalSpeakingTime: 0,
                conversationCount: 0,
                averageConversationLength: 0,
                topTopics: [],
                timeDistribution: [],
                recentInteractions: []
            };
        }
    
        private static calculateSpeakingTime(lifelogs: Lifelog[], participantName: string): number {
            let totalTime = 0;
            
            for (const lifelog of lifelogs) {
                if (!lifelog.contents) continue;
                
                for (const node of lifelog.contents) {
                    if (node.speakerName === participantName &&
                        node.startOffsetMs !== undefined &&
                        node.endOffsetMs !== undefined) {
                        totalTime += node.endOffsetMs - node.startOffsetMs;
                    }
                }
            }
            
            return totalTime;
        }
    
        private static extractTopicsWithParticipant(lifelogs: Lifelog[], participantName: string): string[] {
            const topicCounts = new Map<string, number>();
            
            for (const lifelog of lifelogs) {
                if (!lifelog.contents) continue;
                
                // Check if participant is in this lifelog
                const hasParticipant = lifelog.contents.some(node => 
                    node.speakerName === participantName
                );
                
                if (hasParticipant) {
                    // Extract topics from this lifelog
                    for (const node of lifelog.contents) {
                        if ((node.type === 'heading1' || node.type === 'heading2') && node.content) {
                            const topic = node.content.trim();
                            topicCounts.set(topic, (topicCounts.get(topic) || 0) + 1);
                        }
                    }
                }
            }
            
            return Array.from(topicCounts.entries())
                .sort((a, b) => b[1] - a[1])
                .slice(0, 10)
                .map(([topic]) => topic);
        }
    
        private static analyzeTimeDistribution(lifelogs: Lifelog[], participantName: string): Array<{hour: number, duration: number}> {
            const hourlyData = new Array(24).fill(0).map((_, hour) => ({ hour, duration: 0 }));
            
            for (const lifelog of lifelogs) {
                if (!lifelog.contents) continue;
                
                const startHour = new Date(lifelog.startTime).getHours();
                
                // Calculate duration with this participant
                let participantDuration = 0;
                for (const node of lifelog.contents) {
                    if (node.speakerName === participantName &&
                        node.startOffsetMs !== undefined &&
                        node.endOffsetMs !== undefined) {
                        participantDuration += node.endOffsetMs - node.startOffsetMs;
                    }
                }
                
                hourlyData[startHour].duration += participantDuration;
            }
            
            return hourlyData.filter(h => h.duration > 0);
        }
    
        private static getRecentInteractions(lifelogs: Lifelog[], participantName: string): Array<{date: string, duration: number, topics: string[]}> {
            const interactions: Array<{date: string, duration: number, topics: string[]}> = [];
            
            // Group by date
            const dateGroups = new Map<string, Lifelog[]>();
            for (const lifelog of lifelogs) {
                const date = lifelog.startTime.split('T')[0];
                if (!dateGroups.has(date)) {
                    dateGroups.set(date, []);
                }
                dateGroups.get(date)!.push(lifelog);
            }
            
            // Process each date
            for (const [date, dayLifelogs] of dateGroups) {
                let totalDuration = 0;
                const topics = new Set<string>();
                
                for (const lifelog of dayLifelogs) {
                    if (!lifelog.contents) continue;
                    
                    // Check for participant and extract data
                    for (const node of lifelog.contents) {
                        if (node.speakerName === participantName) {
                            if (node.startOffsetMs !== undefined && node.endOffsetMs !== undefined) {
                                totalDuration += node.endOffsetMs - node.startOffsetMs;
                            }
                        }
                        
                        if ((node.type === 'heading1' || node.type === 'heading2') && node.content) {
                            topics.add(node.content.trim());
                        }
                    }
                }
                
                if (totalDuration > 0) {
                    interactions.push({
                        date,
                        duration: totalDuration,
                        topics: Array.from(topics)
                    });
                }
            }
            
            return interactions
                .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
                .slice(0, 10);
        }
    }

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