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);
        }
    }
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 analytics but doesn't specify whether this is a read-only operation, what permissions are required, how data is sourced, or any rate limits. For a tool that analyzes conversations (potentially sensitive data), this lack of transparency is a significant gap.

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 key information ('Detailed analytics for conversations with a specific person') and lists specific metrics. It avoids unnecessary words, though it could be slightly more structured by separating metrics with commas or bullet points for readability.

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 of analyzing conversations with metrics like 'relationship insights,' no annotations, and no output schema, the description is incomplete. It doesn't explain what the analytics output looks like, how data is processed, or any limitations (e.g., data availability, privacy considerations). For a tool with behavioral and output uncertainty, more context is needed.

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 (participant_name, time_expression, timezone) with clear descriptions. The description adds no additional parameter semantics beyond what's in the schema, such as examples for 'time_expression' or clarifications on 'participant_name' format. Baseline 3 is appropriate when schema does the heavy lifting.

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: 'Detailed analytics for conversations with a specific person' with specific metrics like 'speaking time, topics, interaction patterns, and relationship insights.' It uses a specific verb ('analyze') and resource ('conversations'), but doesn't explicitly differentiate from siblings like 'limitless_get_detailed_analysis' or 'limitless_search_conversations_about' 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 any prerequisites, exclusions, or compare it to sibling tools such as 'limitless_get_detailed_analysis' or 'limitless_search_conversations_about,' leaving the agent to infer usage context based solely on the tool name and description.

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