From f8b3e6a81bf29156ac28c7ab213951b73d33f005 Mon Sep 17 00:00:00 2001 From: Michael Ventura Date: Wed, 13 May 2026 17:21:48 -0400 Subject: [PATCH] Pin gws to 0.7.0 and switch to per-account isolated config dirs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gws mcp` was removed in v0.8.0, and the previous mint-token-once-at-startup wrapper died after 1hr — fatal for Claude Desktop. Replace with isolated per-account config dirs at ~/.config/gws/accounts// using KEYRING_BACKEND=file, so `gws mcp` refreshes access tokens internally with no expiry. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 232 ++++++++++++++++++----------------- README.md | 66 +++++----- docs/manual-setup.md | 169 +++++++++++++++---------- docs/research-notes.md | 8 ++ scripts/gws-token-wrapper.sh | 57 +++------ 5 files changed, 292 insertions(+), 240 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 31329f0..2fd95c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Claude Code Google Workspace + Slack MCP Setup -This repo helps users set up Google Workspace and Slack MCP servers for Claude Code. +This repo helps users set up Google Workspace and Slack MCP servers for Claude Code AND Claude Desktop. ## Your Role @@ -11,10 +11,12 @@ You are a setup assistant. Walk the user through configuring MCP servers for the ### Step 1: Install Google Workspace CLI ```bash -npm install -g @googleworkspace/cli +npm install -g @googleworkspace/cli@0.7.0 ``` -Verify: `gws --version` (should be 0.7+) +Verify: `gws --version` (must be **0.7.x exactly**) + +**CRITICAL:** The `gws mcp` subcommand was REMOVED in v0.8.0 (commit dd3fc90, PR #275). 0.7.0 is the last version where `gws mcp` exists. Do NOT install latest. If the user has a different `gws` on PATH (e.g. the Homebrew package — a different "git workspace" tool), uninstall it first: `brew uninstall gws`. ### Step 2: Create GCP Project @@ -22,6 +24,7 @@ Guide the user to https://console.cloud.google.com/projectcreate - Project name: something like `gws-mcp` (must be globally unique) - Note the project ID — you'll need it +- Sign in with whatever Google account should OWN the project. For users with both personal and workspace accounts, personal Gmail is usually cleanest (no employer IT policy risk; survives leaving the company). ### Step 3: Enable APIs @@ -29,7 +32,23 @@ Guide the user to https://console.cloud.google.com/projectcreate gcloud services enable gmail.googleapis.com drive.googleapis.com calendar-json.googleapis.com sheets.googleapis.com docs.googleapis.com --project=PROJECT_ID ``` -### Step 4: Create OAuth Consent Screen +If `gcloud` is logged in as a DIFFERENT account than the project owner, log it in first: +```bash +gcloud auth login OWNER_EMAIL +``` + +### Step 4: Grant API access to other accounts + +If accounts OTHER than the project owner will call APIs against this project, grant them `serviceusage.serviceUsageConsumer`: + +```bash +gcloud projects add-iam-policy-binding PROJECT_ID \ + --member="user:OTHER_EMAIL" \ + --role="roles/serviceusage.serviceUsageConsumer" \ + --condition=None +``` + +### Step 5: Create OAuth Consent Screen Guide user to: `https://console.cloud.google.com/apis/credentials/consent?project=PROJECT_ID` @@ -40,7 +59,7 @@ Guide user to: `https://console.cloud.google.com/apis/credentials/consent?projec - Test users: **Add ALL Google accounts** they want to use (critical for unverified apps) - Save -### Step 5: Create OAuth Client (one per Google account) +### Step 6: Create OAuth Client (one per Google account) Guide user to: `https://console.cloud.google.com/apis/credentials?project=PROJECT_ID` @@ -48,108 +67,96 @@ For EACH Google account: 1. Create Credentials → OAuth client ID 2. Application type: **Desktop app** 3. Name: descriptive (e.g., "MCP - personal" or "MCP - work") -4. Download the JSON → save to `~/.config/gws/client_secret_ACCOUNTNAME.json` +4. Download the JSON + +Have the user save / rename each downloaded JSON to something memorable like `personal-oauth.json`, `work-oauth.json`, etc. We'll move them into the right place next. **CRITICAL: Each Google account MUST have its own OAuth client.** Using one client for two accounts causes refresh token invalidation. -### Step 6: Authenticate Each Account +### Step 7: Set up per-account config dirs + +For each account (e.g. `personal`, `work`, `iai`): -For each account: +```bash +mkdir -p ~/.config/gws/accounts/ACCOUNTNAME +mv ~/Downloads/ACCOUNTNAME-oauth.json ~/.config/gws/accounts/ACCOUNTNAME/client_secret.json +``` + +This is the **critical architectural choice**: each account gets its own `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` with its own file-backed encrypted credential store. This is what enables `gws mcp` to refresh access tokens internally — no 1-hour expiry issue. + +### Step 8: Authenticate Each Account + +For each account, run: ```bash -# Copy this account's client secret into place -cp ~/.config/gws/client_secret_ACCOUNTNAME.json ~/.config/gws/client_secret.json +env GOOGLE_WORKSPACE_CLI_CONFIG_DIR=$HOME/.config/gws/accounts/ACCOUNTNAME \ + GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file \ + gws auth login -s drive,gmail,calendar,sheets,docs +``` -# Login (browser opens — sign in with the correct Google account) -gws auth login -s drive,gmail,calendar,sheets,docs +The browser opens — make sure the user signs in with the **correct** Google account for this command. `KEYRING_BACKEND=file` causes credentials to be stored in `credentials.enc` inside the per-account config dir, isolated from the system keyring (which only holds one account at a time). -# Export credentials -gws auth export --unmasked > ~/.config/gws/ACCOUNTNAME.json +Verify each account routes correctly: +```bash +env GOOGLE_WORKSPACE_CLI_CONFIG_DIR=$HOME/.config/gws/accounts/ACCOUNTNAME \ + GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file \ + gws drive about get --params '{"fields":"user(emailAddress)"}' ``` -**Important:** When the browser opens, make sure the user signs in with the correct account. The `gws` CLI opens whichever browser is in the foreground. +Should return the email of THAT account. -### Step 7: Install Token Wrapper +### Step 9: Install MCP Launcher Script ```bash cp scripts/gws-token-wrapper.sh ~/.config/gws/gws-token-wrapper.sh chmod +x ~/.config/gws/gws-token-wrapper.sh ``` -### Step 8: Write .mcp.json - -Create `.mcp.json` in the TARGET project root (not this repo — the project where they want to use the MCPs). +The script is a thin shim — it sets `CONFIG_DIR` + `KEYRING_BACKEND=file` and execs `gws mcp`. `gws mcp` then handles its own token refresh from the per-account encrypted creds. -**CRITICAL: `.mcp.json` MUST be at the project root. `settings.local.json` SILENTLY IGNORES `mcpServers`.** +### Step 10: Register the MCP servers -Template for one Google account: -```json -{ - "mcpServers": { - "gws-ACCOUNTNAME": { - "command": "HOME_DIR/.config/gws/gws-token-wrapper.sh", - "args": [ - "HOME_DIR/.config/gws/ACCOUNTNAME.json", - "-s", "gmail,drive,calendar,sheets,docs" - ] - } - } -} -``` +Two scopes to consider: -Template for two Google accounts + Slack: -```json -{ - "mcpServers": { - "gws-personal": { - "command": "HOME_DIR/.config/gws/gws-token-wrapper.sh", - "args": [ - "HOME_DIR/.config/gws/personal.json", - "-s", "gmail,drive,calendar,sheets,docs" - ] - }, - "gws-work": { - "command": "HOME_DIR/.config/gws/gws-token-wrapper.sh", - "args": [ - "HOME_DIR/.config/gws/work.json", - "-s", "gmail,drive,calendar,sheets,docs" - ] - }, - "slack": { - "command": "npx", - "args": ["-y", "slack-mcp-server@latest"], - "env": { - "SLACK_MCP_XOXP_TOKEN": "xoxp-your-token-here" +- **Claude Code, user scope** (available in all projects): + ```bash + claude mcp add --scope user gws-ACCOUNTNAME -- \ + $HOME/.config/gws/gws-token-wrapper.sh \ + $HOME/.config/gws/accounts/ACCOUNTNAME \ + -s gmail,drive,calendar,sheets,docs + ``` + Note the `--` separator (so `claude mcp add` doesn't try to parse `-s` as its own flag). + +- **Claude Code, project scope** (only in a specific project): create `.mcp.json` at the project root: + ```json + { + "mcpServers": { + "gws-ACCOUNTNAME": { + "command": "HOME_DIR/.config/gws/gws-token-wrapper.sh", + "args": [ + "HOME_DIR/.config/gws/accounts/ACCOUNTNAME", + "-s", "gmail,drive,calendar,sheets,docs" + ] } } } -} -``` - -Replace `HOME_DIR` with the actual home directory path (e.g., `/Users/username`). - -**Add `.mcp.json` to `.gitignore`** — it contains tokens. + ``` + `.mcp.json` MUST be at the project root. `settings.local.json` SILENTLY IGNORES `mcpServers`. -### Step 9: Restart Claude Code +- **Claude Desktop / Cowork**: edit `~/Library/Application Support/Claude/claude_desktop_config.json` and add an `mcpServers` block at the top level. Same JSON shape as `.mcp.json`. Disable the built-in `claude.ai` Google connectors in Settings → Connectors — they're read-only and the model may pick them over our write-capable ones. -MCP servers only load at session start. Restart to pick up the new config. +### Step 11: Restart Claude Code / Claude Desktop -### Step 10: Test +MCP servers only load at startup. Restart to pick up the new config. -Use ToolSearch to load and test: +### Step 12: Test ``` # List recent emails -ToolSearch: "select:mcp__gws-ACCOUNTNAME__gmail_users_messages_list" -→ mcp__gws-ACCOUNTNAME__gmail_users_messages_list(params: {"userId": "me", "maxResults": 3}) +mcp__gws-ACCOUNTNAME__gmail_users_messages_list(params: {"userId": "me", "maxResults": 3}) # Search Drive -ToolSearch: "select:mcp__gws-ACCOUNTNAME__drive_files_list" -→ mcp__gws-ACCOUNTNAME__drive_files_list(params: {"q": "name contains 'test'", "pageSize": 5}) - -# Slack channels -ToolSearch: "select:mcp__slack__channels_list" -→ mcp__slack__channels_list(channel_types: "public_channel") +mcp__gws-ACCOUNTNAME__drive_files_list(params: {"q": "name contains 'test'", "pageSize": 5}) ``` ## Slack Setup (Optional) @@ -178,9 +185,17 @@ Optional (for posting): OAuth & Permissions → Install to Workspace → Copy the `xoxp-...` User OAuth Token. -### Add to .mcp.json +### Add to MCP config -See the Slack entry in the template above. Replace `xoxp-your-token-here` with the actual token. +```json +"slack": { + "command": "npx", + "args": ["-y", "slack-mcp-server@latest"], + "env": { + "SLACK_MCP_XOXP_TOKEN": "xoxp-your-token-here" + } +} +``` **Note:** Message posting is disabled by default in `slack-mcp-server`. To enable, add `"SLACK_MCP_ADD_MESSAGE_TOOL": "true"` to the `env` section. @@ -190,54 +205,51 @@ See the Slack entry in the template above. Replace `xoxp-your-token-here` with t - Verify `.mcp.json` is at the **project root** (same directory as `.git/`) - NOT inside `.claude/` — that doesn't work - NOT in `settings.local.json` — silently ignored +- For Claude Desktop: check `~/Library/Application Support/Claude/claude_desktop_config.json` is valid JSON with `mcpServers` at the top level ### "Permission denied" on API calls - Check the GCP project has the right APIs enabled -- For multiple accounts: verify `chris@work.com` has `roles/serviceusage.serviceUsageConsumer` on the GCP project: - ```bash - gcloud projects add-iam-policy-binding PROJECT_ID \ - --member="user:work@example.com" \ - --role="roles/serviceusage.serviceUsageConsumer" - ``` +- For accounts other than the project owner: verify they have `roles/serviceusage.serviceUsageConsumer` on the GCP project (see Step 4) ### Wrong account's data returned +- Each account MUST use its own per-account config dir (Step 7) authenticated with `KEYRING_BACKEND=file` (Step 8) +- If accounts share a single config dir or rely on the system keyring, the last-logged-in account wins for everyone - Each account MUST use a different OAuth client ID -- Check `~/.config/gws/ACCOUNTNAME.json` — the `client_id` fields should differ ### "Access blocked" during OAuth -- Add the account as a test user on the OAuth consent screen -- Go to: `https://console.cloud.google.com/apis/credentials/consent?project=PROJECT_ID` -- Under "Test users" → Add the email +- Add the account as a test user on the OAuth consent screen (Step 5) -### Tokens expire after ~1 hour -- This is normal — access tokens are short-lived -- Restart Claude Code to get fresh tokens -- The wrapper script mints a new token each time a session starts +### "Invalid authentication credentials" after some time +- Tokens are refreshed internally by `gws mcp` using the encrypted creds in each per-account dir — there should be NO 1-hour expiry issue. +- If you ARE seeing this on a long-running session: the user likely set things up with the old "mint-token-once-at-startup" wrapper. Re-do Steps 7–9 with the per-account-dir approach. +- Also disable the built-in `claude.ai` Google connectors in Claude Desktop — they're read-only and intercept tool calls. + +### Read-only access in Claude Desktop despite write scopes +- Claude Desktop ships with built-in `claude.ai` Gmail/Drive/Calendar connectors that are read-only. +- Settings → Connectors → disable Gmail, Google Drive, Google Calendar. +- Our `gws-*` MCPs then handle everything with full read+write. ## Re-authentication -If tokens stop working: +If credentials get revoked or you need to re-grant scopes: ```bash -# 1. Swap to the account's OAuth client -cp ~/.config/gws/client_secret_ACCOUNTNAME.json ~/.config/gws/client_secret.json - -# 2. Re-login (browser opens) -gws auth login -s drive,gmail,calendar,sheets,docs -# Sign in with the correct Google account - -# 3. Re-export -gws auth export --unmasked > ~/.config/gws/ACCOUNTNAME.json - -# 4. Restart Claude Code +env GOOGLE_WORKSPACE_CLI_CONFIG_DIR=$HOME/.config/gws/accounts/ACCOUNTNAME \ + GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file \ + gws auth login -s drive,gmail,calendar,sheets,docs ``` +No restart needed unless the MCP process itself has crashed. + ## Key Gotchas (Read Before Debugging) -1. **`mcpServers` in `settings.local.json` is SILENTLY IGNORED** — no error, no log, servers just don't start -2. **`.mcp.json` must be at project root** — not inside `.claude/` -3. **One OAuth client per Google account** — same client_id across accounts = token invalidation -4. **MCP servers start at session launch only** — config changes require restart -5. **Access tokens expire ~1hr** — restart for fresh tokens on long sessions -6. **`gws` uses single-dash CLI flags** — `-t stdio` not `--transport stdio` -7. **`.mcp.json` contains tokens** — add to `.gitignore` +1. **`gws` must be pinned to 0.7.0** — `gws mcp` was removed in 0.8.0+ +2. **`mcpServers` in `settings.local.json` is SILENTLY IGNORED** — no error, no log, servers just don't start +3. **`.mcp.json` must be at project root** — not inside `.claude/` +4. **One OAuth client per Google account** — same client_id across accounts = token invalidation +5. **One config dir per account** — `KEYRING_BACKEND=file` with isolated `CONFIG_DIR` is what makes multi-account work +6. **MCP servers start at session launch only** — config changes require restart +7. **`gws` uses single-dash CLI flags** — `-s drive` not `--scope drive` +8. **`.mcp.json` contains no secrets in this setup** — secrets are in `~/.config/gws/accounts/*/credentials.enc`, but the per-account-dir paths still shouldn't leak in repos +9. **Claude Desktop built-in Google connectors are read-only** — disable them or write calls will silently fail +10. **For `claude mcp add` with `-s` in inner command** — use `--` separator: `claude mcp add gws-foo -- wrapper.sh dir -s drive` diff --git a/README.md b/README.md index 51ff3ee..daaa371 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ It's not one problem — it's a stack of them, each undocumented: 2. **One OAuth client for two Google accounts breaks both.** Google invalidates account A's refresh token when you authenticate account B with the same OAuth client. You need separate OAuth Desktop App clients — one per account — in the same GCP project. -3. **The credential file env var doesn't work.** `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` is supposed to let you point the [Google Workspace CLI](https://github.com/googleworkspace/cli) at different credential files, but it doesn't reliably route to the right account. You have to mint fresh access tokens and pass them via `GOOGLE_WORKSPACE_CLI_TOKEN` (the highest-priority auth method). +3. **`gws mcp` was removed in v0.8.0.** The Google Workspace CLI's MCP server mode (`gws mcp`) only exists in versions ≤0.7.x. Install with `npm install -g @googleworkspace/cli@0.7.0` and pin it — `npm update` will break the setup. If you have Homebrew's `gws` (a different "git workspace" tool) installed, `brew uninstall gws` first. + +4. **The credential file env var doesn't work — and neither does the system keyring for multi-account.** `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` doesn't reliably route to the right account. The system keyring (`gws` default) only stores credentials for one account at a time — the last `gws auth login` wins for everyone. The fix is per-account isolated config directories with `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` + `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file`, so each account has its own encrypted credential store and `gws mcp` can refresh tokens internally with no 1-hour expiry. 4. **Go binaries use single-dash flags.** Both the `gws` CLI and [`slack-mcp-server`](https://github.com/nichochar/slack-mcp-server) are Go binaries. `--transport stdio` fails silently. It's `-t stdio` — or just omit it (stdio is the default). @@ -30,7 +32,9 @@ Each of these individually cost 30-120 minutes to diagnose. Together they make t |----------|--------------| | Legacy `google-workspace` Claude Code plugin | Single-account only, port 8000 conflict blocks second instance | | Single OAuth client for both accounts | Google invalidates first account's refresh token on second auth | -| `CREDENTIALS_FILE` env var per MCP server | Doesn't reliably route to the right account | +| `CREDENTIALS_FILE` env var per MCP server | Doesn't reliably route to the right account (system keyring wins) | +| Mint-one-shot access token via `GOOGLE_WORKSPACE_CLI_TOKEN` | Works for ~1 hour, then token expires and `gws mcp` can't refresh — fatal for Claude Desktop | +| `gws` CLI 0.8.0+ | `gws mcp` subcommand was removed — must pin to 0.7.x | | `mcpServers` in `settings.local.json` | Silently ignored — Claude Code never reads MCP config from there | | `mcpServers` in `.claude/settings.json` | Also silently ignored | | Service account auth | Can't access personal Gmail/Drive without domain-wide delegation | @@ -42,10 +46,11 @@ For the full investigation log, see [docs/research-notes.md](docs/research-notes The working setup uses: -- **[Google Workspace CLI](https://github.com/googleworkspace/cli)** (`gws` v0.7+) — Google's official CLI with built-in MCP server mode +- **[Google Workspace CLI](https://github.com/googleworkspace/cli)** (`gws` v0.7.0 — pinned; `gws mcp` was removed in 0.8.0) - **Separate OAuth Desktop clients** — one per Google account, in the same GCP project -- **A token wrapper script** — mints fresh access tokens from each account's credential file, passes them via the highest-priority env var -- **`.mcp.json` at project root** — the only config location Claude Code actually reads for MCP servers +- **Per-account isolated config directories** — each at `~/.config/gws/accounts//` with its own `client_secret.json` and file-backed encrypted credential store, via `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` + `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file`. This lets `gws mcp` refresh access tokens internally — no 1-hour expiry issue. +- **A thin launcher script** — sets the env vars and execs `gws mcp` against the right account dir +- **`.mcp.json` at project root** (or user-scope via `claude mcp add --scope user`, or `~/Library/Application Support/Claude/claude_desktop_config.json` for Claude Desktop) - **[slack-mcp-server](https://github.com/nichochar/slack-mcp-server)** — Go-based Slack MCP, read-only by default ### What you get @@ -95,38 +100,37 @@ To understand every decision and failed approach: [docs/research-notes.md](docs/ ## Architecture ``` -Claude Code session start +Claude Code / Claude Desktop session start │ - ├─── reads .mcp.json (project root) + ├─── reads MCP config (.mcp.json | ~/.claude.json | claude_desktop_config.json) │ ├─── starts gws-personal server: │ │ - │ ├── gws-token-wrapper.sh personal.json + │ ├── gws-token-wrapper.sh ~/.config/gws/accounts/personal -s ... │ │ │ - │ │ ├── reads personal.json (client_id_A + refresh_token) - │ │ ├── POST https://oauth2.googleapis.com/token → access_token - │ │ └── exec: GOOGLE_WORKSPACE_CLI_TOKEN= gws mcp -s gmail,drive,... + │ │ └── exec: env GOOGLE_WORKSPACE_CLI_CONFIG_DIR=.../personal \ + │ │ GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file \ + │ │ gws mcp -s gmail,drive,... │ │ - │ └── gws mcp ←→ Google APIs (Gmail, Drive, Calendar, Sheets, Docs) + │ └── gws mcp ←→ refreshes access tokens internally from credentials.enc + │ ←→ Google APIs (Gmail, Drive, Calendar, Sheets, Docs) │ ├─── starts gws-work server: │ │ - │ └── (same flow, different credential file with client_id_B) + │ └── (same flow, points at ~/.config/gws/accounts/work) │ └─── starts slack server: │ └── npx slack-mcp-server (SLACK_MCP_XOXP_TOKEN env var) ``` -### Why the wrapper script? +### Why isolated config dirs? -The `gws` CLI stores one set of credentials at a time internally. For multiple accounts, we can't rely on its credential file routing (`CREDENTIALS_FILE` env var is unreliable). Instead, we: +The `gws` CLI defaults to the system keyring for credential storage, and the keyring only holds one set of credentials at a time — the last `gws auth login` wins for every account. With a single shared config dir, calling `gws drive about get` resolves to whoever logged in last, regardless of which `CREDENTIALS_FILE` env var or `client_secret.json` you point at. -1. Read the exported credential file (has `client_id`, `client_secret`, `refresh_token`) -2. Call Google's token endpoint directly to mint a fresh access token -3. Pass it via `GOOGLE_WORKSPACE_CLI_TOKEN` — the highest-priority auth method in the `gws` auth chain +By giving each account its own `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` and switching the keyring backend to `file`, each account gets an isolated encrypted credential store (`credentials.enc`) that `gws mcp` can decrypt, refresh, and use independently. No shared state, no contention, no 1-hour expiry — `gws mcp` mints new access tokens internally as needed. -This is 15 lines of shell + Python stdlib. No dependencies. +The launcher script is a 5-line shim: it sets the two env vars and execs `gws mcp`. ### Why separate OAuth clients? @@ -151,22 +155,26 @@ These are ordered by how much time they'll cost you if you hit them unaware: |---|--------|-----------|---------|-----| | 1 | `mcpServers` in `settings.local.json` is **silently ignored** | 2+ hours | Servers never start, zero errors | Use `.mcp.json` (project root) or `~/.claude.json` (global). [Issue #24477](https://github.com/anthropics/claude-code/issues/24477) | | 2 | Same OAuth client for two accounts | 1-2 hours | First account stops working after second account authenticates | Create separate OAuth Desktop clients per account | -| 3 | `CREDENTIALS_FILE` env var unreliable | 1 hour | Wrong account's data returned | Use `GOOGLE_WORKSPACE_CLI_TOKEN` via wrapper script | -| 4 | Work account shows "Access blocked" | 30 min | OAuth consent screen blocks non-test users | Add all accounts as test users on OAuth consent screen + grant IAM `serviceUsageConsumer` role | -| 5 | MCP servers only start at session launch | 15 min | Config changes don't take effect | Restart Claude Code after any `.mcp.json` change | -| 6 | Go binaries use single-dash flags | 15 min | `--transport stdio` fails silently | Use `-t stdio` or omit (stdio is default) | -| 7 | Access tokens expire after ~1 hour | 5 min | API calls fail on long sessions | Restart Claude Code for fresh tokens | -| 8 | `gws auth login` opens foreground browser | 5 min | Wrong Chrome profile = wrong account authenticated | Bring correct Chrome profile to front before running | -| 9 | Too many OAuth scopes requested | 10 min | Google blocks unverified app | Use `-s drive,gmail,calendar,sheets,docs` (not all scopes) | +| 3 | `gws mcp` removed in v0.8.0 | 30 min | `Unknown service 'mcp'` from `gws mcp ...` on any current install | Pin to 0.7.0: `npm install -g @googleworkspace/cli@0.7.0` | +| 4 | System keyring stores only one account's creds | 1 hour | Same data returned for every account, regardless of which MCP is invoked | Use per-account `CONFIG_DIR` + `KEYRING_BACKEND=file` (isolated encrypted credential stores) | +| 5 | Mint-one-shot token approach dies after 1hr | 1 hour | API calls work briefly, then fail with "invalid authentication credentials" — fatal in long-running Claude Desktop | Use the per-account-dir architecture so `gws mcp` can refresh tokens itself | +| 6 | Work account shows "Access blocked" | 30 min | OAuth consent screen blocks non-test users | Add all accounts as test users on OAuth consent screen + grant IAM `serviceUsageConsumer` role | +| 7 | Claude Desktop built-in Google connectors are read-only | 30 min | "I don't have write access" even with our MCPs configured | Settings → Connectors → disable the built-in Gmail/Drive/Calendar | +| 8 | MCP servers only start at session launch | 15 min | Config changes don't take effect | Restart Claude Code / Claude Desktop after MCP config changes | +| 9 | Go binaries use single-dash flags | 15 min | `--transport stdio` fails silently | Use `-t stdio` or omit (stdio is default) | +| 10 | `claude mcp add` collides with inner `-s` flag | 10 min | "Invalid scope" error from `claude mcp add` | Use `--` separator: `claude mcp add NAME -- WRAPPER ARGS -s scopes` | +| 11 | Homebrew `gws` package collides on binary | 10 min | `gws` is on PATH but it's the "git workspace" tool, not Google | `brew uninstall gws` before `npm install -g @googleworkspace/cli@0.7.0` | +| 12 | `gws auth login` opens foreground browser | 5 min | Wrong Chrome profile = wrong account authenticated | Bring correct Chrome profile to front before running | +| 13 | Too many OAuth scopes requested | 10 min | Google blocks unverified app | Use `-s drive,gmail,calendar,sheets,docs` (not all scopes) | +| 14 | `gws auth export` includes "Using keyring backend" prefix line | 5 min | Exported JSON file unparseable | (No longer relevant — the new architecture doesn't use `gws auth export` at all) | ## Caveats -- **Written March 2026** — tested with `gws` CLI v0.7.0 and current Claude Code +- **Tested with `gws` CLI v0.7.0 and current Claude Code / Claude Desktop**. `gws mcp` was removed in v0.8.0 — the setup will not work on newer `gws` until/unless someone ships a replacement standalone Google Workspace MCP server we can switch to. - Google and Anthropic will likely make this easier natively — this repo fills the gap until then - GCP project + OAuth consent screen setup requires manual browser steps (can't be fully automated) -- Access tokens minted at session start expire after ~1 hour — restart for long sessions - Tested on macOS — Linux should work, Windows untested -- The `gws` CLI is pre-1.0 — flags and auth behavior may change +- The `gws` CLI is pre-1.0 — flags and auth behavior may change again ## References diff --git a/docs/manual-setup.md b/docs/manual-setup.md index 8988d7a..600ef86 100644 --- a/docs/manual-setup.md +++ b/docs/manual-setup.md @@ -1,25 +1,33 @@ # Manual Setup Guide -Step-by-step instructions for setting up Google Workspace + Slack MCP servers in Claude Code without AI assistance. +Step-by-step instructions for setting up Google Workspace + Slack MCP servers in Claude Code AND Claude Desktop without AI assistance. ## Google Workspace MCP -### 1. Install the Google Workspace CLI +### 1. Install the Google Workspace CLI (pinned to 0.7.0) ```bash -npm install -g @googleworkspace/cli -gws --version # should be 0.7+ +# If Homebrew's git-workspace `gws` is installed, uninstall it first: +brew uninstall gws 2>/dev/null + +npm install -g @googleworkspace/cli@0.7.0 +gws --version # must be 0.7.x ``` +**Why pinned:** `gws mcp` was removed in v0.8.0 (PR #275). 0.7.0 is the last version with the MCP subcommand. Do NOT `npm update` this package. + ### 2. Create a GCP Project -1. Go to https://console.cloud.google.com/projectcreate +1. Go to https://console.cloud.google.com/projectcreate (signed in with the account that should OWN the project — personal Gmail is usually cleanest) 2. Name it something like `gws-mcp` (project ID must be globally unique) 3. Note the project ID ### 3. Enable APIs ```bash +# If gcloud isn't logged in as the project owner: +gcloud auth login OWNER_EMAIL + gcloud services enable \ gmail.googleapis.com \ drive.googleapis.com \ @@ -29,7 +37,18 @@ gcloud services enable \ --project=YOUR_PROJECT_ID ``` -### 4. Configure OAuth Consent Screen +### 4. Grant API access to other accounts (if any) + +For each non-owner account that will use this project: + +```bash +gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \ + --member="user:OTHER_EMAIL" \ + --role="roles/serviceusage.serviceUsageConsumer" \ + --condition=None +``` + +### 5. Configure OAuth Consent Screen 1. Go to: `https://console.cloud.google.com/apis/credentials/consent?project=YOUR_PROJECT_ID` 2. Select **External** user type @@ -41,7 +60,7 @@ gcloud services enable \ 5. **Add test users**: Add **every Google account** you want to connect 6. Save -### 5. Create OAuth Desktop Client(s) +### 6. Create OAuth Desktop Client(s) Go to: `https://console.cloud.google.com/apis/credentials?project=YOUR_PROJECT_ID` @@ -52,51 +71,57 @@ Go to: `https://console.cloud.google.com/apis/credentials?project=YOUR_PROJECT_I 3. Name: something descriptive (e.g., "MCP - personal", "MCP - work") 4. Click "Create" 5. Download the JSON file -6. Save it: - ```bash - mkdir -p ~/.config/gws - # Move the downloaded file: - mv ~/Downloads/client_secret_*.json ~/.config/gws/client_secret_ACCOUNTNAME.json - ``` +6. We'll place each file into its account's isolated config directory next. **Why separate clients?** Using the same OAuth client ID for two Google accounts causes the second login to invalidate the first account's refresh token. This is an OAuth2 behavior, not a bug. -### 6. Authenticate Each Account +### 7. Set up per-account isolated config directories -For each account (e.g., "personal" and "work"): +For each account (e.g., `personal`, `work`): ```bash -# Set this account's client secret as active -cp ~/.config/gws/client_secret_personal.json ~/.config/gws/client_secret.json +mkdir -p ~/.config/gws/accounts/personal +mkdir -p ~/.config/gws/accounts/work -# Login — browser will open, sign in with the CORRECT Google account -gws auth login -s drive,gmail,calendar,sheets,docs - -# Export the credentials (includes refresh token) -gws auth export --unmasked > ~/.config/gws/personal.json +# Move each downloaded OAuth client file into its account dir, renamed: +mv ~/Downloads/.json ~/.config/gws/accounts/personal/client_secret.json +mv ~/Downloads/.json ~/.config/gws/accounts/work/client_secret.json ``` -Repeat for each additional account, swapping the client secret file each time. +**Why isolated dirs?** The `gws` CLI defaults to the system keyring, which only stores credentials for one account at a time. Without isolation, the last login wins for every account. We give each account its own `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` and use `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file` so credentials live in a per-account `credentials.enc` that `gws mcp` can decrypt and refresh internally. + +### 8. Authenticate Each Account -### 7. Install the Token Wrapper +For each account, run: ```bash -cp scripts/gws-token-wrapper.sh ~/.config/gws/gws-token-wrapper.sh -chmod +x ~/.config/gws/gws-token-wrapper.sh +env GOOGLE_WORKSPACE_CLI_CONFIG_DIR=$HOME/.config/gws/accounts/personal \ + GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file \ + gws auth login -s drive,gmail,calendar,sheets,docs +# Browser opens — sign in as the personal account ``` -**What this does:** The `gws` CLI can only hold one account's credentials at a time. The wrapper script reads the exported credential file, mints a fresh OAuth access token using the refresh token, and passes it to `gws mcp` via the `GOOGLE_WORKSPACE_CLI_TOKEN` environment variable (the highest-priority auth method). +Repeat for `work` (and any other accounts), substituting the dir. -### 8. If Using a Second Account from a Different Google Workspace Domain +**Verify** each account routes correctly: -The GCP project owner's account works automatically. For accounts on other domains (e.g., a work Google Workspace), you need to grant API access: +```bash +env GOOGLE_WORKSPACE_CLI_CONFIG_DIR=$HOME/.config/gws/accounts/personal \ + GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file \ + gws drive about get --params '{"fields":"user(emailAddress)"}' +``` + +The returned email should match the account you authenticated with. + +### 9. Install the Launcher Script ```bash -gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \ - --member="user:you@workdomain.com" \ - --role="roles/serviceusage.serviceUsageConsumer" +cp scripts/gws-token-wrapper.sh ~/.config/gws/gws-token-wrapper.sh +chmod +x ~/.config/gws/gws-token-wrapper.sh ``` +**What it does:** Thin shim that sets `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` + `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file` and execs `gws mcp`. `gws mcp` then refreshes access tokens from the per-account `credentials.enc` as needed — no 1-hour expiry. + --- ## Slack MCP (Optional) @@ -137,11 +162,24 @@ Go to "OAuth & Permissions" → "User Token Scopes" → Add these scopes: --- -## Configure Claude Code +## Configure Claude Code / Claude Desktop + +You have three places to register MCP servers, pick whichever fits: + +### Option A: Claude Code user scope (available in all projects) + +```bash +claude mcp add --scope user gws-personal -- \ + $HOME/.config/gws/gws-token-wrapper.sh \ + $HOME/.config/gws/accounts/personal \ + -s gmail,drive,calendar,sheets,docs +``` + +Note the `--` separator so `claude mcp add` doesn't try to parse `-s` as its own scope flag. -### Create `.mcp.json` +Repeat for `gws-work` etc. -In the root of the project where you want these MCP servers available, create `.mcp.json`: +### Option B: Claude Code project scope (`.mcp.json` at project root) ```json { @@ -149,14 +187,14 @@ In the root of the project where you want these MCP servers available, create `. "gws-personal": { "command": "/Users/YOUR_USERNAME/.config/gws/gws-token-wrapper.sh", "args": [ - "/Users/YOUR_USERNAME/.config/gws/personal.json", + "/Users/YOUR_USERNAME/.config/gws/accounts/personal", "-s", "gmail,drive,calendar,sheets,docs" ] }, "gws-work": { "command": "/Users/YOUR_USERNAME/.config/gws/gws-token-wrapper.sh", "args": [ - "/Users/YOUR_USERNAME/.config/gws/work.json", + "/Users/YOUR_USERNAME/.config/gws/accounts/work", "-s", "gmail,drive,calendar,sheets,docs" ] }, @@ -171,67 +209,70 @@ In the root of the project where you want these MCP servers available, create `. } ``` -Replace paths and tokens with your actual values. +Replace `YOUR_USERNAME` with your actual home dir. -### Global Setup (Optional) +### Option C: Claude Desktop / Cowork -To make these available in ALL projects, add the `mcpServers` config to `~/.claude.json` instead. +Edit `~/Library/Application Support/Claude/claude_desktop_config.json` — add an `mcpServers` block at the top level with the same shape as Option B. + +**Important:** Claude Desktop ships with built-in `claude.ai` connectors for Gmail, Google Drive, and Google Calendar. They are **read-only**. The model may pick those over our write-capable `gws-*` MCPs. Go to Settings → Connectors and disable the built-in Google connectors. ### Gitignore -**`.mcp.json` contains tokens — never commit it.** +If using `.mcp.json` in a real project, add it to `.gitignore`. (The new architecture doesn't put OAuth secrets in `.mcp.json` directly — they live in `~/.config/gws/accounts/*/credentials.enc` — but the per-account dir paths can still leak environment info.) -```bash -echo ".mcp.json" >> .gitignore -``` +### Restart Claude Code / Claude Desktop -### Restart Claude Code - -MCP servers only load at session launch. You must restart after creating/editing `.mcp.json`. +MCP servers only load at session launch. --- ## Verify It Works -After restarting Claude Code, test each server: +After restarting: ``` # Gmail — list recent emails -ToolSearch: "+gws gmail messages list" mcp__gws-personal__gmail_users_messages_list(params: {"userId": "me", "maxResults": 3}) # Drive — search files -ToolSearch: "+gws drive files" -mcp__gws-personal__drive_files_list(params: {"q": "modifiedTime > '2024-01-01'", "pageSize": 5}) +mcp__gws-personal__drive_files_list(params: {"q": "modifiedTime > '2024-01-01'", "pageSize": 5, "fields": "files(id,name)"}) # Slack — list channels -ToolSearch: "+slack channels" mcp__slack__channels_list(channel_types: "public_channel") ``` -If tools don't appear in ToolSearch, check: +If tools don't appear: 1. `.mcp.json` is at the project root (not inside `.claude/`) -2. You restarted Claude Code after creating it -3. The wrapper script is executable (`chmod +x`) -4. Credential files exist at the paths specified +2. You restarted Claude Code / Claude Desktop +3. Wrapper script is executable (`chmod +x`) +4. Account dirs exist with both `client_secret.json` AND `credentials.enc` --- ## Troubleshooting -See the [README](../README.md#known-gotchas) for a full list of known gotchas. +See the [README](../README.md#known-gotchas) for a full table. ### Common Issues +**"Unknown service 'mcp'" when wrapper runs:** +→ `gws` is on a version ≥ 0.8.0. Pin to 0.7.0: `npm install -g @googleworkspace/cli@0.7.0 --force`. + +**Wrong account's data returned:** +→ Each account needs its OWN `~/.config/gws/accounts//` dir, authenticated via `gws auth login` with `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` + `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file`. If accounts share a dir or rely on the system keyring, the last login wins for everyone. + +**"Invalid authentication credentials" after some time (especially Claude Desktop):** +→ This was a problem with the old "mint-once-at-startup" wrapper. The new architecture lets `gws mcp` refresh tokens internally, so this should not recur. If it does, verify `credentials.enc` exists in the account dir and re-run the Step 8 login command if needed. + **"Access blocked" during OAuth login:** -→ Add the Google account as a test user on the OAuth consent screen +→ Add the Google account as a test user on the OAuth consent screen (Step 5). -**Wrong account's emails showing up:** -→ Each account needs its own OAuth client ID. Check that `client_id` differs between credential files. +**API calls fail with "permission denied":** +→ For work accounts on a different domain, grant IAM access (Step 4). **MCP servers not starting (no error):** -→ `mcpServers` in `settings.local.json` is silently ignored. Must be in `.mcp.json` (project root) or `~/.claude.json` (global). +→ For Claude Code: `mcpServers` in `settings.local.json` is silently ignored. Use `.mcp.json` (project root), `~/.claude.json` (global via `claude mcp add --scope user`), or `claude_desktop_config.json` for Claude Desktop. -**API calls fail with "permission denied":** -→ For work accounts on a different domain, grant IAM access (see Step 8 above). -→ Also check: `gcloud auth application-default set-quota-project YOUR_PROJECT_ID` +**Claude Desktop says "I don't have write access" despite write scopes:** +→ Settings → Connectors → disable the built-in Gmail/Drive/Calendar (claude.ai-managed, read-only). diff --git a/docs/research-notes.md b/docs/research-notes.md index ca30838..b3ca381 100644 --- a/docs/research-notes.md +++ b/docs/research-notes.md @@ -2,6 +2,14 @@ This documents the full investigation — what we tried, what failed, what we learned, and why the final solution looks the way it does. Written March 2026. +> **Update (May 2026):** Two of the conclusions in these notes have since been superseded. +> +> 1. **`gws mcp` removed in v0.8.0** — the CLI's MCP server mode was deleted in [PR #275](https://github.com/googleworkspace/cli/pull/275). The setup now requires pinning to `@googleworkspace/cli@0.7.0`. There's no detailed rationale in the changelog beyond "BREAKING CHANGE: Remove MCP server mode". +> +> 2. **The "mint-token-via-wrapper" approach has been replaced.** That approach worked but expired after 1 hour, requiring a Claude Code restart — fatal for Claude Desktop. The new architecture uses per-account isolated config dirs with `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` + `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file`, so `gws mcp` can refresh tokens internally with no expiry. See [manual-setup.md](manual-setup.md) for the current procedure. +> +> The rest of this document — the gotchas, the OAuth-client-per-account rule, the `.mcp.json` placement rules, the Claude Desktop built-in connector behavior — still applies. + ## The Starting Point **Goal:** Give Claude Code access to Gmail, Google Drive, Calendar, Sheets, and Docs — for multiple Google accounts (personal + work). Also Slack. diff --git a/scripts/gws-token-wrapper.sh b/scripts/gws-token-wrapper.sh index 7b7fe94..7f89e1c 100755 --- a/scripts/gws-token-wrapper.sh +++ b/scripts/gws-token-wrapper.sh @@ -1,52 +1,35 @@ #!/bin/bash -# Token wrapper for multi-account Google Workspace CLI MCP +# Per-account Google Workspace MCP launcher. # -# Problem: The gws CLI stores one set of credentials at a time. -# The CREDENTIALS_FILE env var doesn't reliably route to the right account. +# Each account has its own isolated config dir at / containing +# its own client_secret.json and file-backed encrypted credentials (created +# by `gws auth login` with KEYRING_BACKEND=file). `gws mcp` refreshes access +# tokens internally from the encrypted creds — no 1-hour expiry issue. # -# Solution: Mint a fresh access token from the exported credential file, -# then pass it via GOOGLE_WORKSPACE_CLI_TOKEN (highest priority in gws auth chain). -# -# Usage: gws-token-wrapper.sh [gws mcp args...] -# Example: gws-token-wrapper.sh ~/.config/gws/personal.json -s gmail,drive +# Usage: gws-token-wrapper.sh [gws mcp args...] +# Example: gws-token-wrapper.sh ~/.config/gws/accounts/personal -s gmail,drive set -euo pipefail -CREDS_FILE="$1" +ACCOUNT_DIR="$1" shift -if [ ! -f "$CREDS_FILE" ]; then - echo "Error: Credential file not found: $CREDS_FILE" >&2 +if [ ! -d "$ACCOUNT_DIR" ]; then + echo "Error: Account config dir not found: $ACCOUNT_DIR" >&2 exit 1 fi -# Mint a fresh access token using the refresh token (Python stdlib only) -TOKEN=$(python3 -c " -import json, urllib.request, urllib.parse, sys - -try: - with open('$CREDS_FILE') as f: - d = json.load(f) - - data = urllib.parse.urlencode({ - 'client_id': d['client_id'], - 'client_secret': d['client_secret'], - 'refresh_token': d['refresh_token'], - 'grant_type': 'refresh_token' - }).encode() - - req = urllib.request.Request('https://oauth2.googleapis.com/token', data) - resp = json.loads(urllib.request.urlopen(req).read()) - print(resp['access_token']) -except Exception as e: - print(f'Token mint failed: {e}', file=sys.stderr) - sys.exit(1) -" 2>/dev/null) +if [ ! -f "$ACCOUNT_DIR/client_secret.json" ]; then + echo "Error: $ACCOUNT_DIR/client_secret.json missing" >&2 + exit 1 +fi -if [ -z "$TOKEN" ]; then - echo "Error: Failed to mint access token from $CREDS_FILE" >&2 +if [ ! -f "$ACCOUNT_DIR/credentials.enc" ]; then + echo "Error: $ACCOUNT_DIR not authenticated. Run:" >&2 + echo " env GOOGLE_WORKSPACE_CLI_CONFIG_DIR=$ACCOUNT_DIR GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file gws auth login -s drive,gmail,calendar,sheets,docs" >&2 exit 1 fi -# Run gws mcp with the minted token (suppressing stderr noise) -exec env GOOGLE_WORKSPACE_CLI_TOKEN="$TOKEN" gws mcp "$@" 2>/dev/null +exec env GOOGLE_WORKSPACE_CLI_CONFIG_DIR="$ACCOUNT_DIR" \ + GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file \ + gws mcp "$@"