MCP Google Workspace Server
README.md
# MCP Google Workspace Server
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that exposes **Gmail** and **Google Docs** capabilities as tools any MCP-compliant agent (Claude Desktop, Cursor, custom agents, etc.) can call.
It provides three tools:
| Tool | Description |
|------|-------------|
| `send_gmail` | Send an email through Gmail on behalf of the authenticated account. |
| `draft_gmail` | Create a Gmail draft without sending it. |
| `append_to_google_doc` | Append text to the end of an existing Google Doc (never overwrites). |
Both **stdio** and **streamable HTTP** transports are supported.
---
## Architecture
The codebase is modular so each layer is independently testable:
- **MCP layer** — `src/server.ts`, `src/tools/*`, `src/transports/*` (tool registration, validation, structured errors, transports).
- **Google service layer** — `src/google/gmailService.ts`, `src/google/docsService.ts`, `src/lib/mime.ts`.
- **Auth / config** — `src/auth/*`, `src/config.ts`.
```
send_gmail / draft_gmail / append_to_google_doc
│ (zod-validated input, JSON-Schema advertised)
▼
MCP server (stdio | streamable HTTP)
▼
Gmail / Docs services ──► Google APIs
▲
OAuth2 client (auto-refresh)
```
---
## Prerequisites
- **Node.js 18+** and npm.
- A Google account (Gmail and/or Google Workspace).
- A Google Cloud project.
---
## 1. Google Cloud Console setup
1. Go to the [Google Cloud Console](https://console.cloud.google.com/) and create (or select) a project.
2. **Enable APIs**: APIs & Services → Library → enable **Gmail API** and **Google Docs API**.
3. **OAuth consent screen**: configure it (External is fine for testing). Add your Google account under **Test users**.
4. Add the required **scopes** (least privilege):
- `https://www.googleapis.com/auth/gmail.send`
- `https://www.googleapis.com/auth/gmail.compose`
- `https://www.googleapis.com/auth/documents`
5. **Create credentials**: APIs & Services → Credentials → **Create Credentials → OAuth client ID → Desktop app**.
6. Add an authorized redirect URI that matches `GOOGLE_OAUTH_REDIRECT` (default `http://localhost:3000/oauth2callback`).
7. Copy the **Client ID** and **Client secret**.
---
## 2. Install & configure
```bash
npm install
cp .env.example .env # then edit .env
```
Fill in at least `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. See [`.env.example`](./.env.example) for every variable.
| Variable | Required | Purpose |
|----------|----------|---------|
| `GOOGLE_CLIENT_ID` | yes | OAuth client ID |
| `GOOGLE_CLIENT_SECRET` | yes | OAuth client secret |
| `GOOGLE_TOKEN_PATH` | no (default `./token.json`) | Where tokens are stored (git-ignored) |
| `GOOGLE_OAUTH_REDIRECT` | no (default `http://localhost:3000/oauth2callback`) | Consent-flow redirect URI |
| `DEFAULT_SEND_AS` | no | Default From address |
| `MCP_TRANSPORT` | no (default `stdio`) | `stdio` or `http` |
| `MCP_HTTP_HOST` / `MCP_HTTP_PORT` | no | HTTP bind address (defaults `127.0.0.1:3333`) |
| `MCP_HTTP_AUTH_TOKEN` | required for `http` | Bearer token clients must present |
| `LOG_LEVEL` | no | `debug`/`info`/`warn`/`error` |
---
## 3. Authorize (one time)
```bash
npm run auth
```
This opens the Google consent screen, captures the redirect, and stores a **refresh token** at `GOOGLE_TOKEN_PATH`. Tokens are refreshed transparently afterward. Re-run it if you change scopes or revoke access.
---
## 4. Build & run
```bash
npm run build # compile TypeScript to dist/
npm start # run the compiled server (uses MCP_TRANSPORT)
# or during development:
npm run dev
```
### stdio transport (local/desktop agents)
Set `MCP_TRANSPORT=stdio` (default). The server communicates over stdin/stdout; all logs go to stderr.
### streamable HTTP transport (remote agents)
```bash
MCP_TRANSPORT=http MCP_HTTP_AUTH_TOKEN=$(openssl rand -hex 32) npm start
```
The endpoint is `http://<host>:<port>/mcp`. Every request must include `Authorization: Bearer <MCP_HTTP_AUTH_TOKEN>`.
---
## 5. Connect an MCP client
See [`mcp-client-config.example.json`](./mcp-client-config.example.json). Example for a stdio client:
```json
{
"mcpServers": {
"google-workspace": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": {
"GOOGLE_CLIENT_ID": "your-client-id.apps.googleusercontent.com",
"GOOGLE_CLIENT_SECRET": "your-client-secret",
"GOOGLE_TOKEN_PATH": "/absolute/path/to/token.json",
"MCP_TRANSPORT": "stdio"
}
}
}
}
```
You can also inspect the server with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
```bash
npx @modelcontextprotocol/inspector node dist/index.js
```
---
## Tool reference
### `send_gmail`
Input: `to: string[]` (req), `subject: string` (req), `body: string` (req), `body_type: "text"|"html"` (default `text`), `cc?: string[]`, `bcc?: string[]`, `reply_to?: string`.
Output: `{ "message_id": string, "thread_id": string, "status": "sent" }`
### `draft_gmail`
Input: same as `send_gmail`.
Output: `{ "draft_id": string, "message_id": string, "status": "drafted" }`
### `append_to_google_doc`
Input: `document_id: string` (req; a raw ID or a full docs URL), `content: string` (req), `add_newline_before: boolean` (default `true`).
Output: `{ "document_id": string, "status": "appended" }`
---
## Authentication model & tradeoffs
This server uses **OAuth2 user-delegated** auth (Approach 1 in the problem statement):
- Each user authorizes once via the standard consent flow; the server stores and auto-refreshes the refresh token.
- Works for **any** Gmail/Workspace account with no admin setup — the best fit for an agent-agnostic server that may act for arbitrary users.
**Alternative — service account with domain-wide delegation** (not used here):
- Suited to a single Workspace domain.
- A plain service account **cannot** send Gmail as arbitrary users; it needs domain-wide delegation configured by a Workspace admin. For Docs, the target document must be shared with the service account's email.
To switch to a service account, replace `getAuthorizedClient` in `src/auth/googleAuth.ts` with a JWT client using domain-wide delegation (`subject` = impersonated user); the rest of the code is unchanged.
---
## Error handling
Tools never crash the connection; they return **structured MCP tool errors** (`isError: true` with a machine-readable `error.code`). Codes include: `INVALID_INPUT`, `DOCUMENT_NOT_FOUND`, `INSUFFICIENT_SCOPE`, `CREDENTIALS_MISSING`, `RATE_LIMITED`, `NETWORK_ERROR`, `GOOGLE_API_ERROR`. Inputs are validated against the schema before any Google API call.
---
## Security
- Least-privilege scopes only.
- Secrets loaded from env; `.env` and `token.json` are git-ignored. Never hard-coded.
- The logger redacts recipients and never logs email bodies or document contents.
- Email addresses and document IDs are validated/sanitized; MIME header injection is prevented.
- The HTTP transport requires a bearer token and binds to `127.0.0.1` by default.
---
## Testing
```bash
npm test
```
Unit tests mock the Google layer and cover each tool's success and error branches, MIME construction, and the Google→MCP error mapping.
---
## Future extensions
Rich-text Docs formatting, email attachments/inline images, Gmail read/reply, and new tools (create Doc, insert at index, Sheets/Calendar) can be added as new modules under `src/tools/` and `src/google/` without changing existing tool contracts.
TDQS
B3.4/5.0
Scored across 3 tools
Disambiguation5/5
Each tool has a unique purpose: appending to a Google Doc, drafting a Gmail, and sending a Gmail. Drafting and sending are distinct actions, so no overlap.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern in snake_case: append_to_google_doc, draft_gmail, send_gmail.
Tool Count2/5
With only 3 tools, the server is too sparse for a Google Workspace domain. It covers only two features (Docs append and Gmail draft/send) but lacks many essential operations.
Completeness2/5
The server covers only a tiny fraction of Google Workspace capabilities. Missing tools for creating/reading docs, managing labels, searching emails, etc. Significant gaps.
Maintenance
ActivityInactive
ResponsivenessNo issues