Skip to main content
Glama

search_granola_transcripts

Search Granola meeting transcripts by query to find and retrieve relevant meeting content from the Granola MCP Server.

Instructions

Search through Granola meeting transcripts by query string. Returns matching transcripts with their content.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesSearch query to find matching transcripts
limitNoMaximum number of results to return (default: 10)

Implementation Reference

  • The handler function for the 'search_granola_transcripts' tool. It extracts query and limit from arguments, searches Granola documents using apiClient.searchDocuments, filters for meeting transcripts (type 'meeting'), converts content to Markdown using convertProseMirrorToMarkdown, truncates content, and returns a JSON-formatted response with results.
    case "search_granola_transcripts": { const query = args?.query as string; const limit = (args?.limit as number) || 10; const results = await apiClient.searchDocuments(query, limit); const transcriptResults = results .filter((doc) => doc.type === "meeting") .map((doc) => { let markdown = ""; if (doc.last_viewed_panel?.content) { markdown = convertProseMirrorToMarkdown( doc.last_viewed_panel.content ); } return { id: doc.id, meeting_id: doc.id, title: doc.title, content: markdown.substring(0, 1000) || "", }; }) .slice(0, limit); return { content: [ { type: "text", text: JSON.stringify( { query, count: transcriptResults.length, results: transcriptResults, }, null, 2 ), }, ], }; }
  • The tool schema definition including name, description, and inputSchema for 'search_granola_transcripts' (query: string required, limit: number optional).
    { name: "search_granola_transcripts", description: "Search through Granola meeting transcripts by query string. Returns matching transcripts with their content.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query to find matching transcripts", }, limit: { type: "number", description: "Maximum number of results to return (default: 10)", default: 10, }, }, required: ["query"], }, },
  • src/index.ts:152-154 (registration)
    Registers the list of tools (including search_granola_transcripts) for the ListToolsRequestSchema handler.
    server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools, }));
  • Helper method in GranolaApiClient that performs the core document search by fetching all documents and filtering client-side by query in title, markdown, or content.
    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); }
  • Helper function to convert Granola's ProseMirror JSON content structure to Markdown format, used in processing transcript and document content.
    export function convertProseMirrorToMarkdown( content: ProseMirrorNode | null | undefined ): string { if (!content || typeof content !== "object" || !content.content) { return ""; } function processNode(node: ProseMirrorNode): string { if (!node || typeof node !== "object") { return ""; } const nodeType = node.type || ""; const nodeContent = node.content || []; const text = node.text || ""; switch (nodeType) { case "heading": { const level = node.attrs?.level || 1; const headingText = nodeContent.map(processNode).join("").trim(); return `${"#".repeat(level)} ${headingText}\n\n`; } case "paragraph": { const paraText = nodeContent.map(processNode).join(""); return paraText ? `${paraText}\n\n` : "\n"; } case "bulletList": { const items: string[] = []; for (const item of nodeContent) { if (item.type === "listItem") { const itemContent = (item.content || []) .map(processNode) .join("") .trim(); if (itemContent) { items.push(`- ${itemContent}`); } } } return items.length > 0 ? items.join("\n") + "\n\n" : ""; } case "orderedList": { const items: string[] = []; for (let i = 0; i < nodeContent.length; i++) { const item = nodeContent[i]; if (item.type === "listItem") { const itemContent = (item.content || []) .map(processNode) .join("") .trim(); if (itemContent) { items.push(`${i + 1}. ${itemContent}`); } } } return items.length > 0 ? items.join("\n") + "\n\n" : ""; } case "listItem": { return nodeContent.map(processNode).join(""); } case "text": { let textContent = text; if (node.marks) { for (const mark of node.marks) { switch (mark.type) { case "bold": textContent = `**${textContent}**`; break; case "italic": textContent = `*${textContent}*`; break; case "code": textContent = `\`${textContent}\``; break; case "link": const href = mark.attrs?.href || ""; textContent = `[${textContent}](${href})`; break; } } } return textContent; } case "hardBreak": return "\n"; case "codeBlock": { const codeContent = nodeContent.map(processNode).join(""); const language = node.attrs?.language || ""; return `\`\`\`${language}\n${codeContent}\n\`\`\`\n\n`; } case "blockquote": { const quoteContent = nodeContent .map(processNode) .join("") .trim() .split("\n") .map((line) => `> ${line}`) .join("\n"); return `${quoteContent}\n\n`; } default: // For unknown node types, just process their content return nodeContent.map(processNode).join(""); } } return processNode(content).trim(); }

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