# MCP-server voor Gmail [[TOC]] ## Gmail API-specifics De Gmail API werkt anders dan de meeste andere Google API's. Berichten worden opgeslagen als MIME-berichten en bevatten een boom van `parts`. Je moet die boom recursief doorlopen om de tekstinhoud te vinden. Plan dit vooraf, want een naieve parser mist vaak de body bij multipart-berichten met bijlagen. :::info title="Scopes zo krap mogelijk" Gebruik `gmail.readonly` voor leestoegang en `gmail.send` voor verzenden. Voor het toevoegen en verwijderen van labels heb je `gmail.modify` nodig. Vraag nooit `mail.google.com` (volledig beheer) aan tenzij je echt alles moet kunnen, want dat is de meest gevoelige scope en vertraagt je OAuth-verificatie bij Google. ::: ## MIME-parser voor e-mailbodies ```typescript import { gmail_v1 } from "googleapis"; function extractBodyFromPayload(payload: gmail_v1.Schema$MessagePart): string { if (!payload) return ""; if (payload.mimeType === "text/plain" && payload.body?.data) { return Buffer.from(payload.body.data, "base64url").toString("utf-8"); } if (payload.parts) { const plainPart = payload.parts.find(p => p.mimeType === "text/plain"); if (plainPart?.body?.data) { return Buffer.from(plainPart.body.data, "base64url").toString("utf-8"); } for (const part of payload.parts) { const text = extractBodyFromPayload(part); if (text) return text; } } return ""; } ``` De Gmail API levert de body-data aan in base64url (de URL-veilige variant met `-` en `_`). Node's `Buffer` ondersteunt `"base64url"` rechtstreeks, dus je hoeft niet handmatig te vervangen. ## E-mails ophalen en lezen Vanaf de huidige MCP TypeScript SDK registreer je tools met `registerTool`. Die neemt een naam, een metadata-object met `description` en een Zod-`inputSchema`, en een handler die een `CallToolResult` teruggeeft. ```typescript server.registerTool( "search_emails", { description: "Zoek e-mails in Gmail. Ondersteunt Gmail-zoeksyntax: is:unread, from:naam, subject:onderwerp, after:2025/01/01.", inputSchema: z.object({ query: z.string().describe("Gmail-zoeksyntax"), max_results: z.number().int().min(1).max(20).default(5), include_body: z .boolean() .default(false) .describe("Voeg e-mailbody toe aan resultaat"), }), }, async ({ query, max_results, include_body }) => { const list = await gmail.users.messages.list({ userId: "me", q: query, maxResults: max_results, }); if (!list.data.messages?.length) { return { content: [{ type: "text", text: "Geen e-mails gevonden" }] }; } const messages = await Promise.all( list.data.messages.map(async (msg) => { const full = await gmail.users.messages.get({ userId: "me", id: msg.id!, format: include_body ? "full" : "metadata", metadataHeaders: ["Subject", "From", "To", "Date"], }); const headers = full.data.payload?.headers ?? []; const result: Record = { id: msg.id, threadId: full.data.threadId, subject: headers.find(h => h.name === "Subject")?.value, from: headers.find(h => h.name === "From")?.value, to: headers.find(h => h.name === "To")?.value, date: headers.find(h => h.name === "Date")?.value, snippet: full.data.snippet, labels: full.data.labelIds, }; if (include_body && full.data.payload) { result.body = extractBodyFromPayload(full.data.payload); } return result; }) ); return { content: [{ type: "text", text: JSON.stringify(messages, null, 2) }] }; } ); ``` :::tip title="Haal alleen op wat je nodig hebt" Gebruik `format: "metadata"` met `metadataHeaders` wanneer je alleen onderwerpen en afzenders nodig hebt. Een volledige `format: "full"`-aanroep kost meer quota en levert grote payloads op die je MCP-client onnodig belasten. Zet `include_body` daarom standaard op `false`. ::: ## E-mail versturen ```typescript function encodeEmail(options: { to: string; subject: string; body: string; replyToMessageId?: string; threadId?: string; }): string { const lines = [ `To: ${options.to}`, `Subject: ${options.subject}`, "Content-Type: text/plain; charset=UTF-8", "", options.body, ]; if (options.replyToMessageId) { lines.splice(2, 0, `In-Reply-To: ${options.replyToMessageId}`); } return Buffer.from(lines.join(" ")).toString("base64url"); } server.registerTool( "send_email", { description: "Verstuur een e-mail via Gmail. Gebruik reply_to_message_id om te antwoorden op een bestaande e-mail.", inputSchema: z.object({ to: z.string().email(), subject: z.string(), body: z.string(), reply_to_message_id: z.string().optional(), thread_id: z.string().optional(), }), }, async ({ to, subject, body, reply_to_message_id, thread_id }) => { const raw = encodeEmail({ to, subject, body, replyToMessageId: reply_to_message_id, threadId: thread_id, }); const res = await gmail.users.messages.send({ userId: "me", requestBody: { raw, threadId: thread_id }, }); return { content: [ { type: "text", text: `E-mail verstuurd. Message ID: ${res.data.id}` }, ], }; } ); ``` :::warn title="Versturen is onomkeerbaar" Een tool die zelfstandig e-mails verstuurt, is een gevoelige actie. Bouw een bevestigingsstap in (laat het model eerst een concept tonen) of beperk verzenden tot expliciet goedgekeurde aanvragen. Markeer de tool met de annotatie `destructiveHint: true` zodat clients dit aan de gebruiker kunnen tonen. ::: ## Labels beheren ```typescript server.registerTool( "manage_email_label", { description: "Voeg een label toe aan of verwijder het van een e-mail. Handig om e-mails te categoriseren.", inputSchema: z.object({ message_id: z.string(), add_labels: z.array(z.string()).optional(), remove_labels: z.array(z.string()).optional(), }), }, async ({ message_id, add_labels, remove_labels }) => { await gmail.users.messages.modify({ userId: "me", id: message_id, requestBody: { addLabelIds: add_labels, removeLabelIds: remove_labels, }, }); return { content: [{ type: "text", text: "Labels bijgewerkt" }] }; } ); ``` ## Thread-navigatie ```typescript server.registerTool( "get_thread", { description: "Haal een volledige e-mailthread op inclusief alle antwoorden", inputSchema: z.object({ thread_id: z.string() }), }, async ({ thread_id }) => { const res = await gmail.users.threads.get({ userId: "me", id: thread_id, format: "full", }); const messages = res.data.messages?.map(msg => { const headers = msg.payload?.headers ?? []; return { id: msg.id, from: headers.find(h => h.name === "From")?.value, date: headers.find(h => h.name === "Date")?.value, body: msg.payload ? extractBodyFromPayload(msg.payload) : "", }; }); return { content: [{ type: "text", text: JSON.stringify(messages, null, 2) }] }; } ); ``` ## Quota's in de gaten houden Google heeft de Gmail API-limieten per 1 mei 2026 omgezet naar limieten per minuut. Voor nieuwe Cloud-projecten geldt nu 6.000 quota-eenheden per gebruiker per minuut per project en 1.200.000 quota-eenheden per minuut voor het hele project. Projecten die de API tussen november 2025 en april 2026 al gebruikten, houden voorlopig hun oude limieten. Houd er rekening mee dat verschillende aanroepen verschillende aantallen eenheden kosten: een `messages.send` is duurder dan een `messages.list`. Bouw daarom van begin af aan exponential backoff in op `429`- en `403`-rateLimitExceeded-fouten, en batch gelijktijdige aanroepen met `Promise.allSettled` zodat een enkele fout niet je hele resultaat laat falen. :::faq ### Hoe verwerk ik HTML-e-mails? Zoek naar `mimeType === "text/html"` als fallback wanneer er geen `text/plain`-part is. Strip de HTML-tags voor je de tekst naar het model stuurt, zodat het model geen ruis krijgt. ### Kan ik bijlagen verwerken? Ja. Het veld `attachment.body.attachmentId` bevat een ID om de bijlage apart op te halen via `gmail.users.messages.attachments.get`. Retourneer de inhoud als base64-blob of sla hem op en geef een verwijzing terug. ### Hoe beperk ik de e-maillengte voor het model? Trunceer lange e-mailbodies tot bijvoorbeeld 2000 tekens en voeg een notitie toe in de trant van: e-mail ingekort, vraag om de volledige tekst als dat nodig is. Zo bespaar je tokens zonder context te verliezen. ### Wat zijn de huidige rate limits? Sinds 1 mei 2026 gelden limieten per minuut: 6.000 quota-eenheden per gebruiker per minuut en 1.200.000 per project per minuut voor nieuwe projecten. Oudere projecten houden hun bestaande quota. Vang `429`-fouten op met exponential backoff. ### Welke scopes heb ik minimaal nodig? Voor lezen `gmail.readonly`, voor versturen `gmail.send` en voor labels beheren `gmail.modify`. Combineer alleen de scopes die je tools daadwerkelijk gebruiken, want elke extra scope vergroot het risico en de impact bij een verkeerd gebruikte token. :::