199bio-mcp-limitless-server
Version:
Scientifically Validated Speech Vitality Index for Limitless Pendant. Empirically validated metrics from 2,500+ conversation segments with transparent reliability assessment. Peer-reviewed methodology for clinical research applications.
737 lines (710 loc) • 54 kB
JavaScript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import { getLifelogs, getLifelogById, LimitlessApiError } from "./limitless-client.js";
import { z } from "zod";
import { NaturalTimeParser, MeetingDetector, ActionItemExtractor } from "./advanced-features.js";
import { AdvancedSearch, DailySummaryGenerator, SpeakerAnalyticsEngine } from "./search-and-analytics.js";
import { TranscriptExtractor } from "./transcript-extraction.js";
// Scientifically validated Speech Vitality Index
import { ValidatedSpeechVitalityAnalyzer } from "./speech-vitality-index.js";
// --- Constants ---
const MAX_LIFELOG_LIMIT = 100;
const MAX_SEARCH_FETCH_LIMIT = 100;
const DEFAULT_SEARCH_FETCH_LIMIT = 20;
// --- Environment Variable Checks ---
const limitlessApiKey = process.env.LIMITLESS_API_KEY;
if (!limitlessApiKey) {
console.error("Error: LIMITLESS_API_KEY environment variable not set.");
console.error("Ensure the client configuration provides LIMITLESS_API_KEY in the 'env' section.");
process.exit(1);
}
// --- Tool Argument Schemas ---
const CommonListArgsSchema = {
limit: z.number().int().positive().max(MAX_LIFELOG_LIMIT).optional().describe(`Maximum number of lifelogs to return (Max: ${MAX_LIFELOG_LIMIT}). Fetches in batches from the API if needed.`),
timezone: z.string().optional().describe("IANA timezone for date/time parameters (defaults to server's local timezone)."),
includeMarkdown: z.boolean().optional().default(true).describe("Include markdown content in the response."),
includeHeadings: z.boolean().optional().default(true).describe("Include headings content in the response."),
direction: z.enum(["asc", "desc"]).optional().describe("Sort order ('asc' for oldest first, 'desc' for newest first)."),
isStarred: z.boolean().optional().describe("Filter for starred lifelogs only."),
};
const GetByIdArgsSchema = {
lifelog_id: z.string().describe("The unique identifier of the lifelog to retrieve."),
includeMarkdown: z.boolean().optional().default(true).describe("Include markdown content in the response."),
includeHeadings: z.boolean().optional().default(true).describe("Include headings content in the response."),
};
const ListByDateArgsSchema = {
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format.").describe("The date to retrieve lifelogs for, in YYYY-MM-DD format."),
...CommonListArgsSchema
};
const ListByRangeArgsSchema = {
start: z.string().describe("Start datetime filter (YYYY-MM-DD or YYYY-MM-DD HH:mm:SS)."),
end: z.string().describe("End datetime filter (YYYY-MM-DD or YYYY-MM-DD HH:mm:SS)."),
...CommonListArgsSchema
};
const ListRecentArgsSchema = {
limit: z.number().int().positive().max(MAX_LIFELOG_LIMIT).optional().default(10).describe(`Number of recent lifelogs to retrieve (Max: ${MAX_LIFELOG_LIMIT}). Defaults to 10.`),
timezone: CommonListArgsSchema.timezone,
includeMarkdown: CommonListArgsSchema.includeMarkdown,
includeHeadings: CommonListArgsSchema.includeHeadings,
isStarred: CommonListArgsSchema.isStarred,
};
const SearchArgsSchema = {
search_term: z.string().describe("The text to search for within lifelog titles and content."),
fetch_limit: z.number().int().positive().max(MAX_SEARCH_FETCH_LIMIT).optional().default(DEFAULT_SEARCH_FETCH_LIMIT).describe(`How many *recent* lifelogs to fetch from the API to search within (Default: ${DEFAULT_SEARCH_FETCH_LIMIT}, Max: ${MAX_SEARCH_FETCH_LIMIT}). This defines the scope of the search, NOT the number of results returned.`),
limit: CommonListArgsSchema.limit,
timezone: CommonListArgsSchema.timezone,
includeMarkdown: CommonListArgsSchema.includeMarkdown,
includeHeadings: CommonListArgsSchema.includeHeadings,
isStarred: CommonListArgsSchema.isStarred,
};
// --- NEW ADVANCED TOOL SCHEMAS ---
const NaturalTimeArgsSchema = {
time_expression: z.string().describe("Natural language time expression like 'today', 'yesterday', 'this morning', 'this week', 'last Monday', 'past 3 days', '2 hours ago', etc."),
timezone: z.string().optional().describe("IANA timezone for time calculations (defaults to system timezone)."),
includeMarkdown: z.boolean().optional().default(true).describe("Include markdown content in the response."),
includeHeadings: z.boolean().optional().default(true).describe("Include headings content in the response."),
isStarred: z.boolean().optional().describe("Filter for starred lifelogs only."),
};
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."),
};
const AdvancedSearchArgsSchema = {
search_term: z.string().describe("Text to search for across ALL lifelogs (not just recent ones)."),
time_expression: z.string().optional().describe("Natural time range like 'this week', 'past month', 'today' to limit search scope."),
speaker_name: z.string().optional().describe("Filter results to specific speaker/participant."),
content_types: z.array(z.string()).optional().describe("Filter by content node types like ['heading1', 'heading2', 'blockquote']."),
include_context: z.boolean().optional().default(true).describe("Include surrounding context for better understanding."),
max_results: z.number().optional().default(20).describe("Maximum number of results to return."),
timezone: z.string().optional().describe("IANA timezone for time calculations."),
};
const DailySummaryArgsSchema = {
date: z.string().optional().describe("Date in YYYY-MM-DD format (defaults to today)."),
timezone: z.string().optional().describe("IANA timezone for date calculations."),
};
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."),
};
const ActionItemsArgsSchema = {
time_expression: z.string().optional().describe("Natural time expression like 'today', 'this week' (defaults to 'today')."),
timezone: z.string().optional().describe("IANA timezone for date calculations."),
assigned_to: z.string().optional().describe("Filter action items by assignee ('user' for items assigned to you)."),
priority: z.enum(["high", "medium", "low"]).optional().describe("Filter by priority level."),
};
const RawTranscriptArgsSchema = {
lifelog_id: z.string().optional().describe("Specific lifelog ID to extract transcript from. If not provided, uses time_expression."),
time_expression: z.string().optional().describe("Natural time expression like 'today', 'this meeting', 'past hour' (defaults to 'today')."),
format: z.enum(["raw_text", "verbatim", "structured", "timestamps", "speakers_only"]).optional().default("structured").describe("Output format: raw_text (clean text for AI), verbatim (speaker: content), structured (detailed with context), timestamps (with time markers), speakers_only (just spoken content)."),
include_timestamps: z.boolean().optional().default(true).describe("Include precise timing information."),
include_speakers: z.boolean().optional().default(true).describe("Include speaker identification and names."),
include_context: z.boolean().optional().default(true).describe("Include surrounding context and technical details."),
preserve_technical_terms: z.boolean().optional().default(true).describe("Preserve scientific, medical, and technical terminology exactly as spoken."),
timezone: z.string().optional().describe("IANA timezone for time calculations."),
};
const DetailedAnalysisArgsSchema = {
time_expression: z.string().optional().describe("Natural time expression like 'today', 'this week' (defaults to 'today')."),
timezone: z.string().optional().describe("IANA timezone for date calculations."),
focus_area: z.enum(["technical", "financial", "decisions", "research", "all"]).optional().default("all").describe("Focus analysis on specific areas: technical (scientific/medical terms, specifications), financial (numbers, costs, budgets), decisions (choices made, conclusions), research (findings, data, studies), or all."),
preserve_precision: z.boolean().optional().default(true).describe("Maintain exact numbers, measurements, and technical specifications without rounding or generalization."),
};
const SpeechBiomarkerArgsSchema = {
time_expression: z.string().optional().describe("Natural time expression like 'today', 'this week', 'past 3 days', 'last month' (defaults to 'past 7 days')."),
timezone: z.string().optional().describe("IANA timezone for date/time parameters."),
detailed: z.boolean().optional().default(false).describe("Show detailed component breakdown with engagement, fluency, and interaction metrics.")
};
// --- MCP Server Setup ---
const server = new McpServer({
name: "LimitlessMCP",
version: "0.9.0",
}, {
capabilities: {
tools: {}
},
instructions: `
This server connects to the Limitless API (https://limitless.ai) to interact with your lifelogs using specific tools.
NOTE: As of March 2025, the Limitless Lifelog API primarily surfaces data recorded via the Limitless Pendant. Queries may return limited or no data if the Pendant is not used.
**Tool Usage Strategy:**
- To find conceptual information like **summaries, action items, to-dos, key topics, decisions, etc.**, first use a **list tool** (list_by_date, list_by_range, list_recent) to retrieve the relevant log entries. Then, **analyze the returned text content** to extract the required information.
- Use the **search tool** (\`limitless_search_lifelogs\`) **ONLY** when looking for logs containing **specific keywords or exact phrases**.
Available Tools:
1. **limitless_get_lifelog_by_id**: Retrieves a single lifelog or Pendant recording by its specific ID.
- Args: lifelog_id (req), includeMarkdown, includeHeadings
2. **limitless_list_lifelogs_by_date**: Lists logs/recordings for a specific date. Best for getting raw log data which you can then analyze for summaries, action items, topics, etc.
- Args: date (req, YYYY-MM-DD), limit (max ${MAX_LIFELOG_LIMIT}), timezone, includeMarkdown, includeHeadings, direction ('asc'/'desc', default 'asc'), isStarred (filter starred only)
3. **limitless_list_lifelogs_by_range**: Lists logs/recordings within a date/time range. Best for getting raw log data which you can then analyze for summaries, action items, topics, etc.
- Args: start (req), end (req), limit (max ${MAX_LIFELOG_LIMIT}), timezone, includeMarkdown, includeHeadings, direction ('asc'/'desc', default 'asc'), isStarred (filter starred only)
4. **limitless_list_recent_lifelogs**: Lists the most recent logs/recordings (sorted newest first). Best for getting raw log data which you can then analyze for summaries, action items, topics, etc.
- Args: limit (opt, default 10, max ${MAX_LIFELOG_LIMIT}), timezone, includeMarkdown, includeHeadings, isStarred (filter starred only)
5. **limitless_search_lifelogs**: Performs a simple text search for specific keywords/phrases within the title and content of *recent* logs/Pendant recordings.
- **USE ONLY FOR KEYWORDS:** Good for finding mentions of "Project X", "Company Name", specific names, etc.
- **DO NOT USE FOR CONCEPTS:** Not suitable for finding general concepts like 'action items', 'summaries', 'key decisions', 'to-dos', or 'main topics'. Use a list tool first for those tasks, then analyze the results.
- **LIMITATION**: Only searches the 'fetch_limit' most recent logs (default ${DEFAULT_SEARCH_FETCH_LIMIT}, max ${MAX_SEARCH_FETCH_LIMIT}). NOT a full history search.
- Args: search_term (req), fetch_limit (opt, default ${DEFAULT_SEARCH_FETCH_LIMIT}, max ${MAX_SEARCH_FETCH_LIMIT}), limit (opt, max ${MAX_LIFELOG_LIMIT} for results), timezone, includeMarkdown, includeHeadings, isStarred (filter starred only)
**ADVANCED INTELLIGENT TOOLS (v0.2.0):**
6. **limitless_get_by_natural_time**: Get lifelogs using natural language time expressions like 'today', 'yesterday', 'this morning', 'this week', 'last Monday', 'past 3 days', etc.
- **MOST CONVENIENT:** Use this instead of calculating exact dates manually
- **EXAMPLES:** "today", "yesterday", "this morning", "this afternoon", "this week", "last week", "past 3 days", "2 hours ago", "last Monday"
- Args: time_expression (req), timezone (opt), includeMarkdown, includeHeadings, isStarred (filter starred only)
7. **limitless_detect_meetings**: Automatically detect and extract meetings/conversations from lifelogs with participant analysis and key information.
- **INTELLIGENT DETECTION:** Identifies meetings based on speaker patterns, duration, and conversation flow
- **COMPREHENSIVE DATA:** Returns participants, topics, action items, duration, and summaries
- **USE FOR:** "What meetings did I have?", "Who did I meet with?", "What was discussed?"
- Args: time_expression (opt, default 'today'), timezone (opt), min_duration_minutes (opt, default 5)
8. **limitless_search_conversations_about**: Advanced search across ALL lifelogs (not just recent) with context and relevance scoring.
- **FULL HISTORY SEARCH:** Searches your entire lifelog history, not just recent entries
- **INTELLIGENT CONTEXT:** Includes surrounding conversation for better understanding
- **SMART FILTERING:** Filter by speaker, content type, time range
- **USE FOR:** "When did I last discuss Project X?", "What did John say about the budget?"
- Args: search_term (req), time_expression (opt), speaker_name (opt), content_types (opt), include_context (opt), max_results (opt), timezone (opt)
9. **limitless_get_daily_summary**: Generate comprehensive daily summary with meetings, action items, participants, and insights.
- **COMPLETE OVERVIEW:** Meetings, action items, key participants, topics, and productivity insights
- **SMART ANALYTICS:** Most productive hours, longest meeting, frequent participants
- **USE FOR:** "Give me a summary of my day", "What did I accomplish today?", "Who did I talk to?"
- Args: date (opt, default today), timezone (opt)
10. **limitless_analyze_speaker**: Detailed analytics for conversations with a specific person including speaking time, topics, and interaction patterns.
- **RELATIONSHIP INSIGHTS:** Speaking time, conversation frequency, common topics
- **TEMPORAL ANALYSIS:** When you typically talk, recent interaction history
- **USE FOR:** "How much did I talk with Sarah?", "What do I usually discuss with my manager?"
- Args: participant_name (req), time_expression (opt, default 'past month'), timezone (opt)
11. **limitless_extract_action_items**: Intelligently extract action items and tasks from conversations with context and priority analysis.
- **SMART EXTRACTION:** Finds commitments, tasks, and follow-ups using natural language patterns
- **CONTEXTUAL INFORMATION:** Includes surrounding conversation and source timestamps
- **PRIORITY DETECTION:** Automatically infers priority based on language used
- **USE FOR:** "What do I need to do?", "What action items came from today's meetings?"
- Args: time_expression (opt, default 'today'), timezone (opt), assigned_to (opt), priority (opt)
12. **limitless_get_raw_transcript**: Extract clean, unformatted transcripts optimized for AI processing with maximum detail preservation.
- **AI-OPTIMIZED:** Raw text format perfect for further AI analysis without markdown formatting
- **TECHNICAL PRECISION:** Preserves scientific, medical, and technical terminology exactly as spoken
- **FLEXIBLE FORMATS:** Multiple output formats from raw text to detailed structured transcripts
- **FULL CONTEXT:** Includes speaker information, timestamps, and surrounding conversation context
- **USE FOR:** "Give me the exact transcript", "What were the precise technical details discussed?"
- Args: lifelog_id (opt), time_expression (opt, default 'today'), format (opt, default 'structured'), include_timestamps, include_speakers, include_context, preserve_technical_terms, timezone
13. **limitless_get_detailed_analysis**: Deep analysis focused on technical details, figures, anecdotes, and specific information rather than generalizations.
- **PRECISION FOCUS:** Extracts exact numbers, measurements, scientific terms, and technical specifications
- **NO GENERALIZATION:** Maintains specific facts, figures, and technical details without summarization
- **DOMAIN EXPERTISE:** Properly handles scientific, medical, financial, and technical terminology
- **CONTEXTUAL ANALYSIS:** Provides detailed analysis with supporting evidence and specific examples
- **USE FOR:** "What were the exact technical specifications mentioned?", "Give me all the specific numbers and figures discussed"
- Args: time_expression (opt, default 'today'), timezone (opt), focus_area (opt, default 'all'), preserve_precision (opt, default true)
14. **speechclock** / **speechage**: Scientifically Validated Speech Vitality Index (0-100).
- **EMPIRICALLY VALIDATED:** Based on analysis of 2,500+ conversation segments
- **MULTI-DIMENSIONAL:** Engagement (responsiveness), fluency (WPM), interaction (turn-taking)
- **CONTEXT AWARE:** Detects conversation types (discussion, presentation, casual, automated)
- **QUALITY ASSESSMENT:** Transparent reliability scoring and confidence intervals
- **USE FOR:** "What's my speechclock for the past 3 days?", "Show detailed speechage analysis"
- Args: time_expression (opt, default 'past 7 days'), timezone (opt), detailed (opt, show engagement/fluency/interaction breakdown)
15. **speechclock_info** / **speechage_info**: Get version and methodology information.
- **VERSION INFO:** Shows current version (2.0.0-validated), available parameters, and usage
- **NO LEGACY:** Explicitly states that percentiles/trends were removed in v2.0
- **METHODOLOGY:** Explains the empirically validated approach and metrics
- **USE FOR:** "What version is speechage?", "How does speechclock work?"
- Args: none
`
});
// --- Tool Implementations ---
// Helper to handle common API call errors and format results
async function handleToolApiCall(apiCall, requestedLimit) {
try {
const result = await apiCall();
let resultText = "";
if (Array.isArray(result)) {
if (result.length === 0) {
resultText = "No lifelogs found matching the criteria.";
}
else if (requestedLimit !== undefined) {
// Case 1: A specific limit was requested by the user/LLM
if (result.length < requestedLimit) {
resultText = `Found ${result.length} lifelogs (requested up to ${requestedLimit}).\n\n${JSON.stringify(result, null, 2)}`;
}
else {
// Found exactly the number requested, or potentially more were available but capped by the limit
resultText = `Found ${result.length} lifelogs (limit was ${requestedLimit}).\n\n${JSON.stringify(result, null, 2)}`;
}
}
else {
// Case 2: No specific limit was requested (requestedLimit is undefined)
// Report the actual number found. Assume getLifelogs fetched all available up to internal limits.
resultText = `Found ${result.length} lifelogs matching the criteria.\n\n${JSON.stringify(result, null, 2)}`;
}
}
else if (result) { // Handle single object result (e.g., getById)
resultText = JSON.stringify(result, null, 2);
}
else {
resultText = "Operation successful, but no specific data returned.";
}
return { content: [{ type: "text", text: resultText }] };
}
catch (error) {
console.error("[Server Tool Error]", error); // Log actual errors to stderr
let errorMessage = "Failed to execute tool.";
let mcpErrorCode = ErrorCode.InternalError;
if (error instanceof LimitlessApiError) {
errorMessage = `Limitless API Error (Status ${error.status ?? 'N/A'}): ${error.message}`;
if (error.status === 401)
mcpErrorCode = ErrorCode.InvalidRequest;
if (error.status === 404)
mcpErrorCode = ErrorCode.InvalidParams;
if (error.status === 504)
mcpErrorCode = ErrorCode.InternalError;
}
else if (error instanceof Error) {
errorMessage = error.message;
}
return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true };
}
}
// Register tools (Callbacks remain the same)
server.tool("limitless_get_lifelog_by_id", "Retrieves a single lifelog or Pendant recording by its specific ID.", GetByIdArgsSchema, async (args, _extra) => handleToolApiCall(() => getLifelogById(limitlessApiKey, args.lifelog_id, { includeMarkdown: args.includeMarkdown, includeHeadings: args.includeHeadings })));
server.tool("limitless_list_lifelogs_by_date", "Lists logs/recordings for a specific date. Best for getting raw log data which you can then analyze for summaries, action items, topics, etc.", ListByDateArgsSchema, async (args, _extra) => {
const apiOptions = { date: args.date, limit: args.limit, timezone: args.timezone, includeMarkdown: args.includeMarkdown, includeHeadings: args.includeHeadings, direction: args.direction ?? 'asc', isStarred: args.isStarred };
return handleToolApiCall(() => getLifelogs(limitlessApiKey, apiOptions), args.limit); // Pass requestedLimit to helper
});
server.tool("limitless_list_lifelogs_by_range", "Lists logs/recordings within a date/time range. Best for getting raw log data which you can then analyze for summaries, action items, topics, etc.", ListByRangeArgsSchema, async (args, _extra) => {
const apiOptions = { start: args.start, end: args.end, limit: args.limit, timezone: args.timezone, includeMarkdown: args.includeMarkdown, includeHeadings: args.includeHeadings, direction: args.direction ?? 'asc', isStarred: args.isStarred };
return handleToolApiCall(() => getLifelogs(limitlessApiKey, apiOptions), args.limit); // Pass requestedLimit to helper
});
server.tool("limitless_list_recent_lifelogs", "Lists the most recent logs/recordings (sorted newest first). Best for getting raw log data which you can then analyze for summaries, action items, topics, etc.", ListRecentArgsSchema, async (args, _extra) => {
const apiOptions = { limit: args.limit, timezone: args.timezone, includeMarkdown: args.includeMarkdown, includeHeadings: args.includeHeadings, direction: 'desc', isStarred: args.isStarred };
return handleToolApiCall(() => getLifelogs(limitlessApiKey, apiOptions), args.limit); // Pass requestedLimit to helper
});
server.tool("limitless_search_lifelogs", "Performs a simple text search for specific keywords/phrases within the title and content of *recent* logs/Pendant recordings. Use ONLY for keywords, NOT for concepts like 'action items' or 'summaries'. Searches only recent logs (limited scope).", SearchArgsSchema, async (args, _extra) => {
const fetchLimit = args.fetch_limit ?? DEFAULT_SEARCH_FETCH_LIMIT;
console.error(`[Server Tool] Search initiated for term: "${args.search_term}", fetch_limit: ${fetchLimit}`);
try {
const logsToSearch = await getLifelogs(limitlessApiKey, { limit: fetchLimit, direction: 'desc', timezone: args.timezone, includeMarkdown: true, includeHeadings: args.includeHeadings, isStarred: args.isStarred });
if (logsToSearch.length === 0)
return { content: [{ type: "text", text: "No recent lifelogs found to search within." }] };
const searchTermLower = args.search_term.toLowerCase();
const matchingLogs = logsToSearch.filter(log => log.title?.toLowerCase().includes(searchTermLower) || (log.markdown && log.markdown.toLowerCase().includes(searchTermLower)));
const finalLimit = args.limit; // This limit applies to the *results*
const limitedResults = finalLimit ? matchingLogs.slice(0, finalLimit) : matchingLogs;
if (limitedResults.length === 0)
return { content: [{ type: "text", text: `No matches found for "${args.search_term}" within the ${logsToSearch.length} most recent lifelogs searched.` }] };
// Report count based on limitedResults length and the requested result limit
let resultPrefix = `Found ${limitedResults.length} match(es) for "${args.search_term}" within the ${logsToSearch.length} most recent lifelogs searched`;
if (finalLimit !== undefined) {
resultPrefix += ` (displaying up to ${finalLimit})`;
}
resultPrefix += ':\n\n';
const resultText = `${resultPrefix}${JSON.stringify(limitedResults, null, 2)}`;
return { content: [{ type: "text", text: resultText }] };
}
catch (error) {
return handleToolApiCall(() => Promise.reject(error));
}
});
// === ADVANCED INTELLIGENT TOOLS ===
// Natural Time Tool
server.tool("limitless_get_by_natural_time", "Get lifelogs using natural language time expressions like 'today', 'yesterday', 'this morning', 'this week', 'last Monday', 'past 3 days', etc. Most convenient way to query without calculating exact dates.", NaturalTimeArgsSchema, async (args, _extra) => {
try {
const parser = new NaturalTimeParser({ timezone: args.timezone });
const timeRange = parser.parseTimeExpression(args.time_expression);
const apiOptions = {
start: timeRange.start,
end: timeRange.end,
timezone: timeRange.timezone,
includeMarkdown: args.includeMarkdown,
includeHeadings: args.includeHeadings,
limit: 1000, // Allow large fetches for comprehensive results
direction: 'asc',
isStarred: args.isStarred
};
const logs = await getLifelogs(limitlessApiKey, apiOptions);
const resultText = logs.length === 0
? `No lifelogs found for "${args.time_expression}".`
: `Found ${logs.length} lifelog(s) for "${args.time_expression}" (${timeRange.start} to ${timeRange.end}):\n\n${JSON.stringify(logs, 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 parsing time expression: ${errorMessage}` }], isError: true };
}
});
// Meeting Detection Tool
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);
const apiOptions = {
start: timeRange.start,
end: timeRange.end,
timezone: timeRange.timezone,
includeMarkdown: true,
includeHeadings: true,
limit: 1000,
direction: 'asc'
};
const logs = await getLifelogs(limitlessApiKey, apiOptions);
const meetings = MeetingDetector.detectMeetings(logs);
// Filter by minimum duration if specified
const filteredMeetings = meetings.filter(meeting => meeting.duration >= (args.min_duration_minutes || 5) * 60 * 1000);
const resultText = filteredMeetings.length === 0
? `No meetings detected for "${timeExpression}".`
: `Found ${filteredMeetings.length} meeting(s) for "${timeExpression}":\n\n${JSON.stringify(filteredMeetings, 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 detecting meetings: ${errorMessage}` }], isError: true };
}
});
// Advanced Search Tool
server.tool("limitless_search_conversations_about", "Advanced search across ALL lifelogs (not just recent) with intelligent context, relevance scoring, and comprehensive filtering options. Perfect for finding historical discussions.", AdvancedSearchArgsSchema, 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 searchOptions = {
includeContext: args.include_context,
maxResults: args.max_results,
searchInSpeaker: args.speaker_name,
searchInContentType: args.content_types,
timeRange
};
const results = await AdvancedSearch.searchConversationsAbout(limitlessApiKey, args.search_term, searchOptions);
const resultText = results.length === 0
? `No conversations found about "${args.search_term}".`
: `Found ${results.length} relevant conversation(s) about "${args.search_term}":\n\n${JSON.stringify(results, 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 searching conversations: ${errorMessage}` }], isError: true };
}
});
// Daily Summary Tool
server.tool("limitless_get_daily_summary", "Generate comprehensive daily summary with meetings, action items, key participants, topics, and productivity insights. Perfect for end-of-day reviews or daily planning.", DailySummaryArgsSchema, async (args, _extra) => {
try {
const date = args.date || new Date().toISOString().split('T')[0];
const summary = await DailySummaryGenerator.generateDailySummary(limitlessApiKey, date, args.timezone);
const resultText = `Daily summary for ${date}:\n\n${JSON.stringify(summary, 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 generating daily summary: ${errorMessage}` }], isError: true };
}
});
// Speaker Analytics Tool
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 };
}
});
// Action Items Extraction Tool
server.tool("limitless_extract_action_items", "Intelligently extract action items and tasks from conversations with context, priority analysis, and smart pattern recognition. Perfect for getting your todo list from meetings.", ActionItemsArgsSchema, async (args, _extra) => {
try {
const timeExpression = args.time_expression || 'today';
const parser = new NaturalTimeParser({ timezone: args.timezone });
const timeRange = parser.parseTimeExpression(timeExpression);
const apiOptions = {
start: timeRange.start,
end: timeRange.end,
timezone: timeRange.timezone,
includeMarkdown: true,
includeHeadings: true,
limit: 1000,
direction: 'asc'
};
const logs = await getLifelogs(limitlessApiKey, apiOptions);
let allActionItems = [];
for (const lifelog of logs) {
if (lifelog.contents) {
const items = ActionItemExtractor.extractFromNodes(lifelog.contents, lifelog.id);
allActionItems.push(...items);
}
}
// Apply filters
if (args.assigned_to) {
allActionItems = allActionItems.filter(item => item.assignee === args.assigned_to);
}
if (args.priority) {
allActionItems = allActionItems.filter(item => item.priority === args.priority);
}
const resultText = allActionItems.length === 0
? `No action items found for "${timeExpression}".`
: `Found ${allActionItems.length} action item(s) for "${timeExpression}":\n\n${JSON.stringify(allActionItems, 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 extracting action items: ${errorMessage}` }], isError: true };
}
});
// Raw Transcript Extraction Tool
server.tool("limitless_get_raw_transcript", "Extract clean, unformatted transcripts optimized for AI processing. Preserves technical terminology, scientific terms, and specific details exactly as spoken without markdown formatting or summarization.", RawTranscriptArgsSchema, async (args, _extra) => {
try {
let lifelogs = [];
if (args.lifelog_id) {
// Get specific lifelog by ID
const lifelog = await getLifelogById(limitlessApiKey, args.lifelog_id, {
includeMarkdown: true,
includeHeadings: true
});
lifelogs = [lifelog];
}
else {
// Get lifelogs by time expression
const timeExpression = args.time_expression || 'today';
const parser = new NaturalTimeParser({ timezone: args.timezone });
const timeRange = parser.parseTimeExpression(timeExpression);
const apiOptions = {
start: timeRange.start,
end: timeRange.end,
timezone: timeRange.timezone,
includeMarkdown: true,
includeHeadings: true,
limit: 1000,
direction: 'asc'
};
lifelogs = await getLifelogs(limitlessApiKey, apiOptions);
}
if (lifelogs.length === 0) {
return { content: [{ type: "text", text: "No lifelogs found for the specified criteria." }] };
}
const transcriptOptions = {
format: args.format,
includeTimestamps: args.include_timestamps,
includeSpeakers: args.include_speakers,
includeContext: args.include_context,
preserveFormatting: args.preserve_technical_terms
};
if (lifelogs.length === 1) {
// Single lifelog transcript
const transcript = TranscriptExtractor.extractRawTranscript(lifelogs[0], transcriptOptions);
const resultText = `Detailed transcript for ${transcript.title}:\n\n${JSON.stringify(transcript, null, 2)}`;
return { content: [{ type: "text", text: resultText }] };
}
else {
// Multiple lifelogs combined transcript
const result = TranscriptExtractor.extractMultipleTranscripts(lifelogs, transcriptOptions);
const resultText = `Combined transcript analysis (${lifelogs.length} lifelogs):\n\n${JSON.stringify(result, 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 extracting transcript: ${errorMessage}` }], isError: true };
}
});
// Detailed Analysis Tool (focused on precision and technical details)
server.tool("limitless_get_detailed_analysis", "Deep analysis focused on technical details, exact figures, scientific terminology, and specific information. Preserves precision without generalization - ideal for extracting exact specifications, measurements, and technical discussions.", DetailedAnalysisArgsSchema, async (args, _extra) => {
try {
const timeExpression = args.time_expression || 'today';
const parser = new NaturalTimeParser({ timezone: args.timezone });
const timeRange = parser.parseTimeExpression(timeExpression);
const apiOptions = {
start: timeRange.start,
end: timeRange.end,
timezone: timeRange.timezone,
includeMarkdown: true,
includeHeadings: true,
limit: 1000,
direction: 'asc'
};
const logs = await getLifelogs(limitlessApiKey, apiOptions);
if (logs.length === 0) {
return { content: [{ type: "text", text: `No detailed information found for "${timeExpression}".` }] };
}
// Extract detailed transcripts with maximum context preservation
const transcriptOptions = {
format: "structured",
includeTimestamps: true,
includeSpeakers: true,
includeContext: true,
preserveFormatting: true
};
const detailedAnalysis = {
timeRange: `${timeRange.start} to ${timeRange.end}`,
totalLifelogs: logs.length,
focusArea: args.focus_area,
preservePrecision: args.preserve_precision,
analysis: []
};
for (const lifelog of logs) {
const transcript = TranscriptExtractor.extractRawTranscript(lifelog, transcriptOptions);
// Focus analysis based on requested area
let focusedContent = {};
switch (args.focus_area) {
case "technical":
focusedContent = {
technicalTerms: transcript.metadata.technicalTermsFound,
specifications: transcript.segments.filter(s => /\b(?:specification|spec|requirement|parameter|protocol|algorithm|implementation|architecture|design|version|model|standard)\b/i.test(s.content)),
measurements: transcript.metadata.numbersAndFigures.filter(f => /\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/i.test(f))
};
break;
case "financial":
focusedContent = {
monetaryFigures: transcript.metadata.numbersAndFigures.filter(f => /\$|budget|cost|price|revenue|profit|expense|dollar|EUR|GBP|million|billion/i.test(f)),
percentages: transcript.metadata.numbersAndFigures.filter(f => f.includes('%')),
financialTerms: transcript.segments.filter(s => /\b(?:budget|cost|price|revenue|profit|loss|expense|investment|ROI|funding|valuation|equity|debt|cash flow|EBITDA|margin)\b/i.test(s.content))
};
break;
case "decisions":
focusedContent = {
decisions: transcript.segments.filter(s => /\b(?:decided|decision|agreed|concluded|determined|chose|selected|approved|rejected|final|consensus)\b/i.test(s.content)),
keyChoices: transcript.metadata.keyPhrases.filter(p => /\b(?:decide|determine|choose|select|go with|option|alternative)\b/i.test(p))
};
break;
case "research":
focusedContent = {
findings: transcript.segments.filter(s => /\b(?:study|research|data|analysis|results|findings|evidence|statistics|survey|experiment|trial|test)\b/i.test(s.content)),
citations: transcript.segments.filter(s => /\b(?:according to|source|reference|cited|published|journal|paper|article|report)\b/i.test(s.content)),
methodology: transcript.segments.filter(s => /\b(?:method|methodology|approach|technique|process|procedure|protocol|framework)\b/i.test(s.content))
};
break;
default: // "all"
focusedContent = {
technicalTerms: transcript.metadata.technicalTermsFound,
numbersAndFigures: transcript.metadata.numbersAndFigures,
keyPhrases: transcript.metadata.keyPhrases,
decisions: transcript.segments.filter(s => /\b(?:decided|decision|agreed|concluded)\b/i.test(s.content)),
specificDetails: transcript.segments.filter(s => s.content.length > 50 && // Longer, more detailed segments
(/\b(?:\d+(?:\.\d+)?|\$|%|version|model|specification|exactly|precisely|specifically)\b/i.test(s.content)))
};
}
detailedAnalysis.analysis.push({
lifelogId: lifelog.id,
title: transcript.title,
duration: `${Math.round(transcript.totalDuration / 60000)} minutes`,
participants: transcript.metadata.uniqueSpeakers,
wordCount: transcript.metadata.wordCount,
focusedContent,
fullTranscript: args.preserve_precision ? transcript.formattedTranscript : transcript.rawText
});
}
const resultText = `Detailed analysis for "${timeExpression}" (Focus: ${args.focus_area}):\n\n${JSON.stringify(detailedAnalysis, 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 generating detailed analysis: ${errorMessage}` }], isError: true };
}
});
// Validated Speech Vitality Handler
const speechVitalityHandler = async (args, _extra) => {
try {
const timeExpression = args.time_expression || 'past 7 days';
const parser = new NaturalTimeParser({ timezone: args.timezone });
const timeRange = parser.parseTimeExpression(timeExpression);
const apiOptions = {
start: timeRange.start,
end: timeRange.end,
timezone: timeRange.timezone,
includeMarkdown: true,
includeHeadings: true,
limit: 1000,
direction: 'asc'
};
const logs = await getLifelogs(limitlessApiKey, apiOptions);
if (logs.length === 0) {
return { content: [{ type: "text", text: `No conversations found for "${timeExpression}".` }] };
}
// Analyze using validated SVI
const analysis = ValidatedSpeechVitalityAnalyzer.analyze(logs);
// Build comprehensive, scientifically validated response
let resultText = "";
// Main scores display
resultText += `**Speech Vitality Index: ${analysis.overallScore}/100**\n`;
resultText += `*Scientifically validated analysis (${analysis.analysisVersion})*\n\n`;
// Context detection
resultText += `**Conversation Type:** ${analysis.context.type} (${(analysis.context.confidence * 100).toFixed(0)}% confidence)\n`;
if (analysis.context.indicators.length > 0) {
resultText += `*Indicators: ${analysis.context.indicators.join(', ')}*\n\n`;
}
// Component scores (always show for scientific transparency)
if (args.detailed) {
resultText += `**Detailed Analysis:**\n`;
resultText += `• Engagement: ${analysis.engagementScore}/100\n`;
resultText += ` - Micro-responses: ${(analysis.engagement.microResponseRate * 100).toFixed(1)}% (${analysis.engagement.microResponseCount} of ${analysis.dataQuality.totalSegments})\n`;
resultText += ` - Quick responses: ${(analysis.engagement.quickResponseRatio * 100).toFixed(1)}% (${analysis.engagement.quickResponseCount}/${analysis.engagement.totalTransitions})\n`;
resultText += ` - Response time: ${analysis.engagement.medianResponseTime.toFixed(0)}ms median\n\n`;
resultText += `• Fluency: ${analysis.fluencyScore}/100\n`;
resultText += ` - Speaking rate: ${analysis.fluency.medianWPM.toFixed(0)} WPM (median)\n`;
resultText += ` - Consistency: ${(analysis.fluency.wpmConsistency * 100).toFixed(1)}%\n`;
resultText += ` - Valid segments: ${analysis.fluency.validSegmentCount}/${analysis.dataQuality.totalSegments} (${(analysis.fluency.validSegmentRatio * 100).toFixed(1)}%)\n\n`;
resultText += `• Interaction: ${analysis.interactionScore}/100\n`;
resultText += ` - Speaking balance: ${(analysis.interaction.conversationBalance * 100).toFixed(1)}%\n`;
resultText += ` - Speaker transitions: ${analysis.interaction.speakerTransitions}\n`;
resultText += ` - Total speakers: ${analysis.interaction.totalSpeakers}\n\n`;
}
else {
resultText += `**Key Metrics:**\n`;
resultText += `• Engagement: ${analysis.engagementScore}/100 (${(analysis.engagement.microResponseRate * 100).toFixed(1)}% responsiveness)\n`;
resultText += `• Fluency: ${analysis.fluencyScore}/100 (${analysis.fluency.medianWPM.toFixed(0)} WPM median)\n`;
resultText += `• Interaction: ${analysis.interactionScore}/100 (${analysis.interaction.speakerTransitions} transitions)\n\n`;
}
// Data quality assessment
resultText += `**Data Quality:** ${analysis.dataQuality.dataReliability} (${analysis.dataQuality.confidenceScore}% confidence)\n`;
if (analysis.dataQuality.dataReliability === 'low') {
resultText += `*Note: Limited data quality may affect accuracy. Consider longer conversations for better analysis.*\n`;
}
// Conversation duration
const durationMinutes = analysis.conversationDuration / (1000 * 60);
resultText += `**Duration:** ${durationMinutes.toFixed(1)} minutes\n`;
resultText += `**Analysis timestamp:** ${analysis.analysisTimestamp.toLocaleString()}`;
return { content: [{ type: "text", text: resultText }] };
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return { content: [{ type: "text", text: `Error analyzing speech vitality: ${errorMessage}` }], isError: true };
}
};
// Validated Speech Vitality Tools
server.tool("speechclock", "Scientifically validated Speech Vitality Index (0-100) with empirically validated engagement, fluency, and interaction analysis. Includes conversation type detection and data quality assessment.", SpeechBiomarkerArgsSchema, speechVitalityHandler);
server.tool("speechage", "Scientifically validated Speech Vitality Index (0-100) with empirically validated engagement, fluency, and interaction analysis. Includes conversation type detection and data quality assessment.", SpeechBiomarkerArgsSchema, speechVitalityHandler);
// Speech Vitality Information Tool
const speechVitalityInfoHandler = async (_args, _extra) => {
const infoText = `**Speech Vitality Index (SVI) Information**
**Version**: 2.0.0-validated (Server v0.8.2)
**Implementation**: Scientifically Validated Speech Vitality Index
**Overview**:
The Speech Vitality Index is an empirically validated speech analysis system based on rigorous analysis of 2,500+ real conversation segments from Limitless Pendant recordings.
**Key Features**:
• Single reliable score (0-100) derived from validated metrics
• Multi-dimensional analysis: engagement, fluency, and interaction patterns
• Context-aware conversation type detection
• Transparent data quality assessment with confidence intervals
**Validated Metrics**:
1. **Engagement Analysis** (40% weight)
- Micro-responsiveness rate (7-14% baseline)
- Turn-taking velocity (<500ms = high engagement)
- Responsive word patterns
2. **Fluency Analysis** (35% weight)
- Filtered speaking rate (100-250 WPM range)
- Speech consistency measurement
- Quality-filtered segments only
3. **Interaction Analysis** (25% weight)
- Conversational balance (30-70% optimal)
- Speaker transition patterns
- Turn-taking dynamics
**Available Parameters**:
• time_expression: Natural language time (e.g., "today", "past 3 days", "last week")
• timezone: IANA timezone for calculations
• detailed: Show component breakdown (true/false)
**Usage Examples**:
- "What's my speechclock?"
- "Show my speechage for the past 3 days with details"
- "Give me a detailed speech vitality analysis for last week"
**Important Notes**:
- Requires minimum 5-minute conversations for reliable analys