{
  "markdown": "<h1 align=\"center\">better-workspace-mcp</h1>\n\n<p align=\"center\">\n  <strong>Google Workspace MCP — Docs, Drive, Calendar, Gmail, Sheets, Slides, Tasks, Chat, People, Forms</strong>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/n24q02m/better-workspace-mcp/actions/workflows/ci.yml\"><img alt=\"CI\" src=\"https://github.com/n24q02m/better-workspace-mcp/actions/workflows/ci.yml/badge.svg\"></a>\n  <a href=\"https://github.com/n24q02m/better-workspace-mcp/actions/workflows/cd.yml\"><img alt=\"CD\" src=\"https://github.com/n24q02m/better-workspace-mcp/actions/workflows/cd.yml/badge.svg\"></a>\n  <a href=\"https://codecov.io/gh/n24q02m/better-workspace-mcp\"><img alt=\"codecov\" src=\"https://codecov.io/gh/n24q02m/better-workspace-mcp/graph/badge.svg\"></a>\n  <a href=\"https://github.com/n24q02m/better-workspace-mcp/releases/latest\"><img alt=\"Latest release\" src=\"https://img.shields.io/github/v/release/n24q02m/better-workspace-mcp?display_name=tag&sort=semver\"></a>\n  <a href=\"https://github.com/python-semantic-release/python-semantic-release\"><img alt=\"semantic-release\" src=\"https://img.shields.io/badge/semantic--release-e10079?logo=semantic-release&logoColor=white\"></a>\n  <a href=\"LICENSE\"><img alt=\"License: Apache-2.0\" src=\"https://img.shields.io/github/license/n24q02m/better-workspace-mcp\"></a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://mcp.n24q02m.com\">Docs</a> ·\n  <a href=\"#install\">Install</a> ·\n  <a href=\"#quick-start\">Quick start</a> ·\n  <a href=\"https://github.com/n24q02m/better-workspace-mcp/discussions\">Community</a>\n</p>\n\n<!-- BEGIN: AUTO-GENERATED-CROSS-PROMO -->\n<details>\n  <summary><strong>Sister projects from n24q02m</strong> (click to expand)</summary>\n\n| Project | Tagline | Tag |\n|---|---|---|\n| [agent-chat-plugin](https://github.com/n24q02m/agent-chat-plugin) | Peer AI agents chat in a shared folder — no human relay, no orchestrator, wor... | Tooling |\n| [better-code-review-graph](https://github.com/n24q02m/better-code-review-graph) | Knowledge graph for token-efficient code reviews -- semantic search and call-... | MCP |\n| [better-drive](https://github.com/n24q02m/better-drive) | 2-way Google Drive sync with .driveignore filter — rclone engine, Windows tray | Tooling |\n| [better-email-mcp](https://github.com/n24q02m/better-email-mcp) | IMAP/SMTP email for AI agents -- read, send, organize folders, and manage att... | MCP |\n| [better-godot-mcp](https://github.com/n24q02m/better-godot-mcp) | Composite MCP server for Godot Engine -- 17 composite tools for AI-assisted g... | MCP |\n| [better-notion-mcp](https://github.com/n24q02m/better-notion-mcp) | Markdown-first Notion for AI agents -- pages, databases, blocks, and comments... | MCP |\n| [better-semantic-release](https://github.com/n24q02m/better-semantic-release) | Drop-in python-semantic-release fork with built-in release-safety guards (orp... | Tooling |\n| [better-telegram-mcp](https://github.com/n24q02m/better-telegram-mcp) | Telegram for AI agents -- messages, chats, media, and contacts across both bo... | MCP |\n| [better-workspace-mcp](https://github.com/n24q02m/better-workspace-mcp) | Google Workspace MCP server (Docs/Drive/Calendar/Gmail/Sheets/Slides/Tasks/Ch... | MCP |\n| [claude-plugins](https://github.com/n24q02m/claude-plugins) | Claude Code plugin marketplace for the n24q02m MCP servers -- install web sea... | Marketplace |\n| [imagine-mcp](https://github.com/n24q02m/imagine-mcp) | Image and video understanding + generation for AI agents -- across Gemini, Op... | MCP |\n| [jules-task-archiver](https://github.com/n24q02m/jules-task-archiver) | Chrome Extension for bulk operations on Jules tasks via batchexecute API -- a... | Tooling |\n| [mcp-core](https://github.com/n24q02m/mcp-core) | Shared foundation for building MCP servers -- Streamable HTTP transport, OAut... | MCP |\n| [mnemo-mcp](https://github.com/n24q02m/mnemo-mcp) | Persistent AI memory with hybrid search and embedded sync. Open, free, unlimi... | MCP |\n| [qwen3-embed](https://github.com/n24q02m/qwen3-embed) | Lightweight Qwen3 text embedding and reranking via ONNX Runtime and GGUF | Library |\n| [skret](https://github.com/n24q02m/skret) | Secrets without the server. | CLI |\n| [tacet](https://github.com/n24q02m/tacet) | A self-distilling neuro-symbolic cascade that amortises LLM cost across knowl... | Tooling |\n| [web-core](https://github.com/n24q02m/web-core) | Shared web infrastructure package for search, scraping, HTTP security, and st... | Library |\n| [wet-mcp](https://github.com/n24q02m/wet-mcp) | Open-source MCP server for AI agents: web search, content extraction, and lib... | MCP |\n\n</details>\n<!-- END: AUTO-GENERATED-CROSS-PROMO -->\n\n## Table of contents\n\n- [Install](#install)\n- [Remote (HTTP mode)](#remote-http-mode)\n- [Tools](#tools)\n- [Quick start](#quick-start)\n- [Multi-account](#multi-account)\n- [Documentation](#documentation)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Install\n\nThe server runs in two modes: **stdio** (default, single-user, one Google OAuth\nclient of your own) and **HTTP** (multi-user, OAuth 2.1 delegated to Google). For\nstdio, add it to your MCP client config:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"better-workspace\": {\n      \"command\": \"npx\",\n      \"args\": [\"--yes\", \"@n24q02m/better-workspace-mcp@latest\"],\n      \"env\": {\n        \"GOOGLE_OAUTH_CLIENT_ID\": \"<your-client-id>.apps.googleusercontent.com\",\n        \"GOOGLE_OAUTH_CLIENT_SECRET\": \"<your-client-secret>\"\n      }\n    }\n  }\n}\n```\n\nThose two values come from an OAuth 2.0 client of type **Desktop app**, created in\nthe [Google Cloud Console](https://console.cloud.google.com/apis/credentials)\nunder APIs & Services → Credentials. Desktop is the right type because the server\nreceives the consent redirect on a\n[loopback address](https://developers.google.com/identity/protocols/oauth2/native-app),\nnot on a public URL. Enable the Workspace APIs you plan to call on the same\nproject, and add yourself as a test user while the consent screen is unpublished.\n\nThe first run opens the Google consent screen in your browser; the refresh token\nis stored encrypted on your machine, so later runs start without asking again.\n\nThe same server can also serve several people over HTTP -- see\n[Remote (HTTP mode)](#remote-http-mode).\n\n## Remote (HTTP mode)\n\nBesides stdio, the server runs as a multi-user HTTP service. Authentication is\nOAuth 2.1 delegated to Google, and each user's Google credentials are kept in\ntheir own bucket keyed by their JWT `sub`, so one deployment serves several\npeople without them sharing an account.\n\nPoint your MCP client at the host you deployed it on:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"better-workspace\": {\n      \"type\": \"http\",\n      \"url\": \"https://<your-host>/mcp\"\n    }\n  }\n}\n```\n\n### Modes\n\nTwo, and only two. There is no proxy or daemon mode: stdio speaks the MCP stdio\ntransport directly, with no HTTP hop inside it.\n\n| Mode | Selected by | Serves | Google OAuth client |\n| --- | --- | --- | --- |\n| `stdio` | the default | one user, on their own machine | **Desktop app** |\n| `http` | `--http`, `MCP_TRANSPORT=http`, or `TRANSPORT_MODE=http` | several users, one credential bucket per JWT `sub` | **Web application** |\n\n### Two OAuth clients, not one\n\nGoogle binds redirect URIs to the *type* of the OAuth client, so the two modes\ncannot share one:\n\n- A **Desktop** client may redirect to any loopback port. That is what stdio\n  needs -- it stands a throwaway consent server on a random port.\n- A **Web** client may only redirect to URIs registered in advance. That is what\n  a deployment needs, because a public host has no loopback to come back to.\n\nRegister both of these on the Web client, or the flows that use them fail with\n`redirect_uri_mismatch`:\n\n| Redirect URI | Used by |\n| --- | --- |\n| `<PUBLIC_URL>/callback` | signing in to the server itself (delegated OAuth) |\n| `<PUBLIC_URL>/accounts/callback` | `config(action=\"account_add\")` -- adding a second Google account |\n\nKeep the two client credentials under separate names. Pointing\n`GOOGLE_OAUTH_CLIENT_ID`/`_SECRET` at the Web client on a machine that also runs\nstdio breaks every stdio install, which is why the Cloudflare deploy carries the\nWeb pair as `GOOGLE_OAUTH_WEB_CLIENT_ID`/`_SECRET` and renames it on the way into\nthe container (`src/worker.ts`).\n\n### Environment\n\n| Variable | Required | What it does |\n| --- | --- | --- |\n| `GOOGLE_OAUTH_CLIENT_ID` | yes | Web client id. The server refuses to start without it. |\n| `GOOGLE_OAUTH_CLIENT_SECRET` | yes | Web client secret. Same. |\n| `CREDENTIAL_SECRET` | yes | Derives each subject's credential-encryption key, and the JWT signing key. Without it that signing key would land on a container filesystem that does not survive a restart, so the server refuses to start rather than lose everyone's session on the next deploy. |\n| `PUBLIC_URL` | in practice | The exact public origin, e.g. `https://<your-host>`. It is what the redirect URIs above are built from; unset, the server falls back to the `Host` header and the redirect stops matching what Google has registered. |\n| `MCP_RELAY_PASSWORD` | recommended | One shared password gating `/authorize` behind a login page. Empty disables the gate, which leaves anyone who can reach the host able to start an OAuth flow against your deployment. |\n| `PORT` | no | Listen port. `0` (the default) asks the OS for a free one. |\n| `HOST` | no | Bind address. Defaults to loopback, so a container needs `0.0.0.0`. |\n| `MCP_STORAGE_BACKEND` / `MCP_KV_BASE_URL` | Cloudflare only | Set to `cf-kv` and the worker's internal KV URL when running as a Cloudflare Worker + Container. Left unset, credentials are stored on local disk. |\n\n`docker-compose.http.yml` in this repo is these variables written out as a\nrunnable overlay.\n\n## Tools\n\nOne composite tool per Workspace domain, plus `config` and `help`:\n\n| Tool | What it covers |\n| --- | --- |\n| `docs` | Google Docs -- `getText`, `create`, `writeText`, `getSuggestions`, `replaceText`, `formatText` |\n| `drive` | Files and folders -- `search`, `findFolder`, `createFolder`, `moveFile`, `renameFile`, `trashFile`, `downloadFile`, `getComments` |\n| `calendar` | Events -- `listCalendars`, `listEvents`, `getEvent`, `createEvent`, `updateEvent`, `deleteEvent`, `respondToEvent`, `findFreeTime` |\n| `gmail` | Mail -- `search`, `get`, `send`, `createDraft`, `sendDraft`, `modify`, `batchModify`, `modifyThread`, `downloadAttachment`, `listLabels`, `createLabel` |\n| `sheets` | Spreadsheets, read-only -- `getText`, `getRange`, `getMetadata` |\n| `slides` | Presentations -- 19 actions covering slides, text, shapes, images, tables, and speaker notes |\n| `tasks` | Task lists and tasks -- `listTaskLists`, `listTasks`, `createTask`, `updateTask`, `completeTask`, `deleteTask` |\n| `chat` | Google Chat -- `listSpaces`, `findSpaceByName`, `setUpSpace`, `getMessages`, `listThreads`, `sendMessage`, `sendDm`, `findDmByEmail` |\n| `people` | Profile lookups -- `getMe`, `getUserProfile`, `getUserRelations` |\n| `forms` | Forms -- `create`, `get`, `batchUpdate`, `listResponses`, `getResponse` |\n| `time` | Local date/time/timezone helpers (no Google account needed) |\n| `config` | Credential state and account management |\n| `help` | Full documentation for any tool |\n\nQuestions are added to a form with `forms(action=\"batchUpdate\")`, not at `create`;\nresponses are read-only, because the Forms API cannot write one. Listing or\ndeleting forms goes through `drive`.\n\n## Quick start\n\nCheck that the credentials landed, with the `config` tool. Before the first\nconsent this reports `awaiting_setup`; afterwards it names the account the server\nis acting as:\n\n```json\n{ \"action\": \"status\" }\n```\n\nThen call a domain tool. On `docs`, `create` returns the new document's ID, which\n`getText` reads back:\n\n```json\n{ \"action\": \"create\", \"title\": \"Notes\", \"content\": \"First line.\" }\n{ \"action\": \"getText\", \"documentId\": \"<id-from-create>\" }\n```\n\n`time`, `config`, and `help` need no Google account, so they answer even before\nconsent -- `time` is the quickest check that the server is wired up at all:\n\n```json\n{ \"action\": \"getCurrentTime\" }\n```\n\nTo act as a second Google account, see [Multi-account](#multi-account).\n\n## Multi-account\n\nEvery domain tool takes an `account` parameter -- the email of the Google account\nthe call acts as. Omit it and the call runs against the primary account.\n\n```json\n{ \"action\": \"search\", \"query\": \"is:unread\", \"account\": \"work@example.com\" }\n{ \"action\": \"search\", \"query\": \"is:unread\" }\n```\n\nThose two `gmail` calls read two different mailboxes: the first `work@example.com`,\nthe second whichever account is primary.\n\nAccounts are managed through the `config` tool:\n\n| Call | Effect |\n| --- | --- |\n| `config(action=\"account_add\")` | Returns a URL to open; completing the Google consent there adds one more account. |\n| `config(action=\"account_list\")` | The configured accounts and which one is primary. |\n| `config(action=\"account_remove\", account=\"<email>\")` | Forget one account. |\n| `config(action=\"account_set_default\", account=\"<email>\")` | Make one account the primary. |\n\nThe first account authorized becomes the primary. Removing the primary promotes\none of the remaining accounts; removing the last one puts the server back to\nawaiting setup. Naming an account that is not configured is an error that names\nit -- the call is never rerouted to the primary, because a silent fallback would\nact on the wrong mailbox.\n\n`account_add` works in both transports and chooses the flow itself: a temporary\nloopback consent server in stdio, and a fixed `/accounts/callback` on the running\nserver over HTTP, since a Web OAuth client's redirect URI must be registered in\nadvance. The HTTP link is single-use and expires in 10 minutes.\n\n`value=\"primary\"` is stdio only. Over HTTP that request would have to ride the\nURL through Google, where anyone who obtained it could aim your default account\nat one of theirs -- so remote callers change the default with\n`account_set_default` instead, from inside an authenticated call.\n\n### Coming from an earlier single-account build\n\nCredentials stored by a build from before multi-account support are one flat blob\nof tokens. The first run afterwards adopts that blob into the multi-account layout\nunder the account's email. The email comes from the stored `id_token` when it is\npresent -- no network needed, and that is the usual case. Otherwise the server asks\nGoogle's userinfo endpoint, which needs network access and a token that is still\nvalid or refreshable.\n\nIf neither works, the server reports itself as awaiting setup and opens the browser\nOAuth flow even though the stored token may still be fine. Completing that consent\nworks, and nothing is discarded: the account you just authorized is stored and\nbecomes the primary, while the old tokens are carried across under the key\n`(unidentified)` rather than being overwritten. `config(action=\"account_list\")` then\nshows two entries:\n\n```json\n{ \"accounts\": [\"(unidentified)\", \"you@example.com\"], \"primary\": \"you@example.com\" }\n```\n\nNothing routes to `(unidentified)`: it is never promoted to primary while a real\naccount remains, and no call reaches it unless you name it explicitly. It is there\nso a credential whose owner could not be determined is not silently thrown away.\nRemove it once the re-authorized account is working:\n\n```json\n{ \"action\": \"account_remove\", \"account\": \"(unidentified)\" }\n```\n\nAfter a successful adoption -- the usual case -- `config(action=\"account_list\")`\nshows exactly one account, and it is the primary.\n\n### Forms scopes and re-consent\n\nThe consent screen has requested the Google Forms scopes since before the `forms`\ntool existed, so it arrived without a second trip through consent.\n\nAccounts authorized *before* those scopes were added are covered too, but by a\ndifferent route: Google accepts `https://www.googleapis.com/auth/drive` in place of\nthe Forms scopes for every Forms method this server calls, and full `drive` has\nbeen on the consent screen since the first release. Google never widens a token it\nhas already issued, so if a grant covers neither -- a user may withhold individual\nrestricted scopes at the consent screen -- the first `forms` call returns a 403\n(`Request had insufficient authentication scopes`). Re-authorize just that account\nwith `config(action=\"account_add\")`, signing in as the same account; the record is\nreplaced in place, so nothing else changes.\n\n## Documentation\n\nDocs for the whole MCP server stack are at\n**[mcp.n24q02m.com](https://mcp.n24q02m.com)**. A page dedicated to this server is\nnot published yet; until it is, the two references that apply here are:\n\n- [Modes overview](https://mcp.n24q02m.com/get-started/modes-overview/) -- stdio (default) and HTTP (multi-user, OAuth 2.1)\n- [Multi-user setup](https://mcp.n24q02m.com/get-started/multi-user/) -- the per-JWT-`sub` credential model behind [Remote (HTTP mode)](#remote-http-mode)\n\nEvery tool also documents itself at runtime: call `help` for the full reference on\nany of them, including the exact parameters each action takes.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## License\n\n[Apache-2.0](LICENSE) © n24q02m\n",
  "bytes": 17133,
  "sha": "e353b440675f969d3d6ae589a0bc9c1e6029931cad3fb277f3f9ba82c211c015",
  "repo_slug": "n24q02m/better-workspace-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_n24q02m_better_workspace_mcp_4efef1c4/readme"
}