Naar inhoud
lightbulb Welkom op de nieuwe kennisbank | We hebben de docs volledig vernieuwd met meer dan 160 features. Bekijk wat nieuw isarrow_forward

MCP-server voor Gmail

Bouw een MCP-server die e-mails kan lezen, versturen, labelen en in threads kan navigeren via de Gmail API.

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

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

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.

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<string, unknown> = {
          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) }] };
  }
);
lightbulb

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

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}` },
      ],
    };
  }
);
warning

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

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

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.

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.