AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified Apache-2.0 Self-run

Better Workspace Mcp

mcp-n24q02m-better-workspace-mcp · by n24q02m

Google Workspace MCP server (Docs/Drive/Calendar/Gmail/Sheets/Slides/Tasks/Chat/People + multi-account) — vendored gemini-cli workspace-server + mcp-core auth

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add mcp-n24q02m-better-workspace-mcp

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-n24q02m-better-workspace-mcp)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Better Workspace Mcp? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

better-workspace-mcp

Google Workspace MCP — Docs, Drive, Calendar, Gmail, Sheets, Slides, Tasks, Chat, People, Forms

Docs · Install · Quick start · Community

Sister projects from n24q02m (click to expand)

| Project | Tagline | Tag | |---|---|---| | agent-chat-plugin | Peer AI agents chat in a shared folder — no human relay, no orchestrator, wor... | Tooling | | better-code-review-graph | Knowledge graph for token-efficient code reviews -- semantic search and call-... | MCP | | better-drive | 2-way Google Drive sync with .driveignore filter — rclone engine, Windows tray | Tooling | | better-email-mcp | IMAP/SMTP email for AI agents -- read, send, organize folders, and manage att... | MCP | | better-godot-mcp | Composite MCP server for Godot Engine -- 17 composite tools for AI-assisted g... | MCP | | better-notion-mcp | Markdown-first Notion for AI agents -- pages, databases, blocks, and comments... | MCP | | better-semantic-release | Drop-in python-semantic-release fork with built-in release-safety guards (orp... | Tooling | | better-telegram-mcp | Telegram for AI agents -- messages, chats, media, and contacts across both bo... | MCP | | better-workspace-mcp | Google Workspace MCP server (Docs/Drive/Calendar/Gmail/Sheets/Slides/Tasks/Ch... | MCP | | claude-plugins | Claude Code plugin marketplace for the n24q02m MCP servers -- install web sea... | Marketplace | | imagine-mcp | Image and video understanding + generation for AI agents -- across Gemini, Op... | MCP | | jules-task-archiver | Chrome Extension for bulk operations on Jules tasks via batchexecute API -- a... | Tooling | | mcp-core | Shared foundation for building MCP servers -- Streamable HTTP transport, OAut... | MCP | | mnemo-mcp | Persistent AI memory with hybrid search and embedded sync. Open, free, unlimi... | MCP | | qwen3-embed | Lightweight Qwen3 text embedding and reranking via ONNX Runtime and GGUF | Library | | skret | Secrets without the server. | CLI | | tacet | A self-distilling neuro-symbolic cascade that amortises LLM cost across knowl... | Tooling | | web-core | Shared web infrastructure package for search, scraping, HTTP security, and st... | Library | | wet-mcp | Open-source MCP server for AI agents: web search, content extraction, and lib... | MCP |

Table of contents

  • [Install](#install)
  • [Remote (HTTP mode)](#remote-http-mode)
  • [Tools](#tools)
  • [Quick start](#quick-start)
  • [Multi-account](#multi-account)
  • [Documentation](#documentation)
  • [Contributing](#contributing)
  • [License](#license)

Install

The server runs in two modes: stdio (default, single-user, one Google OAuth client of your own) and HTTP (multi-user, OAuth 2.1 delegated to Google). For stdio, add it to your MCP client config:

{
  "mcpServers": {
    "better-workspace": {
      "command": "npx",
      "args": ["--yes", "@n24q02m/better-workspace-mcp@latest"],
      "env": {
        "GOOGLE_OAUTH_CLIENT_ID": ".apps.googleusercontent.com",
        "GOOGLE_OAUTH_CLIENT_SECRET": ""
      }
    }
  }
}

Those two values come from an OAuth 2.0 client of type Desktop app, created in the Google Cloud Console under APIs & Services → Credentials. Desktop is the right type because the server receives the consent redirect on a loopback address, not on a public URL. Enable the Workspace APIs you plan to call on the same project, and add yourself as a test user while the consent screen is unpublished.

The first run opens the Google consent screen in your browser; the refresh token is stored encrypted on your machine, so later runs start without asking again.

The same server can also serve several people over HTTP -- see [Remote (HTTP mode)](#remote-http-mode).

Remote (HTTP mode)

Besides stdio, the server runs as a multi-user HTTP service. Authentication is OAuth 2.1 delegated to Google, and each user's Google credentials are kept in their own bucket keyed by their JWT sub, so one deployment serves several people without them sharing an account.

Point your MCP client at the host you deployed it on:

{
  "mcpServers": {
    "better-workspace": {
      "type": "http",
      "url": "https:///mcp"
    }
  }
}

Modes

Two, and only two. There is no proxy or daemon mode: stdio speaks the MCP stdio transport directly, with no HTTP hop inside it.

| Mode | Selected by | Serves | Google OAuth client | | --- | --- | --- | --- | | stdio | the default | one user, on their own machine | Desktop app | | http | --http, MCP_TRANSPORT=http, or TRANSPORT_MODE=http | several users, one credential bucket per JWT sub | Web application |

Two OAuth clients, not one

Google binds redirect URIs to the type of the OAuth client, so the two modes cannot share one:

  • A Desktop client may redirect to any loopback port. That is what stdio

needs -- it stands a throwaway consent server on a random port.

  • A Web client may only redirect to URIs registered in advance. That is what

a deployment needs, because a public host has no loopback to come back to.

Register both of these on the Web client, or the flows that use them fail with redirect_uri_mismatch:

| Redirect URI | Used by | | --- | --- | | /callback | signing in to the server itself (delegated OAuth) | | /accounts/callback | config(action="account_add") -- adding a second Google account |

Keep the two client credentials under separate names. Pointing GOOGLE_OAUTH_CLIENT_ID/_SECRET at the Web client on a machine that also runs stdio breaks every stdio install, which is why the Cloudflare deploy carries the Web pair as GOOGLE_OAUTH_WEB_CLIENT_ID/_SECRET and renames it on the way into the container (src/worker.ts).

Environment

| Variable | Required | What it does | | --- | --- | --- | | GOOGLE_OAUTH_CLIENT_ID | yes | Web client id. The server refuses to start without it. | | GOOGLE_OAUTH_CLIENT_SECRET | yes | Web client secret. Same. | | 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. | | PUBLIC_URL | in practice | The exact public origin, e.g. https://. 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. | | 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. | | PORT | no | Listen port. 0 (the default) asks the OS for a free one. | | HOST | no | Bind address. Defaults to loopback, so a container needs 0.0.0.0. | | 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. |

docker-compose.http.yml in this repo is these variables written out as a runnable overlay.

Tools

One composite tool per Workspace domain, plus config and help:

| Tool | What it covers | | --- | --- | | docs | Google Docs -- getText, create, writeText, getSuggestions, replaceText, formatText | | drive | Files and folders -- search, findFolder, createFolder, moveFile, renameFile, trashFile, downloadFile, getComments | | calendar | Events -- listCalendars, listEvents, getEvent, createEvent, updateEvent, deleteEvent, respondToEvent, findFreeTime | | gmail | Mail -- search, get, send, createDraft, sendDraft, modify, batchModify, modifyThread, downloadAttachment, listLabels, createLabel | | sheets | Spreadsheets, read-only -- getText, getRange, getMetadata | | slides | Presentations -- 19 actions covering slides, text, shapes, images, tables, and speaker notes | | tasks | Task lists and tasks -- listTaskLists, listTasks, createTask, updateTask, completeTask, deleteTask | | chat | Google Chat -- listSpaces, findSpaceByName, setUpSpace, getMessages, listThreads, sendMessage, sendDm, findDmByEmail | | people | Profile lookups -- getMe, getUserProfile, getUserRelations | | forms | Forms -- create, get, batchUpdate, listResponses, getResponse | | time | Local date/time/timezone helpers (no Google account needed) | | config | Credential state and account management | | help | Full documentation for any tool |

Questions are added to a form with forms(action="batchUpdate"), not at create; responses are read-only, because the Forms API cannot write one. Listing or deleting forms goes through drive.

Quick start

Check that the credentials landed, with the config tool. Before the first consent this reports awaiting_setup; afterwards it names the account the server is acting as:

{ "action": "status" }

Then call a domain tool. On docs, create returns the new document's ID, which getText reads back:

{ "action": "create", "title": "Notes", "content": "First line." }
{ "action": "getText", "documentId": "" }

time, config, and help need no Google account, so they answer even before consent -- time is the quickest check that the server is wired up at all:

{ "action": "getCurrentTime" }

To act as a second Google account, see [Multi-account](#multi-account).

Multi-account

Every domain tool takes an account parameter -- the email of the Google account the call acts as. Omit it and the call runs against the primary account.

{ "action": "search", "query": "is:unread", "account": "work@example.com" }
{ "action": "search", "query": "is:unread" }

Those two gmail calls read two different mailboxes: the first work@example.com, the second whichever account is primary.

Accounts are managed through the config tool:

| Call | Effect | | --- | --- | | config(action="account_add") | Returns a URL to open; completing the Google consent there adds one more account. | | config(action="account_list") | The configured accounts and which one is primary. | | config(action="account_remove", account="") | Forget one account. | | config(action="account_set_default", account="") | Make one account the primary. |

The first account authorized becomes the primary. Removing the primary promotes one of the remaining accounts; removing the last one puts the server back to awaiting setup. Naming an account that is not configured is an error that names it -- the call is never rerouted to the primary, because a silent fallback would act on the wrong mailbox.

account_add works in both transports and chooses the flow itself: a temporary loopback consent server in stdio, and a fixed /accounts/callback on the running server over HTTP, since a Web OAuth client's redirect URI must be registered in advance. The HTTP link is single-use and expires in 10 minutes.

value="primary" is stdio only. Over HTTP that request would have to ride the URL through Google, where anyone who obtained it could aim your default account at one of theirs -- so remote callers change the default with account_set_default instead, from inside an authenticated call.

Coming from an earlier single-account build

Credentials stored by a build from before multi-account support are one flat blob of tokens. The first run afterwards adopts that blob into the multi-account layout under the account's email. The email comes from the stored id_token when it is present -- no network needed, and that is the usual case. Otherwise the server asks Google's userinfo endpoint, which needs network access and a token that is still valid or refreshable.

If neither works, the server reports itself as awaiting setup and opens the browser OAuth flow even though the stored token may still be fine. Completing that consent works, and nothing is discarded: the account you just authorized is stored and becomes the primary, while the old tokens are carried across under the key (unidentified) rather than being overwritten. config(action="account_list") then shows two entries:

{ "accounts": ["(unidentified)", "you@example.com"], "primary": "you@example.com" }

Nothing routes to (unidentified): it is never promoted to primary while a real account remains, and no call reaches it unless you name it explicitly. It is there so a credential whose owner could not be determined is not silently thrown away. Remove it once the re-authorized account is working:

{ "action": "account_remove", "account": "(unidentified)" }

After a successful adoption -- the usual case -- config(action="account_list") shows exactly one account, and it is the primary.

Forms scopes and re-consent

The consent screen has requested the Google Forms scopes since before the forms tool existed, so it arrived without a second trip through consent.

Accounts authorized before those scopes were added are covered too, but by a different route: Google accepts https://www.googleapis.com/auth/drive in place of the Forms scopes for every Forms method this server calls, and full drive has been on the consent screen since the first release. Google never widens a token it has already issued, so if a grant covers neither -- a user may withhold individual restricted scopes at the consent screen -- the first forms call returns a 403 (Request had insufficient authentication scopes). Re-authorize just that account with config(action="account_add"), signing in as the same account; the record is replaced in place, so nothing else changes.

Documentation

Docs for the whole MCP server stack are at mcp.n24q02m.com. A page dedicated to this server is not published yet; until it is, the two references that apply here are:

  • Modes overview -- stdio (default) and HTTP (multi-user, OAuth 2.1)
  • Multi-user setup -- the per-JWT-sub credential model behind [Remote (HTTP mode)](#remote-http-mode)

Every tool also documents itself at runtime: call help for the full reference on any of them, including the exact parameters each action takes.

Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

License

[Apache-2.0](LICENSE) © n24q02m

Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.