# Directory Structure ``` ├── .gitignore ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── limitless-client.ts │ └── server.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules dist .env ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Limitless MCP Server (v0.1.0)   This is an MCP (Model Context Protocol) server that connects your Limitless Pendant data to AI tools like Claude, Windsurf, and others via the [Limitless API](https://limitless.ai/developers). It lets AI chat interfaces and agents interact with your Lifelog in a structured, programmable way. Let’s build towards a more organized, intelligent future—one context-rich interaction at a time. > **What’s MCP?** > [Model Context Protocol](https://modelcontextprotocol.io/introduction) is an open standard for connecting AI models to external tools and data—think of it like the USB-C port or even the HTTP protocol for AI—universal, reliable, and designed for extensibility. The standard that everyone adopts. It enables rich integrations, custom workflows, and seamless communication between AI and the tools you use every day. **IMPORTANT NOTE:** As of March 2025, the Limitless API **requires data recorded via the Limitless Pendant**. This server depends on real data recorded from your Limitless Pendant—it won’t return anything meaningful without it. Ensure your Pendant is connected and recording. **API Status & Future Plans:** * The official Limitless API is currently in **beta**. As such, it may occasionally be unreliable, subject to change, or experience temporary outages. * Requesting large amounts of data (e.g., listing or searching hundreds of logs) may sometimes result in **timeout errors (like 504 Gateway Time-out)** due to API or network constraints. The server includes a 120-second timeout per API call to mitigate this, but very large requests might still fail. * The Limitless API is under **active development**. This MCP server will be updated with new features and improvements as they become available in the official API. * **Version 0.2.0** of this MCP server is already under development, with plans to add more robust features and potentially new tools in the near future! ## Features (v0.1.0) * **List/Get Lifelogs:** Retrieve Pendant recordings by ID, date, date range, or list recent entries. Includes control over sort direction (`asc`/`desc`). * **Search Recent Logs:** Perform simple text searches within the content of a configurable number of recent Pendant recordings (Note: only recent logs are searchable; full-history search is not supported). > With this server, you can do things like pull action items from your Lifelog and send them directly into Notion—via Claude, ChatWise, Windsurf, or any other AI assistant/app that supports MCP. ## Prerequisites * Node.js (v18 or later required) * npm or yarn * A Limitless account and API key ([Get one here](https://limitless.ai/developers)) * **A Limitless Pendant (Required for data)** * An MCP Client application (e.g., Claude, Windsurf, Cursor, ChatWise, ChatGPT (coming soon!)) capable of spawning stdio servers and passing environment variables. ## Setup 1. **Clone or download this project.** 2. **Navigate to the directory:** ```bash cd mcp-limitless-server ``` 3. **Install dependencies:** ```bash npm install ``` 4. **Build the code:** ```bash npm run build ``` ## Configuration (Client-Side) This server expects the `LIMITLESS_API_KEY` to be provided as an **environment variable** when it is launched by your MCP client. You need to add a server configuration block to your MCP client's settings file. Below are two examples depending on whether you are adding this as your first server or adding it alongside existing servers. **Example A: Adding as the first/only server** If your client's configuration file currently has an empty `mcpServers` object (`"mcpServers": {}`), replace it with this: ```json { "mcpServers": { "limitless": { "command": "node", "args": ["<FULL_FILE_PATH_TO_DIST_SERVER.js>"], "env": { "LIMITLESS_API_KEY": "<YOUR_LIMITLESS_API_KEY_HERE>" } } } } ``` **Example B: Adding to existing servers** If your `mcpServers` object already contains other servers (like `"notion": {...}`), add the `"limitless"` block alongside them, ensuring correct JSON syntax (commas between entries): ```json { "mcpServers": { "some_other_server": { "command": "...", "args": ["..."], "env": { "EXAMPLE_VAR": "value" } }, "limitless": { "command": "node", "args": ["<FULL_FILE_PATH_TO_DIST_SERVER.js>"], "env": { "LIMITLESS_API_KEY": "<YOUR_LIMITLESS_API_KEY_HERE>" } } } } ``` **Important:** * Replace `<FULL_FILE_PATH_TO_DIST_SERVER.js>` with the correct, **absolute path** to the built server script (e.g., `/Users/yourname/Documents/MCP/mcp-limitless-server/dist/server.js`). Relative paths might not work reliably depending on the client. * Replace `<YOUR_LIMITLESS_API_KEY_HERE>` with your actual Limitless API key. * MCP config files **cannot contain comments**. Remove any placeholder text like `<YOUR_LIMITLESS_API_KEY_HERE>` and replace it with your actual key. ## Running the Server (via Client) **Do not run `npm start` directly.** 1. Ensure the server is built successfully (`npm run build`). 2. Configure your MCP client as shown above. 3. Start your MCP client application. It will launch the `mcp-limitless-server` process automatically when needed. ## Exposed MCP Tools (v0.1.0) (Refer to [`src/server.ts`](./src/server.ts) or ask the server via your client for full details.) 1. **`limitless_get_lifelog_by_id`**: Retrieves a single Pendant recording by its specific ID. 2. **`limitless_list_lifelogs_by_date`**: Lists Pendant recordings for a specific date. 3. **`limitless_list_lifelogs_by_range`**: Lists Pendant recordings within a date/time range. 4. **`limitless_list_recent_lifelogs`**: Lists the most recent Pendant recordings. 5. **`limitless_search_lifelogs`**: Searches title/content of *recent* Pendant recordings (limited scope!). ## Notes & Limitations 🚫 **Pendant Required** This server depends on data generated by the Limitless Pendant. 🧪 **API Beta Status** The Limitless API is in beta and may experience occasional instability or rate limiting. Large requests might result in timeouts (e.g., 504 errors). 🔍 **Search Scope** `limitless_search_lifelogs` only scans a limited number of recent logs (default 20, max 100). It does *not* search your full history — use listing tools first for broader analysis. ⚠️ **Error Handling & Timeout** API errors are translated into MCP error results. Each API call has a 120-second timeout. 🔌 **Transport** This server uses `stdio` and is meant to be launched by an MCP-compatible client app. ## Contributing Have ideas, improvements, or feedback? Feel free to open an issue or PR—contributions are always welcome! Let’s keep pushing the boundaries of what’s possible with wearable context and intelligent tools. [https://github.com/ipvr9/mcp-limitless-server](https://github.com/ipvr9/mcp-limitless-server) ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@ipvr9/mcp-limitless-server", "version": "0.1.0", "description": "MCP Server for interacting with the Limitless API", "license": "MIT", "author": "Ryan Boyle (https://github.com/ipvr9/mcp-limitless-server)", "type": "module", "main": "dist/server.js", "scripts": { "build": "tsc", "start": "node dist/server.js", "dev": "tsx watch --clear-screen=false src/server.ts" }, "dependencies": { "@modelcontextprotocol/sdk": "1.8.0", "zod": "^3.24.2" }, "devDependencies": { "@types/node": "^22.0.0", "tsx": "^4.19.3", "typescript": "^5.8.0" }, "engines": { "node": ">=18.0.0" } } ``` -------------------------------------------------------------------------------- /src/limitless-client.ts: -------------------------------------------------------------------------------- ```typescript // Native fetch is available in Node.js >= 18 const LIMITLESS_API_URL = process.env.LIMITLESS_API_URL || "https://api.limitless.ai"; const API_TIMEOUT_MS = 120000; // 120 seconds timeout for API calls export interface LifelogParams { limit?: number; batch_size?: number; // Used internally for pagination fetching includeMarkdown?: boolean; includeHeadings?: boolean; date?: string; // YYYY-MM-DD start?: string; // YYYY-MM-DD or YYYY-MM-DD HH:mm:SS end?: string; // YYYY-MM-DD or YYYY-MM-DD HH:mm:SS timezone?: string; direction?: "asc" | "desc"; cursor?: string; } // Define LifelogContentNode - ADD export keyword export interface LifelogContentNode { type: string; content?: string; startTime?: string; endTime?: string; startOffsetMs?: number; endOffsetMs?: number; children?: LifelogContentNode[]; speakerName?: string | null; speakerIdentifier?: "user" | null; // Add other potential fields based on API spec if needed } export interface Lifelog { id: string; title?: string; markdown?: string; startTime: string; endTime: string; contents?: LifelogContentNode[]; // Use specific type // Add other fields from the API response as needed } export interface LifelogsResponse { data: { lifelogs: Lifelog[]; }; meta: { lifelogs: { nextCursor?: string; count: number; }; }; } export interface SingleLifelogResponse { data: { lifelog: Lifelog; }; } export class LimitlessApiError extends Error { constructor(message: string, public status?: number, public responseBody?: any) { super(message); this.name = "LimitlessApiError"; } } // Helper to get the system's default IANA timezone name function getDefaultTimezone(): string | undefined { try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) { // Cannot log here reliably for stdio // console.error("Could not determine default system timezone:", e); return undefined; } } async function makeApiRequest<T>(apiKey: string, endpoint: string, params: Record<string, string | number | boolean | undefined>): Promise<T> { if (!apiKey) { // This error happens before connection, so logging might be okay, but let's throw directly. throw new LimitlessApiError("Limitless API key is missing. Please set LIMITLESS_API_KEY environment variable.", 401); } const url = new URL(`${LIMITLESS_API_URL}/${endpoint}`); const queryParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined) { queryParams.set(key, String(value)); } } url.search = queryParams.toString(); const requestUrl = url.toString(); // Cannot log here reliably for stdio // console.error(`[Limitless Client] Requesting: ${requestUrl}`); let response: Response | undefined; // Uses global Response type now const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS); try { response = await fetch(requestUrl, { // Uses global fetch now headers: { "X-API-Key": apiKey, "Accept": "application/json", }, signal: controller.signal // Pass abort signal }); clearTimeout(timeoutId); // Clear timeout if fetch completes if (!response.ok) { // Read body as text first to avoid "body used already" const errorText = await response.text(); let errorBody: any; try { errorBody = JSON.parse(errorText); } catch (e) { errorBody = errorText; } // Cannot log here reliably for stdio // console.error(`[Limitless Client] Error Response ${response.status} from ${requestUrl}:`, errorBody); throw new LimitlessApiError(`Limitless API Error: ${response.status} ${response.statusText}`, response.status, errorBody); } // Only parse JSON if response is ok return await response.json() as T; } catch (error: any) { clearTimeout(timeoutId); // Clear timeout on error too if (error.name === 'AbortError') { // Cannot log here reliably for stdio // console.error(`[Limitless Client] Timeout error for ${requestUrl}`); throw new LimitlessApiError(`Limitless API request timed out after ${API_TIMEOUT_MS}ms`, 504); } // Don't log generic network errors here either, just re-throw if (error instanceof LimitlessApiError) { throw error; } throw new LimitlessApiError(`Network error calling Limitless API: ${error instanceof Error ? error.message : String(error)}`); } } export async function getLifelogs(apiKey: string, options: LifelogParams = {}): Promise<Lifelog[]> { const allLifelogs: Lifelog[] = []; let currentCursor = options.cursor; const limit = options.limit; const batchSize = 10; const defaultTimezone = getDefaultTimezone(); const originalOptions = { includeMarkdown: options.includeMarkdown ?? true, includeHeadings: options.includeHeadings ?? true, date: options.date, start: options.start, end: options.end, direction: options.direction ?? 'desc', timezone: options.timezone ?? defaultTimezone, }; // Cannot log here reliably for stdio // if (originalOptions.timezone === undefined && defaultTimezone === undefined) { ... } let page = 0; while (true) { page++; const remainingNeeded = limit !== undefined ? limit - allLifelogs.length : Infinity; if (remainingNeeded <= 0 && limit !== undefined) break; const fetchLimit = Math.min(batchSize, remainingNeeded === Infinity ? batchSize : remainingNeeded); const params: Record<string, string | number | boolean | undefined> = { limit: fetchLimit, includeMarkdown: originalOptions.includeMarkdown, includeHeadings: originalOptions.includeHeadings, date: originalOptions.date, start: originalOptions.start, end: originalOptions.end, direction: originalOptions.direction, timezone: originalOptions.timezone, cursor: currentCursor, }; if (!params.timezone) delete params.timezone; // Cannot log here reliably for stdio // console.error(`[Limitless Client] Fetching page ${page} ...`); const response = await makeApiRequest<LifelogsResponse>(apiKey, "v1/lifelogs", params); const lifelogs = response.data?.lifelogs ?? []; // Cannot log here reliably for stdio // console.error(`[Limitless Client] Received ${lifelogs.length} logs from page ${page}.`); allLifelogs.push(...lifelogs); const nextCursor = response.meta?.lifelogs?.nextCursor; if (!nextCursor || lifelogs.length < fetchLimit || (limit !== undefined && allLifelogs.length >= limit)) { break; } currentCursor = nextCursor; } return limit !== undefined ? allLifelogs.slice(0, limit) : allLifelogs; } export async function getLifelogById(apiKey: string, lifelogId: string, options: Pick<LifelogParams, 'includeMarkdown' | 'includeHeadings'> = {}): Promise<Lifelog> { // Cannot log here reliably for stdio // console.error(`[Limitless Client] Requesting lifelog by ID: ${lifelogId}`); const params: Record<string, string | number | boolean | undefined> = { includeMarkdown: options.includeMarkdown ?? true, includeHeadings: options.includeHeadings ?? true, }; const response = await makeApiRequest<SingleLifelogResponse>(apiKey, `v1/lifelogs/${lifelogId}`, params); if (!response.data?.lifelog) { throw new LimitlessApiError(`Lifelog with ID ${lifelogId} not found`, 404); } return response.data.lifelog; } ``` -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { McpError, ErrorCode, CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { getLifelogs, getLifelogById, LimitlessApiError, Lifelog, LifelogParams } from "./limitless-client.js"; import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { z } from "zod"; // --- 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)."), }; 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, }; 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, }; // --- MCP Server Setup --- const server = new McpServer({ name: "LimitlessMCP", version: "0.1.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') 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') 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 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 ` }); // --- Tool Implementations --- // Helper to handle common API call errors and format results async function handleToolApiCall<T>(apiCall: () => Promise<T>, requestedLimit?: number): Promise<CallToolResult> { 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: LifelogParams = { date: args.date, limit: args.limit, timezone: args.timezone, includeMarkdown: args.includeMarkdown, includeHeadings: args.includeHeadings, direction: args.direction ?? 'asc' }; 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: LifelogParams = { start: args.start, end: args.end, limit: args.limit, timezone: args.timezone, includeMarkdown: args.includeMarkdown, includeHeadings: args.includeHeadings, direction: args.direction ?? 'asc' }; 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: LifelogParams = { limit: args.limit, timezone: args.timezone, includeMarkdown: args.includeMarkdown, includeHeadings: args.includeHeadings, direction: 'desc' }; 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 }); 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)); } } ); // --- Server Startup --- async function main() { const transport = new StdioServerTransport(); console.error("Limitless MCP Server starting..."); server.server.onclose = () => { console.error("Connection closed."); }; server.server.onerror = (error: Error) => { console.error("MCP Server Error:", error); }; server.server.oninitialized = () => { console.error("Client initialized."); }; try { await server.server.connect(transport); } catch (error) { console.error("Failed to start server:", error); process.exit(1); } } main(); ```