Skip to main content
Glama
btn0s

Granola MCP Server

by btn0s

get_granola_document

Retrieve a specific document from Granola by its unique ID to access notes, transcripts, or meeting content.

Instructions

Get a specific Granola document by its ID.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
idYesThe document ID to retrieve

Implementation Reference

  • The main handler for the 'get_granola_document' tool. It retrieves the document by ID using the GranolaApiClient, extracts and converts the content (from last_viewed_panel or notes) to markdown using convertProseMirrorToMarkdown, and returns a JSON-formatted response with document details or an error if not found.
    case "get_granola_document": {
      const id = args?.id as string;
      const doc = await apiClient.getDocumentById(id);
      if (!doc) {
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                error: `Document with id ${id} not found`,
              }),
            },
          ],
          isError: true,
        };
      }
    
      let markdown = "";
      if (
        doc.last_viewed_panel &&
        typeof doc.last_viewed_panel === "object" &&
        doc.last_viewed_panel.content &&
        typeof doc.last_viewed_panel.content === "object" &&
        doc.last_viewed_panel.content.type === "doc"
      ) {
        markdown = convertProseMirrorToMarkdown(
          doc.last_viewed_panel.content
        );
      } else if (
        doc.notes &&
        typeof doc.notes === "object" &&
        doc.notes.type === "doc"
      ) {
        markdown = convertProseMirrorToMarkdown(doc.notes);
      }
    
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(
              {
                id: doc.id,
                title: doc.title || "Untitled",
                markdown,
                created_at: doc.created_at,
                updated_at: doc.updated_at,
                metadata: {
                  type: doc.type,
                  people: doc.people,
                  google_calendar_event: doc.google_calendar_event,
                },
              },
              null,
              2
            ),
          },
        ],
      };
    }
  • src/index.ts:108-121 (registration)
    Registration of the 'get_granola_document' tool in the tools array provided to the MCP server's ListToolsRequestHandler, including name, description, and input schema.
    {
      name: "get_granola_document",
      description: "Get a specific Granola document by its ID.",
      inputSchema: {
        type: "object",
        properties: {
          id: {
            type: "string",
            description: "The document ID to retrieve",
          },
        },
        required: ["id"],
      },
    },
  • Supporting utility method in GranolaApiClient to retrieve a specific document by ID from all fetched documents.
    async getDocumentById(id: string): Promise<GranolaDocument | null> {
      const allDocs = await this.getAllDocuments();
      return allDocs.find((doc) => doc.id === id) || null;
    }
  • Supporting utility to convert ProseMirror document structure (JSON) to Markdown format, used to extract readable content from Granola documents.
    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