Skip to main content
Glama
get-doc-contents.ts6.75 kB
import type { OAuth2Client } from "google-auth-library"; import type { docs_v1 } from "googleapis"; import { google } from "googleapis"; import { z } from "zod"; export const tool = { name: "gdrive_get_doc_contents", description: "Get the contents of a Google Doc in different formats. Supports markdown (default, preserves structure like headings, lists, tables, formatting) or JSON (full document structure).", inputSchema: z.object({ doc_id_or_url: z .string() .describe( "Google Docs URL (e.g., https://docs.google.com/document/d/...) or document ID", ), format: z .enum(["markdown", "json"]) .default("markdown") .describe( "Output format: 'markdown' (default) for formatted text with structure, 'json' for full document structure", ), }), annotations: { readOnlyHint: true, idempotentHint: true, }, } as const; export const handler = async ( args: z.infer<typeof tool.inputSchema>, auth: OAuth2Client, ) => { const { doc_id_or_url, format } = args; try { const contents = await getDocContents(auth, doc_id_or_url, format); return { content: [ { type: "text" as const, text: contents, }, ], }; } catch (error: unknown) { if (error instanceof Error) { return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], isError: true, }; } throw error; } }; const getDocContents = async ( auth: OAuth2Client, docIdOrUrl: string, format: "markdown" | "json", ): Promise<string> => { const docId = extractDocId(docIdOrUrl); if (!docId) { throw new Error("Invalid document ID or URL"); } const docs = google.docs({ version: "v1", auth }); try { const response = await docs.documents.get({ documentId: docId, }); const doc = response.data; if (!doc.body?.content) { return format === "json" ? JSON.stringify({ content: [] }, null, 2) : ""; } if (format === "json") { return JSON.stringify(doc, null, 2); } return convertToMarkdown(doc); } catch (error: unknown) { if (error instanceof Error) { throw new Error(`Failed to get document: ${error.message}`); } throw error; } }; function convertToMarkdown(doc: docs_v1.Schema$Document): string { if (!doc.body?.content) return ""; let markdown = ""; for (const element of doc.body.content) { if (element.paragraph) { markdown += processParagraph(element.paragraph); } else if (element.table) { markdown += processTable(element.table); } else if (element.sectionBreak) { markdown += "\n---\n\n"; } } return markdown.trim(); } function processParagraph(paragraph: docs_v1.Schema$Paragraph): string { const style = paragraph.paragraphStyle?.namedStyleType; let text = ""; // Process all text elements for (const element of paragraph.elements || []) { if (element.textRun) { text += processTextRun(element.textRun); } } // Remove trailing newlines from the text content itself const trimmedText = text.replace(/\n+$/, ""); // Return empty string for empty paragraphs (preserves document spacing) if (!trimmedText) { return "\n"; } // Apply paragraph-level formatting based on style switch (style) { case "HEADING_1": return `# ${trimmedText}\n\n`; case "HEADING_2": return `## ${trimmedText}\n\n`; case "HEADING_3": return `### ${trimmedText}\n\n`; case "HEADING_4": return `#### ${trimmedText}\n\n`; case "HEADING_5": return `##### ${trimmedText}\n\n`; case "HEADING_6": return `###### ${trimmedText}\n\n`; case "TITLE": return `# ${trimmedText}\n\n`; case "SUBTITLE": return `## ${trimmedText}\n\n`; default: { // Handle lists const bullet = paragraph.bullet; if (bullet) { const nestingLevel = bullet.nestingLevel || 0; const indent = " ".repeat(nestingLevel); // Use numbered list for first level, bullets for nested const marker = nestingLevel === 0 ? "1." : "-"; return `${indent}${marker} ${trimmedText}\n`; } // Regular paragraph return `${trimmedText}\n\n`; } } } function processTextRun(textRun: docs_v1.Schema$TextRun): string { const text = textRun.content || ""; const style = textRun.textStyle; if (!style) return text; // Don't format if it's just a newline if (text === "\n") return text; let formatted = text; // Apply text formatting if (style.bold && style.italic) { formatted = `***${formatted}***`; } else if (style.bold) { formatted = `**${formatted}**`; } else if (style.italic) { formatted = `*${formatted}*`; } if (style.underline) { formatted = `__${formatted}__`; } if (style.strikethrough) { formatted = `~~${formatted}~~`; } // Handle links if (style.link?.url) { formatted = `[${formatted}](${style.link.url})`; } // Handle code (monospace font) if ( style.weightedFontFamily?.fontFamily?.includes("Courier") || style.weightedFontFamily?.fontFamily?.includes("Mono") ) { formatted = `\`${formatted}\``; } return formatted; } function processTable(table: docs_v1.Schema$Table): string { const rows = table.tableRows || []; if (rows.length === 0) return ""; let markdown = "\n"; // Process each row for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (!row) continue; const cells = row.tableCells || []; // Build row content const cellContents = cells.map((cell) => { let cellText = ""; for (const element of cell.content || []) { if (element.paragraph) { for (const textElement of element.paragraph.elements || []) { if (textElement.textRun?.content) { cellText += textElement.textRun.content .replace(/\n/g, " ") .trim(); } } } } return cellText || " "; }); // Add row markdown += `| ${cellContents.join(" | ")} |\n`; // Add header separator after first row if (i === 0) { markdown += `| ${cellContents.map(() => "---").join(" | ")} |\n`; } } markdown += "\n"; return markdown; } function extractDocId(input: string): string | null { // Try to extract document ID from URL const urlMatch = input.match(/\/document\/d\/([a-zA-Z0-9-_]+)/); if (urlMatch) { return urlMatch[1] ?? null; } // If it's not a URL, assume it's already a document ID if (/^[a-zA-Z0-9-_]+$/.test(input)) { return input; } return null; }

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/benjamine/gdrive-mcp'

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