Skip to main content
Glama
granola-api.ts3.85 kB
import { readFileSync } from "fs"; import { join } from "path"; import { homedir } from "os"; const GRANOLA_APP_SUPPORT_PATH = join( homedir(), "Library", "Application Support", "Granola" ); export interface GranolaDocument { id: string; title?: string; content?: string; markdown?: string; created_at?: string; updated_at?: string; last_viewed_panel?: any; [key: string]: any; } export interface GranolaApiResponse { docs: GranolaDocument[]; [key: string]: any; } export class GranolaApiClient { private accessToken: string | null = null; private tokenExpiry: number = 0; private readonly apiUrl = "https://api.granola.ai/v2/get-documents"; private loadCredentials(): string | null { try { const credsPath = join(GRANOLA_APP_SUPPORT_PATH, "supabase.json"); const fileContent = readFileSync(credsPath, "utf-8"); const data = JSON.parse(fileContent); const workosTokens = JSON.parse(data.workos_tokens); const accessToken = workosTokens.access_token; const expiresIn = workosTokens.expires_in || 21600; // Default 6 hours const obtainedAt = workosTokens.obtained_at || Date.now(); this.tokenExpiry = obtainedAt + expiresIn * 1000; this.accessToken = accessToken; return accessToken; } catch (error) { console.error("Error loading Granola credentials:", error); return null; } } private getAccessToken(): string | null { if (!this.accessToken || Date.now() >= this.tokenExpiry - 5 * 60 * 1000) { return this.loadCredentials(); } return this.accessToken; } async fetchDocuments( limit: number = 100, offset: number = 0 ): Promise<GranolaDocument[]> { const token = this.getAccessToken(); if (!token) { throw new Error("Failed to load Granola credentials"); } const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json", Accept: "*/*", "User-Agent": "Granola/5.354.0", "X-Client-Version": "5.354.0", }; const body = { limit, offset, include_last_viewed_panel: true, }; try { const response = await fetch(this.apiUrl, { method: "POST", headers, body: JSON.stringify(body), }); if (!response.ok) { throw new Error( `Granola API error: ${response.status} ${response.statusText}` ); } const data = (await response.json()) as GranolaApiResponse; return data.docs || []; } catch (error) { console.error("Error fetching documents from Granola API:", error); throw error; } } async getAllDocuments(): Promise<GranolaDocument[]> { const allDocs: GranolaDocument[] = []; let offset = 0; const limit = 100; while (true) { const docs = await this.fetchDocuments(limit, offset); if (docs.length === 0) { break; } allDocs.push(...docs); offset += limit; if (offset > 10000) { break; } } return allDocs; } async searchDocuments( query: string, limit: number = 10 ): Promise<GranolaDocument[]> { const allDocs = await this.getAllDocuments(); const lowerQuery = query.toLowerCase(); return allDocs .filter((doc) => { const title = doc.title?.toLowerCase() || ""; const markdown = doc.markdown?.toLowerCase() || ""; const content = doc.content?.toLowerCase() || ""; return ( title.includes(lowerQuery) || markdown.includes(lowerQuery) || content.includes(lowerQuery) ); }) .slice(0, limit); } async getDocumentById(id: string): Promise<GranolaDocument | null> { const allDocs = await this.getAllDocuments(); return allDocs.find((doc) => doc.id === id) || null; } }

Implementation Reference

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/btn0s/granola-mcp'

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