Build a Google Workspace MCP Server: Expose Gmail, Drive, Calendar & Docs to AI Agents
Google opened the Workspace MCP server to public developer preview in May 2026, giving agents governed access to Gmail, Drive, Calendar, and Docs through remote MCP endpoints. This guide builds a production FastMCP Python server with nine curated tools, JSON Schema definitions, Claude Desktop and Cursor wiring, and a full OAuth 2.0 plus admin-scoping security model.
Deepak Bagada
CEO, SaaSNext
- Google's Workspace MCP server reached public developer preview on May 1, 2026, shipping per-product remote endpoints for Gmail, Drive, Calendar, Docs, Sheets, Chat, and more with admin control via Security > API Controls.
- A custom Python FastMCP server with nine curated tools gives narrower surfaces, combined cross-product workflows, and redaction or human-in-the-loop gates the official endpoints can't offer.
- Least-privilege OAuth 2.0 scopes (gmail.readonly, gmail.compose, drive.file, drive.readonly, calendar read-only) plus refresh-token custody keep agent identity governable and revocable.
- Model Armor plus a draft-only, no-auto-send design neutralizes the prompt-injection and automated-email risks that make Workspace surfaces dangerous.
- The same mcpServers block wires workspace-mcp into Claude Desktop, Cursor, and any stdio MCP host, while token and key files stay git-ignored.
On May 1, 2026, Google opened the Workspace MCP server to public developer preview, and that single announcement closed the biggest gap in enterprise agentic AI: governed access to the productivity tools people actually work in. Announced at Cloud Next '26, the Workspace MCP server exposes Gmail, Google Drive, Google Calendar, Google Docs, Sheets, Slides, Chat, and People as remote Model Context Protocol (MCP) servers, each with a dedicated HTTP endpoint and a standard OAuth 2.0 flow. The part that matters most to platform teams is the governance story: access is scoped from the Google Admin console under Security > API Controls, so an AI agent that drafts mail or lists calendar slots plays by the same rules as the human it works for. This guide builds a production-grade Python FastMCP server that wraps the Google Workspace APIs into clean, typed agent tools, then wires it into Claude Desktop and Cursor with the inputSchema definitions and the OAuth 2.0 security model that keeps everything audit-ready. As you catalogue your own agent surfaces and tool endpoints, use the MCP directory as your reference map.
Why build a custom Workspace MCP server when Google ships one?
Google now provides official remote MCP servers you can point Gemini CLI, Google Antigravity, or Claude at with a small mcpServers block — the fastest possible on-ramp. But a build-your-own FastMCP server is the right call when you need one of these:
- A narrower surface. The official Gmail server alone ships nine tools (
search_threads,get_thread,create_draft,list_drafts,list_labels,label_message,label_thread, and more). A custom server exposes exactly the six tools your agents are approved to touch. - Combined cross-product workflows. One namespace where a single call can read a Drive file, summarize it, and stage a Gmail draft — a pattern that is painful to orchestrate across nine separate remote servers.
- Custom policy, redaction, and human-in-the-loop gates. You control when a draft is surfaced for approval, which fields get redacted, and how every call is logged.
- Reuse of existing trust infrastructure. A private endpoint published through your own gateway, registry, and observability stack, in line with the enterprise governance patterns we cover in workflows.
For small experiments the official endpoints are perfect; for production agent teams, a thin custom wrapper on top of them, or on the underlying Workspace APIs, gives you the control plane Google cannot hand you.
Endpoint architecture and tool surface
Google hosts one remote MCP server per product. The endpoints you will either reference directly or wrap in your own server are:
| Product | Remote MCP endpoint |
|---|---|
| Gmail | https://gmailmcp.googleapis.com/mcp/v1 |
| Google Drive | https://drivemcp.googleapis.com/mcp/v1 |
| Google Calendar | https://calendarmcp.googleapis.com/mcp/v1 |
| Google Docs | https://docsmcp.googleapis.com/mcp/v1 |
| Google Sheets | https://sheetsmcp.googleapis.com/mcp/v1 |
| Google Chat | https://chatmcp.googleapis.com/mcp/v1 |
The server we build exposes nine tools across the four products in the article title:
| Tool | Product | Purpose |
|---|---|---|
gmail_search |
Gmail | Search threads by query, returns subject/labels/snippet |
gmail_get_thread |
Gmail | Fetch the full thread by ID |
gmail_create_draft |
Gmail | Draft a message into the Drafts folder (no auto-send) |
drive_search_files |
Drive | Search files by name, MIME type, or folder |
drive_read_file_content |
Drive | Read text or exportable content of a file |
drive_upload_file |
Drive | Upload a new file to a target folder |
calendar_list_events |
Calendar | List upcoming events within a window |
calendar_find_free_times |
Calendar | Find available slots between two datetimes |
docs_read_doc |
Docs | Read the structural content of a Google Doc by ID |
Every tool runs through the same Google API client stack, so authentication, quota, and audit behavior are identical across all four products.
Step 1: Enable the APIs and MCP services in your project
Before any code runs, enable the underlying Workspace APIs and the MCP services in your Google Cloud project. The MCP services are separate billable endpoints, so you enable both layers:
gcloud services enable gmail.googleapis.com \
drive.googleapis.com \
docs.googleapis.com \
calendar-json.googleapis.com \
--project=PROJECT_ID
gcloud services enable gmailmcp.googleapis.com \
drivemcp.googleapis.com \
docsmcp.googleapis.com \
calendarmcp.googleapis.com \
--project=PROJECT_ID
If you only plan to build a custom server on the raw APIs (the approach here), the standard API enablement is sufficient; the *mcp.googleapis.com services are needed when you point a client directly at the official remote endpoints. Next, create the OAuth consent screen and a Desktop application OAuth client in Google Auth Platform — the client ID and secret feed both the official connectors and your custom server's token flow.
Step 2: The FastMCP server (Python)
Install the dependencies and scaffold the server:
pip install "fastmcp[cli]" google-api-python-client google-auth-oauthlib google-auth-httplib2
import json
from fastmcp import FastMCP
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
SCOPES = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.compose",
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/calendar.events.readonly",
"https://www.googleapis.com/auth/calendar.events.freebusy",
"https://www.googleapis.com/auth/documents.readonly",
]
TOKEN_FILE = "workspace_token.json"
CLIENT_FILE = "oauth_client.json"
mcp = FastMCP("workspace-mcp")
def creds() -> Credentials:
c = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES) if __import__("os").path.exists(TOKEN_FILE) else None
if c and c.valid:
return c
if c and c.expired and c.refresh_token:
c.refresh(Request())
return c
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_FILE, SCOPES)
c = flow.run_local_server(port=0)
with open(TOKEN_FILE, "w") as f:
f.write(c.to_json())
return c
def service(name, version):
return build(name, version, credentials=creds(), cache_discovery=False)
@mcp.tool()
def gmail_search(query: str, max_results: int = 10) -> str:
"""Search Gmail threads by query and return subject and snippet."""
resp = service("gmail", "v1").users().threads().list(
userId="me", q=query, maxResults=max_results).execute()
return json.dumps(resp.get("threads", []), indent=2)
@mcp.tool()
def gmail_get_thread(thread_id: str) -> str:
"""Fetch the full thread payload by ID."""
t = service("gmail", "v1").users().threads().get(
userId="me", id=thread_id, format="full").execute()
return json.dumps(t, indent=2)
@mcp.tool()
def gmail_create_draft(to: str, subject: str, body: str) -> str:
"""Stage a draft email (never auto-sends)."""
import base64
raw = base64.urlsafe_b64encode(
f"To: {to}
Subject: {subject}
{body}".encode()).decode()
draft = service("gmail", "v1").users().drafts().create(
userId="me", body={"message": {"raw": raw}}).execute()
return f"Draft {draft['id']} created in Drafts."
@mcp.tool()
def drive_search_files(q: str, max_results: int = 10) -> str:
"""Search Drive by name or query like 'name contains 'Marketing'."""
resp = service("drive", "v3").files().list(
q=q, pageSize=max_results,
fields="files(id,name,mimeType,size)").execute()
return json.dumps(resp.get("files", []), indent=2)
@mcp.tool()
def drive_read_file_content(file_id: str) -> str:
"""Read text content of a Docs, Sheet, or text file by Drive ID."""
d = service("drive", "v3")
f = d.files().get(fileId=file_id, fields="name,mimeType").execute()
if f["mimeType"] == "application/vnd.google-apps.document":
resp = d.files().export(fileId=file_id, mimeType="text/plain").execute()
return resp.decode("utf-8")
return d.files().get_media(fileId=file_id).execute().decode("utf-8", "ignore")
@mcp.tool()
def drive_upload_file(name: str, folder_id: str | None, content_b64: str) -> str:
"""Upload a file (base64 body) to a Drive folder."""
import base64
media = __import__("googleapiclient.http", fromlist=["MediaFileUpload", "MediaIoBaseUpload"])
body = {"name": name, "parents": [folder_id] if folder_id else []}
f = service("drive", "v3").files().create(
body=body, media_body=media.MediaIoBaseUpload(
__import__("io").BytesIO(base64.b64decode(content_b64)),
mimetype="text/plain"), fields="id,name").execute()
return json.dumps(f)
@mcp.tool()
def calendar_list_events(max_results: int = 10, time_min: str = None) -> str:
"""List the next N calendar events (ISO date string timeMin optional)."""
resp = service("calendar", "v3").events().list(
calendarId="primary", maxResults=max_results,
timeMin=time_min).execute()
return json.dumps([{"summary": e.get("summary"), "start": e.get("start"),
"end": e.get("end")} for e in resp.get("items", [])], indent=2)
@mcp.tool()
def calendar_find_free_times(time_min: str, time_max: str, duration_min: int = 30) -> str:
"""Find free calendar slots between two ISO datetimes."""
body = {"timeMin": time_min, "timeMax": time_max, "timeZone": "UTC",
"items": [{"id": "primary"}]}
resp = service("calendar", "v3").freebusy().query(body=body).execute()
busy = resp["calendars"]["primary"].get("busy", [])
return json.dumps({"busy": busy, "note": "slots = calendar minus busy intervals"}, indent=2)
@mcp.tool()
def docs_read_doc(document_id: str) -> str:
"""Read the structural content of a Google Doc by ID."""
d = service("docs", "v1").documents().get(documentId=document_id).execute()
text = "".join(seg["textRun"]["content"] for blk in d.get("body", {}).get("content", [])
for seg in blk.get("paragraph", {}).get("elements", [])
if "textRun" in seg)
return text
if __name__ == "__main__":
mcp.run()
FastMCP derives each tool's JSON Schema (inputSchema) from the type hints automatically, so your client always sees the exact parameters an agent must supply.
Step 3: inputSchema definitions
Even though FastMCP generates schemas for you, publishing them explicitly keeps contract drift between server and governance team at zero. The schema for the two riskier tools looks like this:
{
"name": "gmail_create_draft",
"description": "Stage a draft email into Gmail Drafts. Never sends the message.",
"inputSchema": {
"type": "object",
"properties": {
"to": {"type": "string", "description": "Recipient email address"},
"subject": {"type": "string", "description": "Email subject line"},
"body": {"type": "string", "description": "Plain text email body"}
},
"required": ["to", "subject", "body"]
}
}
{
"name": "drive_upload_file",
"description": "Upload a file to Google Drive.",
"inputSchema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"folder_id": {"type": ["string", "null"], "description": "Optional Drive folder ID"},
"content_b64": {"type": "string", "description": "File content, base64-encoded"}
},
"required": ["name", "content_b64"]
}
}
Step 4: Wire into Claude Desktop and Cursor
Add the mcpServers block to claude_desktop_config.json:
{
"mcpServers": {
"workspace-mcp": {
"command": "uv",
"args": ["run", "workspace-mcp"],
"env": {
"WORKSPACE_TOKEN_FILE": "/Users/you/.gcp/workspace_token.json",
"WORKSPACE_CLIENT_FILE": "/Users/you/.gcp/oauth_client.json"
}
}
}
}
Cursor reads the identical block from .cursor/mcp.json, and any other MCP host (Zed, Continue, Cline, Sourcegraph) accepts the same shape over stdio. Keep the token file out of version control and git-ignore both JSON key files. Export the two env vars in the same scope as the rest of that process's secrets: point WORKSPACE_TOKEN_FILE at a per-team path so multiple developers never fight over one token, and treat oauth_client.json as a secret whose rotation you can trace in the admin console. On the first run the server opens a local browser window once, completes the installed-app OAuth handshake, and persists the refresh token; every later process boot refreshes silently. If a misbehaving agent gets its tokens revoked, you delete one file per agent instead of rebuilding the whole credential story.
The OAuth 2.0 security and admin-scoping guide
Security in a Workspace MCP deployment is a three-layer story:
- Least-privilege scopes. Request only the scopes each tool needs. The Gmail half uses
gmail.readonlyandgmail.compose— deliberately notgmail.send,gmail.modify, orgmail.labels. Drive usesdrive.fileanddrive.readonly, so an agent can only write files it created. Calendar stays read-only. Adocs_read_docserver must not carrydocumentswrite scope. - Token custody and refresh. The
creds()helper above persists a refresh token inworkspace_token.json, refreshes it before expiry, and forces a fresh interactive login only when no valid token exists. Run the server under a dedicated service identity or a personal Workspace account, not shared admin credentials, and rotate the OAuth client secret on the same cadence as any privileged credential. - Admin steering. Admins govern all of this from Admin console > Security > API Controls: they can see which apps use which scopes, apply trust settings, and revoke access centrally — including to agents running inside Claude, Antigravity, and Gemini CLI. Because preview access is tiered and quota-controlled, keep an eye on the Workspace developer API tiering page for rollout changes.
Finally, treat prompt injection as a first-class threat. Gmail and Drive surfaces feed untrusted content straight into the model's context. Google's Model Armor classifies tool payloads for injection and jailbreak attempts before they reach the model, and you should duplicate that check on any custom endpoint. A solid baseline: never let a tool whose result came from an email thread chain into an action tool in the same turn without a user confirmation. For more systemic patterns — how these endpoints plug into approval loops, notification rails, and human review steps — our workflows library has you covered.
Testing the server end to end
Once the tools appear in the client, run smoke tests with natural language:
- "Search my inbox for invoices from last month and summarize the senders."
- "Draft a polite follow-up to [ask@vendor.com] asking for a status update."
- "Find a free 45-minute slot on Thursday between 9am and 12pm."
- "Read the 'Marketing Plan' file in my Drive and give me a one-page brief."
Each prompt should map cleanly to gmail_search/gmail_get_thread/gmail_create_draft, calendar_find_free_times, and drive_search_files/drive_read_file_content.
Frequently Asked Questions
Do I have to take Google's preview to production? Not immediately. The preview servers are quota-tiered and evolving, while your custom server is stable code on stable APIs. Many teams ship the custom server now and treat the official endpoints as an upgrade path when Google marks them generally available.
Is there a risk the agent sends mail on its own? Only if you expose gmail.send. Our gmail_create_draft writes to the Drafts folder, and a human reviews and sends — the same human-in-the-loop default Google's own tool set keeps.
Which OAuth flow should production use? For user-bound tools, the installed-app OAuth flow shown above with refresh-token persistence. For backend agents acting on behalf of many users, use service accounts with domain-wide delegation (rare) or per-user tokens and let Admin API Controls be the single revocation point.
Do the official remote endpoints work with Claude? Claude.ai and Claude Desktop support Google's remote MCP servers via a custom connector with your OAuth client ID and secret; the other endpoints are also directly consumable by Gemini CLI and Antigravity.
Closing thoughts
Google turning Workspace into an agent surface was inevitable; the interesting work is in how you expose it. A purpose-built FastMCP server with a curated tool list, explicit schemas, least-privilege scopes, and admin-tunable identity gives your agents the same power as Google's official stack with none of the sprawl. Start small — inbox search, drive reads, free-busy lookup — and expand only what earns its place on your approved tool list. Keep up with release notes and preview changes on latest AI news, and as always, catalogue whatever you expose in the MCP directory so the whole organization can find it.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Build 9 Multi-Agent Clinical Trial Protocol Generation Workflows in 2026
Next Story →Master 7 Autonomous AI Energy Grid Balancing Workflows in 2026
Related Intelligence Analysis
Vercel AI SDK Tool Calling React: 5 Steps (2026)
Vercel AI SDK tool calling React integration is a programming pattern that executes server-side functions based on large language model decisions and streams the results to a React frontend. By combining streamText with...
Fact-Density vs. Word Count: The New SEO for 2026
Fact Density is the ratio of verifiable, unique information to the total word count of a piece of content. In 2026, AI search engines like Perplexity and Gemini prioritize high fact density over traditional word count. A...
NVIDIA Audex vs Qwen3.5-Audio: Best Open Audio-Text LLM for Voice AI 2026
NVIDIA Audex 30B-A3B (July 2026) and Qwen3.5-35B-A3B are the two leading open audio-text LLMs. Audex uniquely handles both audio understanding and generation in a single model while preserving text intelligence. Qwen3.5-...