Case study
employmentGoogle Workspace MCP Server
A remote, multi-tenant MCP server with a built-in OAuth 2.1 authorization server that exposes 186 tools across 12 Google services to Claude, used by 15 people at Empire Media.
- Role
- Sole developer
- Organization
Empire Media
- Period
- Jun 2026 – Present
- Stack
- TypeScript
- Node.js
- Fastify
- Model Context Protocol
- PostgreSQL
- Google Workspace APIs
- Docker
- Railway
By Mahdi Talal Published Updated
The Google Workspace MCP Server is one remote connector that a Claude Team admin adds once; each employee then signs in and gets isolated access to their own linked Google accounts. I built it alone at Empire Media, including the OAuth 2.1 authorization server inside it. It exposes 186 tools across 12 Google services (CAPABILITIES.md, Sep 2026) and is used by 15 people (reported by me, Sep 2026).
- Tools across 12 Google services
- 186
- Source: CAPABILITIES.md in the server repository · Sep 2026
- People using it
- 15
- Source: My own count · Sep 2026
The problem
Empire Media's marketing, accounting and HR teams do much of their work in Claude, and that work lives in Google Workspace: mail, calendars, Drive files, spreadsheets, analytics. The Claude Team admin wanted to add Google Workspace once for everyone. Each person had to reach only their own data, and some people work across more than one Google account.
Later the work moved to documents: filling in Word forms, ticking checkboxes and checking whether a page had been signed. Text extraction can't answer "is this box ticked?" or "is there a signature here?", so the server also had to let the model see documents, not just read them.
Constraints
- One connector, many people. The admin adds a single URL. Isolation has to come from the server, not from a deployment per person.
- Several Google accounts per person. A user may link more than one account and choose the right one for each request.
- A remote team connector. A claude.ai connector is a remote MCP server reached over HTTP, and per-person identity needs OAuth, so a local stdio server wasn't an option.
- Google's rules. Gmail and Drive scopes are sensitive or restricted, and an unverified app is capped at 100 test users (README, Sep 2026). Every scope added later forces each linked account to consent again.
- Long-lived secrets. Google refresh tokens must never be logged or stored in plain text.
- A solo build on one service and one Postgres database, deployed on Railway.
Architecture
- A user clicks Connect on the connector in Claude
- Claude registers as a client and starts OAuth 2.1 with PKCE
- The user signs in to the server's own account system and consents
- On a dashboard, the user links one or more Google accounts as workspaces
- Claude calls list_workspaces, then a tool with a workspace_id
- The server checks that the workspace belongs to the user and loads its Google token
- The tool calls the Google API and returns structured JSON
One Fastify service and one Postgres database play four roles at once (README, Sep 2026):
- An OAuth 2.1 authorization and resource server for Claude, with dynamic client registration, PKCE (S256 only), single-use authorization codes, rotating refresh tokens and the standard metadata documents (RFC 8414, RFC 9728 and RFC 7591).
- Its own account system: email and password hashed with argon2, and signed httpOnly session cookies.
- A Google OAuth client per workspace, with refresh tokens encrypted at rest using AES-256-GCM and access tokens refreshed automatically.
- The MCP server, over Streamable HTTP.
That gives three identity layers: a person's account on the server, Claude's access token bound to that account, and one Google credential per linked workspace. The model is simple: user, then many workspaces, each one Google account.
Tenant isolation lives in one wrapper. Every tool except whoami and list_workspaces takes a required workspace_id. Tools are registered through a helper that resolves the calling user from the bearer token, checks that the workspace belongs to that user and only then builds an authenticated Google client. A tool author can't forget the ownership check, because they never write it.
Stateless transport. Each request builds a fresh MCP server and transport with no session ID, so any instance can serve any request without session affinity.
A broad, annotated tool surface. The 186 tools cover Gmail, Calendar, Tasks, Drive, Sheets, Docs, Slides, Forms, Chat, Contacts, GA4 and Search Console (CAPABILITIES.md, Sep 2026). Every tool is annotated as read-only, additive or destructive, so Claude runs reads without prompting and always asks before anything that modifies, deletes or sends. A verification script fails if a tool is missing its annotation. Bulk Drive tools run with bounded concurrency and return a per-item failure list, and list tools return page tokens so an answer is never silently truncated.
Office files and seeing pages. Models don't reliably write Office files as raw bytes; the result opens corrupt or unstyled. So the plain file tools refuse Office types outright, and every Word, Excel and PowerPoint write goes through structured tools that build or edit the document. For reading, drive_render_pages turns pages into PNG images the model can look at. Google-native files are exported to PDF, Office files go through a temporary Google copy that is deleted before the call returns, and PDFium rasterises the PDF in-process (CAPABILITIES.md, Sep 2026). Word checkboxes get their own inspect-and-update tools, bound to a hash of the document revision, so an edit against a stale version is refused.
Related MCP servers
Two personal servers reuse these patterns on a smaller scale. They share one entry, Remote MCP Servers, among all projects and case studies:
- The daily.dev server: 42 tools over the daily.dev Public API (lib/tools.ts and the live health endpoint, Sep 2026), served statelessly from a serverless deployment with no database. ChatGPT's connector dialog has no field for a bearer token, so I added a small stateless OAuth 2.1 server: the user proves they hold the server's secret on a login page, and ChatGPT receives a normal access token.
- The fal.ai server: image generation, editing and upscaling for agents. It returns permanent CDN links instead of base64, so images don't flood the model's context, and it polls slow jobs against a time limit. When the limit runs out it hands back a request ID the agent can check later, so no work is lost to a serverless timeout.
Key decisions
How the server reaches Google
| Option | How it works | Fit for this problem |
|---|---|---|
| Service account with domain-wide delegation | One credential impersonates any user in the domain | Poor: one key reaches every mailbox, and it can't cover accounts outside the domain |
| Per-user OAuth, one account per connection | Each person connects a single Google account | Partial: doesn't fit people with several accounts |
| Per-user OAuth with workspaces (chosen) | Each person links any number of accounts; every call names one | Least privilege per person; one consent covers all of a user's workspaces, present and future |
With workspaces, Claude calls list_workspaces, picks the account that matches the request and passes its ID. There's no hidden "active account" state to get out of sync.
Build the OAuth server first
My plan rated OAuth 2.1 with dynamic client registration as the fiddliest part of a remote connector, so it became phase one (PLAN.md, Jun 2026). The first milestone was a server whose only tool was whoami: add the connector in Claude, click Connect, log in, consent and call it. Google came after that worked end to end.
Wrap Google's APIs directly
I wrote the tools against the official Google client library instead of proxying to a separate MCP server per account (PLAN.md, Jun 2026). That kept one place for ownership checks, annotations and error handling, and let tools such as page rendering combine several Google APIs in one call.
Results
- 186 tools across 12 Google services (CAPABILITIES.md, Sep 2026), grown from a planned first set of about 18 (PLAN.md, Jun 2026).
- Used by 15 people at Empire Media (reported by me, Sep 2026).
- Built solo: all 38 commits in the repository are mine, from 17 June to 14 September 2026 (git log, Sep 2026).
- Offline verification scripts for Docs, Gmail, Word inspection and tool annotations; the Word and annotation checks run the real MCP schemas and handlers without touching Google or the database (repository scripts, Sep 2026).
What didn't work
- One endpoint, one very long permission list. With every tool on a single endpoint, Claude's permission panel became one long list grouped only by read and write, never by service. In September 2026 I added per-service endpoints (
/mcp/gmail,/mcp/driveand so on), each its own connector with its own OAuth metadata. It works, but it means one authorization for each connector. - Adding scopes later hurts users. Each new scope (Tasks, GA4 editing, Search Console) meant every linked account had to reconnect before those tools worked; until then they returned permission errors (README, Sep 2026). Next time I'd settle the scope set before rollout.
- Verification scripts, not a test suite. The repository has offline verification scripts but no unit-test suite with coverage. For a server that holds other people's tokens, I'd write the OAuth and isolation tests first.
- Rendering has limits I can only report. Google's import drops Word ink strokes, some symbol-font ticks come out as empty boxes and overflowing table cells are clipped (CAPABILITIES.md, Sep 2026). The tool flags these as notes, so a blank box reads as "unknown", not "unticked", but it can't fix them.
- Google verification is still ahead. The server works under the 100-test-user cap for unverified apps (README, Sep 2026). A wider rollout needs Google's app verification, and possibly a security assessment for the restricted scopes.
Stack
- Runtime: TypeScript, Node 20, Fastify
- MCP: MCP TypeScript SDK, Streamable HTTP (stateless)
- Auth: OAuth 2.1 with PKCE and dynamic client registration, argon2, AES-256-GCM
- Data: PostgreSQL
- Google: the official
googleapisclient - Documents: docx, exceljs, pptxgenjs, mammoth, pdf-parse, PDFium (WebAssembly)
- Deployment: Docker on Railway
Questions
How do you build a multi-tenant MCP server?
Put the tenant boundary in the server, not in separate deployments. Here each request's bearer token resolves to a user, every tool takes a workspace_id, and one shared wrapper checks that the workspace belongs to that user before any Google client exists. Tool code never touches credentials directly.
How do you add OAuth 2.1 to a remote MCP server for Claude?
Make the MCP server its own authorization server: publish the authorization-server and protected-resource metadata, support dynamic client registration, and require PKCE with S256. Issue access tokens bound to your own user accounts, and return a WWW-Authenticate header that points Claude to the metadata. Prove the flow with a single whoami tool before building anything else.
Can one Claude connector access several Google accounts?
Yes. In this server each person links several Google accounts as workspaces, and Claude calls list_workspaces to pick the one that matches the request. Every other tool takes that workspace's ID, so the account is explicit on every call.
How can Claude see a Word or PDF document, not just its text?
Render the pages to images and return them as image content. This server exports the file to PDF through Google, rasterises it with PDFium and sends back PNG pages the model can inspect, with optional cropping to zoom into a signature box or a checkbox.
Why not use a Google service account for an MCP server?
A service account with domain-wide delegation can act as any user in the domain, so one leaked key exposes every mailbox. It also can't reach accounts outside the domain. Per-user OAuth keeps each person's access limited to what they consented to.
Links
- How I built the Autonomous AI Coding Pipeline, which turns labelled GitHub issues into tested pull requests
- The Model Context Protocol specification and its authorization section, which remote connectors like this one implement
- The OAuth 2.1 draft at the IETF and RFC 7636 on PKCE
- RFC 8414: OAuth 2.0 Authorization Server Metadata
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- RFC 7591: OAuth 2.0 Dynamic Client Registration
- The MCP TypeScript SDK and Google's official Node.js client library, the two libraries the server is built on
- All projects and case studies