Skip to main content
Glama

Limitless MCP Server

by ipvr9
limitless-client.ts8.04 kB
// 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; }

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/ipvr9/mcp-limitless-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server