google-sheets-mcp
by ezrazhang7
README.md
# Google Sheets MCP
An MCP server that lets Claude work with your Google Sheets **in place** — no more
"here's a new spreadsheet" every time.
Read data, edit cells inline with natural language, append rows, insert/delete rows and
columns, fill formulas, format cells, and find & replace — all against the sheet you
already have open.
Runs two ways from one codebase:
- **Local** — a `.mcpb` desktop extension for Claude Desktop and local Cowork sessions.
- **Remote** — a Cloudflare Worker you add as a custom connector, which also works in
**cloud Cowork sessions, claude.ai on the web, and mobile**.
Built on the [MCP TypeScript SDK v2](https://github.com/modelcontextprotocol/typescript-sdk)
(2026-07-28 spec) and the Google Sheets API v4, with no Google SDK dependency — just
`fetch` and WebCrypto, so the same code runs on Node and in V8 isolates.
## Tools
| Tool | What it does |
|---|---|
| `list_spreadsheets` | List/search your spreadsheets, most recent first |
| `get_spreadsheet_info` | Spreadsheet title, tabs, and grid sizes |
| `read_range` | Read values, raw values, or formulas from a range |
| `update_cells` | Overwrite a range in place (`=`-prefixed strings become formulas) |
| `append_rows` | Append rows after the last row of data |
| `clear_range` | Clear values (keeps formatting) |
| `fill_formula` | Fill one formula across a range with relative references adjusting |
| `insert_rows` / `insert_columns` | Insert blank rows/columns at a position |
| `delete_rows` / `delete_columns` | Delete entire rows/columns |
| `add_sheet` / `delete_sheet` | Manage tabs |
| `format_cells` | Bold, colors, alignment, wrap, number formats |
| `find_replace` | Find & replace across a sheet or the whole file |
| `batch_update` | Raw Sheets API batchUpdate escape hatch (charts, merges, validation, …) |
Spreadsheet IDs can be given as bare IDs **or full Google Sheets URLs**. Rows are 1-based
numbers and columns are letters, exactly as in the Sheets UI.
## 1. Google Cloud credentials (one-time, needed either way)
1. In [Google Cloud Console](https://console.cloud.google.com/), create or pick a project.
2. Enable the **Google Sheets API** and **Google Drive API**.
3. **APIs & Services → OAuth consent screen**: set it up, add yourself under **Test users**.
Then click **Publish app**. This does *not* require Google's verification review —
personal-use apps (under 100 known users) are exempt; you just click through an
"unverified app" warning once during authorization. Publishing matters because while
the app sits in **Testing**, Google revokes refresh tokens every 7 days.
The local extension recovers automatically by re-opening the browser, but a remote
deployment needs a manual re-mint each time.
4. **APIs & Services → Credentials → Create Credentials → OAuth client ID →
Desktop app**. Save the **Client ID** and **Client Secret**.
> The Sheets and Drive APIs are free to use, and this project never needs a Cloud billing
> account attached. Leaving billing unattached is the simplest guarantee against charges.
## 2a. Local install (Claude Desktop / local Cowork sessions)
```bash
npm install
npm run pack # builds and produces google-sheets.mcpb (~1.7 MB)
```
Double-click `google-sheets.mcpb`, or Claude Desktop → **Settings → Extensions →
Install Extension…**. Paste your Client ID and Secret into the extension settings.
On first use a browser opens to authorize; the refresh token is cached at
`~/.config/google-sheets-mcp/token.json`.
The extension asks only for OAuth credentials. Service-account auth is still
supported when running the server directly or remotely — set
`GOOGLE_SERVICE_ACCOUNT_KEY_FILE` or `GOOGLE_SERVICE_ACCOUNT_KEY` in the
environment instead.
> Local MCP servers do **not** run in cloud Cowork sessions or on claude.ai — for those,
> use the remote deployment below.
## 2b. Remote install (cloud Cowork sessions, web, mobile)
**Mint a refresh token** — this runs the same browser consent flow and prints a
long-lived token for the server to use:
```bash
GOOGLE_OAUTH_CLIENT_ID=…apps.googleusercontent.com \
GOOGLE_OAUTH_CLIENT_SECRET=… \
npm run mint-token
```
**Pick a bearer token.** This is the shared secret Claude must present on every request:
```bash
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
```
**Deploy to Cloudflare Workers** (free tier is plenty; the Worker is ~174 KB gzipped):
```bash
npx wrangler login
npx wrangler secret put MCP_BEARER_TOKEN # the value you just generated
npx wrangler secret put GOOGLE_OAUTH_CLIENT_ID
npx wrangler secret put GOOGLE_OAUTH_CLIENT_SECRET
npx wrangler secret put GOOGLE_REFRESH_TOKEN # from mint-token
npm run deploy
```
Wrangler prints a URL like `https://google-sheets-mcp.<you>.workers.dev`. Verify it with
`curl https://…/health` — it should return `ok`.
**Add it to Claude**: Settings → **Connectors** → **Add custom connector**.
- If your Add-connector dialog has a **Request headers** section (beta, gradual
rollout): URL `https://google-sheets-mcp.<you>.workers.dev/mcp` with header
`authorization: Bearer <your MCP_BEARER_TOKEN>`.
- If it doesn't, put the token in the URL path instead:
`https://google-sheets-mcp.<you>.workers.dev/mcp/<your MCP_BEARER_TOKEN>`.
### Security notes
- The endpoint is public, so the bearer token is the only thing standing between the
internet and your spreadsheets. Use a long random value and treat it like a password.
- Auth **fails closed**: if `MCP_BEARER_TOKEN` is unset, every request to `/mcp` is
rejected with 401 rather than running unauthenticated.
- Token comparison is constant-time, so the secret can't be recovered by timing.
- `GOOGLE_REFRESH_TOKEN` grants access to every spreadsheet your account can open. To
narrow that, use a service account instead (`GOOGLE_SERVICE_ACCOUNT_KEY`, inline JSON)
and share only specific sheets with its email address.
- To revoke everything at once: [myaccount.google.com/permissions](https://myaccount.google.com/permissions).
## Configuration reference
| Env var / secret | Purpose |
|---|---|
| `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` | OAuth client (both modes) |
| `GOOGLE_REFRESH_TOKEN` | Pre-minted refresh token (required for remote) |
| `GOOGLE_SERVICE_ACCOUNT_KEY` | Inline service-account JSON (alternative to OAuth) |
| `GOOGLE_SERVICE_ACCOUNT_KEY_FILE` | Path to a service-account key (local only) |
| `MCP_BEARER_TOKEN` | Shared secret required by the remote endpoint |
| `GSHEETS_TOKEN_PATH` | Override the local token cache location |
Scopes: `spreadsheets` (read/write) + `drive.metadata.readonly` (list/search files).
## Development
```bash
npm install
npm run build # compile to dist/
npm run typecheck # tsc --noEmit
npm run pack # build + package the .mcpb bundle
npm run dev:worker # run the Worker locally via wrangler
npm run deploy # deploy the Worker
```
```
src/
index.ts stdio entry point (local)
worker.ts Cloudflare Worker entry (remote) + bearer auth
server.ts server factory: registers all tool groups
config.ts credentials → token provider, shared by both entries
google/
auth.ts fetch + WebCrypto token providers (refresh token, service account)
client.ts REST client for the Sheets/Drive endpoints used
types.ts minimal API types
node/
localOAuth.ts Node-only desktop OAuth loopback flow
mintToken.ts CLI to print a refresh token for remote deploys
tools/ one module per tool group (read, write, structure, format, advanced)
utils/ A1-notation parsing, tool result/error helpers
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues