google-workspace-admin-mcp
by micahyee415
README.md
# google-workspace-admin-mcp
> A create-only Google Workspace admin MCP server for employee onboarding — availability checks, account creation, and credentials delivery behind Google OAuth, a per-user allowlist, and keyless domain-wide delegation.
## Overview
`google-workspace-admin-mcp` exposes a deliberately narrow slice of the Google Workspace Admin SDK as Model Context Protocol (MCP) tools, so an AI onboarding agent (e.g. Claude) can provision new-hire accounts without ever holding broad admin rights. The surface is **create-only by design** — there are no delete, suspend, or update tools — and temporary passwords are generated server-side and never appear in tool results or logs.
It is built with the official `@modelcontextprotocol/sdk`, runs as a stateless HTTP service on Cloud Run, restricts access to a configurable Google Workspace domain plus a per-user allowlist, and reaches the Admin SDK through keyless domain-wide delegation (no service-account key ever touches disk).
## MCP Tools
| Tool | Description |
|------|-------------|
| `check_email_availability` | Verifies a proposed address is free in **both** Google Workspace **and** the full Slack member history (including deactivated members). Fails closed: no definitive Slack answer, no availability verdict. |
| `create_user` | Creates the Workspace account in the configured organizational unit with a server-side temporary password. Refuses any address that has ever been used, per the never-reuse rule. Capped at 3 creations per day. |
| `send_credentials_email` | Resets the account to a fresh temporary password and emails the credentials to the hire's personal address ahead of day one. |
### The never-reuse rule
Google Workspace frees a deleted address after ~20 days, but Slack retains deactivated members forever — which makes Slack the durable source of truth for *"has this address ever been used?"*. `src/slackCheck.ts` calls `users.lookupByEmail` with a bot token (`users:read.email` scope):
- `ok: true` with `user.deleted: true` → the address was used before (burned)
- `users_not_found` → genuinely free
**Fail closed:** a missing token, missing scope, rate limit, or any HTTP error throws — availability is never vouched for without a successful Slack check.
## Architecture
```
MCP client (e.g. Claude)
│ HTTPS + Bearer token (Google OAuth)
▼
Cloud Run service (google-workspace-admin-mcp)
┌─────────────────────────────────────────────┐
│ Express HTTP server │
│ ├── /health — health check │
│ ├── /.well-known/... — OAuth discovery │
│ ├── /register — dynamic client │
│ │ registration │
│ └── /mcp — MCP endpoint │
│ │ │
│ ├── verifyGoogleToken() │
│ │ • @ALLOWED_DOMAIN enforcement │
│ │ • ALLOWED_USERS allowlist │
│ │ │
│ ├── Zod input validation (schemas.ts) │
│ ├── Guards: target OU, daily create cap│
│ └── Audit log on every tool call │
└─────────────────────────────────────────────┘
│ keyless DWD (IAM Credentials signJwt)
▼
Google Admin SDK + Gmail API Slack Web API
(create user, send credentials) (never-reuse check)
```
**Key design decisions:**
- **Create-only surface** — the server cannot delete, suspend, or modify existing accounts. The blast radius of a compromised client is limited to creating (capped, audited) accounts.
- **Keyless domain-wide delegation** — the Cloud Run service account self-signs the delegation JWT via the IAM Credentials `signJwt` API. No service-account key exists on disk or in the container image. The connecting user's OAuth token only proves identity; it never touches Workspace APIs.
- **Server-side temporary passwords** — generated in `src/tempPassword.ts` and delivered only via the credentials email. They never appear in MCP responses, logs, or error messages.
- **Fail-closed availability** — the Slack never-reuse check throws on any non-definitive answer, so an outage can delay onboarding but never hand out a burned address.
- **Guard rails** — new users land in a configurable organizational unit, creations are capped per day, and Zod schemas validate every tool input at the MCP boundary.
- **Rate limiting** — 60 requests/min per user on the MCP endpoint; 512 KB JSON body limit.
- **Audit logging** — every tool call is logged with user, action, and outcome (Cloud Logging on Cloud Run).
## Tech Stack
| Layer | Technology |
|-------|-----------|
| Runtime | Node.js 22 (ESM) |
| Language | TypeScript 5 |
| MCP SDK | `@modelcontextprotocol/sdk` |
| HTTP server | Express 5 |
| Google APIs | `googleapis` + `google-auth-library` (keyless DWD) |
| Validation | Zod 4 |
| Tests | `node:test` (unit tests for every module) |
| Container | Docker (multi-stage) |
| CI | GitHub Actions (CodeQL) + Google Cloud Build (`npm audit` gate) |
| Hosting | Google Cloud Run |
## Getting Started
### Prerequisites
- Node.js 22+
- A Google Workspace domain with super-admin access (to configure domain-wide delegation)
- A GCP project with Cloud Run enabled and a Google OAuth 2.0 Web Application client
- A Slack app bot token with the `users:read.email` scope (for the never-reuse check)
### Install
```bash
git clone https://github.com/micahyee415/google-workspace-admin-mcp
cd google-workspace-admin-mcp
npm install
```
### Configuration
Copy `.env.example` to `.env` and fill in the values:
```bash
cp .env.example .env
```
| Variable | Required | Description |
|----------|----------|-------------|
| `ALLOWED_DOMAIN` | Yes | Only verified Google accounts at this domain may connect (default: `example.com`) |
| `ALLOWED_USERS` | No | Per-user allowlist on top of the domain check (comma-separated emails). Unset = domain check only. |
| `DWD_SERVICE_ACCOUNT` | Yes | Service account that self-signs the delegation JWT (keyless, via `signJwt`) |
| `GWS_ADMIN_SUBJECT` | Yes | Workspace admin user the DWD service account impersonates |
| `GOOGLE_CLIENT_ID` | Yes | GCP OAuth 2.0 client ID (per-user sign-in) |
| `GOOGLE_CLIENT_SECRET` | Yes | GCP OAuth 2.0 client secret |
| `SLACK_BOT_TOKEN` | Yes | Slack bot token for the never-reuse check (from Secret Manager on Cloud Run) |
| `TARGET_OU` | No | Organizational unit new users are created into (default: `/Employees`) |
| `COMPANY_NAME` | No | Display name used in the credentials email (default: `Example Corp`) |
| `CREDENTIALS_FROM` | No | From: address for the credentials email (default: `it@<ALLOWED_DOMAIN>`) |
| `PORT` | No | HTTP port (default: `8080`; Cloud Run sets this automatically) |
**Domain-wide delegation setup:** in the Google Admin console, grant the DWD service account's client ID the scopes `https://www.googleapis.com/auth/admin.directory.user` and `https://www.googleapis.com/auth/gmail.send`. In GCP, grant the Cloud Run runtime service account `roles/iam.serviceAccountTokenCreator` on the DWD service account.
**Send-as note:** if `CREDENTIALS_FROM` is a group address (e.g. `it@your-domain.com`) rather than the impersonated admin's mailbox, configure a Gmail send-as alias for that address on the impersonated admin account first — Gmail silently rewrites the `From:` header otherwise.
### Run locally
```bash
npm run build
npm start
```
Run the test suite:
```bash
npm test
```
### Deploy to Cloud Run
```bash
gcloud builds submit --config cloudbuild.yaml --project your-gcp-project
```
Cloud Build will:
1. Run `npm audit` at `high` severity — fails the build if high/critical CVEs are found.
2. Build and push the Docker image.
3. Deploy to Cloud Run via `service.yaml` (environment variables and the `SLACK_BOT_TOKEN` Secret Manager reference are declared there).
4. Restore the public-invoker IAM binding — `gcloud run services replace` resets IAM on deploy (authentication is enforced in-app via Google OAuth).
## Connecting an MCP Client
Point your MCP client at the Cloud Run service URL:
```
https://your-service.example.com/mcp
```
The server implements [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) OAuth authorization server metadata and [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) dynamic client registration, so MCP clients that support these standards can auto-discover OAuth settings.
When prompted, authenticate with a Google account in the `ALLOWED_DOMAIN` domain. If `ALLOWED_USERS` is set, your email must also be on the allowlist.
## Security
- **Authentication** — every request requires a valid Google OAuth bearer token; only verified `@ALLOWED_DOMAIN` accounts (and, if set, `ALLOWED_USERS` members) are accepted, with audience validation against the configured OAuth client.
- **Least privilege** — create-only tool surface; delegation scopes limited to `admin.directory.user` + `gmail.send`; no service-account keys anywhere.
- **Secret hygiene** — temporary passwords are never returned, logged, or embedded in errors; the Slack token lives in Secret Manager, not in the image or env files.
- **Guard rails** — configurable target OU, daily creation cap, Zod validation at the MCP boundary, fail-closed availability checks, CRLF header-injection guard on personal emails.
- **CORS** — only `https://claude.ai` and `https://api.claude.ai` are accepted as origins.
- **Dynamic client registration caveat** — `/register` (RFC 7591) returns the configured OAuth client credentials so MCP clients can complete the Google OAuth flow; Google's registered redirect-URI allowlist is the effective security boundary. Register **only trusted callback URIs** on the OAuth client, and treat the client as effectively public.
- **Rate limiting** — 60 requests/min per user on the MCP endpoint.
- **Security headers** — `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Strict-Transport-Security` on all responses.
- **Audit logging** — every tool call is logged with the calling user, action, and result.
- **Dependency scanning** — `npm audit --audit-level=high` gates every Cloud Build deploy; Dependabot and CodeQL run on GitHub.
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues