Skip to main content
Glama
EoinFalconer

Granola MCP Server

by EoinFalconer

get_meeting_content

Retrieve complete meeting notes and transcripts in Markdown format using a meeting ID. Access organized meeting content for review and reference.

Instructions

Get the full notes/content for a meeting in Markdown format

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
meeting_idYesMeeting ID to retrieve content for

Implementation Reference

  • Main handler function for get_meeting_content tool. Executes the tool logic: ensures data is loaded, retrieves the document by meeting_id, extracts and converts content to Markdown, and returns the formatted result with title.
    server.addTool({
      name: 'get_meeting_content',
      description: 'Get the full notes/content for a meeting in Markdown format',
      parameters: z.object({
        meeting_id: z.string().describe('Meeting ID to retrieve content for')
      }),
      execute: async ({ meeting_id }) => {
        await ensureDataLoaded();
    
        const doc = documentsCache.get(meeting_id);
        if (!doc) {
          return `Meeting '${meeting_id}' not found`;
        }
    
        const content = extractAndConvertContent(doc);
        if (!content) {
          return `No content available for meeting '${getTitle(doc)}'`;
        }
    
        return `# ${getTitle(doc)}\n\n${content}`;
      }
    });
  • Input parameter schema for get_meeting_content tool. Defines the required 'meeting_id' string parameter using zod validation.
    parameters: z.object({
      meeting_id: z.string().describe('Meeting ID to retrieve content for')
    }),
  • Helper function extractAndConvertContent that extracts content from a GranolaDocument's last_viewed_panel and converts it from ProseMirror format to Markdown.
    function extractAndConvertContent(doc: GranolaDocument): string {
      if (!doc.last_viewed_panel?.content) {
        return '';
      }
    
      const content = doc.last_viewed_panel.content;
      if (content.type === 'doc') {
        return convertProseMirrorToMarkdown(content);
      }
    
      return '';
    }
  • Helper function getTitle that retrieves the title from a GranolaDocument, returning 'Untitled Meeting' if no title exists.
    function getTitle(doc: GranolaDocument): string {
      return doc.title || 'Untitled Meeting';
    }
  • Core conversion function convertProseMirrorToMarkdown that transforms ProseMirror JSON content into formatted Markdown, supporting headings, paragraphs, lists, code blocks, blockquotes, text formatting, and links.
    export function convertProseMirrorToMarkdown(content: ProseMirrorContent | null | undefined): string {
      if (!content || !content.content) {
        return '';
      }
    
      const markdownLines: string[] = [];
    
      function processNode(node: ProseMirrorNode): string {
        const nodeType = node.type;
        const contentItems = node.content || [];
        const text = node.text || '';
    
        if (nodeType === 'heading') {
          const level = node.attrs?.level || 1;
          const headingText = contentItems.map(child => processNode(child)).join('');
          return `${'#'.repeat(level)} ${headingText}\n\n`;
        }
    
        if (nodeType === 'paragraph') {
          const paraText = contentItems.map(child => processNode(child)).join('');
          if (paraText.trim()) {
            return `${paraText}\n\n`;
          }
          return '\n';
        }
    
        if (nodeType === 'bulletList') {
          const items: string[] = [];
          for (const item of contentItems) {
            if (item.type === 'listItem') {
              const itemContent = (item.content || [])
                .map(child => processNode(child))
                .join('');
              items.push(`- ${itemContent.trim()}`);
            }
          }
          if (items.length > 0) {
            return items.join('\n') + '\n\n';
          }
          return '';
        }
    
        if (nodeType === 'orderedList') {
          const items: string[] = [];
          const start = node.attrs?.start || 1;
          contentItems.forEach((item, idx) => {
            if (item.type === 'listItem') {
              const itemContent = (item.content || [])
                .map(child => processNode(child))
                .join('');
              items.push(`${start + idx}. ${itemContent.trim()}`);
            }
          });
          if (items.length > 0) {
            return items.join('\n') + '\n\n';
          }
          return '';
        }
    
        if (nodeType === 'codeBlock') {
          const codeText = contentItems.map(child => processNode(child)).join('');
          const language = node.attrs?.language || '';
          return `\`\`\`${language}\n${codeText}\n\`\`\`\n\n`;
        }
    
        if (nodeType === 'blockquote') {
          const quoteText = contentItems.map(child => processNode(child)).join('');
          const quotedLines = quoteText.trim().split('\n').map(line => `> ${line}`);
          return quotedLines.join('\n') + '\n\n';
        }
    
        if (nodeType === 'horizontalRule') {
          return '---\n\n';
        }
    
        if (nodeType === 'text') {
          let formattedText = text;
          const marks = node.marks || [];
    
          for (const mark of marks) {
            const markType = mark.type;
            if (markType === 'bold' || markType === 'strong') {
              formattedText = `**${formattedText}**`;
            } else if (markType === 'italic' || markType === 'em') {
              formattedText = `*${formattedText}*`;
            } else if (markType === 'code') {
              formattedText = `\`${formattedText}\``;
            } else if (markType === 'link') {
              const href = mark.attrs?.href || '';
              formattedText = `[${formattedText}](${href})`;
            } else if (markType === 'strike' || markType === 'strikethrough') {
              formattedText = `~~${formattedText}~~`;
            }
          }
    
          return formattedText;
        }
    
        // Default: process child content if available
        return contentItems.map(child => processNode(child)).join('');
      }
    
      // Process top-level content
      for (const node of content.content) {
        const result = processNode(node);
        if (result) {
          markdownLines.push(result);
        }
      }
    
      return markdownLines.join('').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/EoinFalconer/granola-mcp-server'

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