Skip to content
TwilightCodersPublic

About

Google Workspace MCP server (Gmail, Calendar, Drive, Voice) with per-capability scoping

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

GMCP

Google Workspace MCP server (Gmail, Calendar, Drive) for Claude Code.

Built on him (REST ORM), the official Ruby MCP SDK, and Google's OAuth2 library (googleauth).

Why

The built-in claude.ai Gmail/Calendar connectors have limited OAuth scopes — no trash, no labels, no send, no Drive access. GMCP runs as a local stdio MCP server with full gmail.modify, calendar.events, calendar.calendarlist.readonly and drive.readonly scopes.

Tools

Gmail (9 tools) gmail_search, gmail_get_message, gmail_list_labels, gmail_trash_message, gmail_archive_message, gmail_label_message, gmail_send, gmail_create_draft, gmail_reply

Calendar (7 tools) calendar_list_calendars, calendar_list_events, calendar_get_event, calendar_create_event, calendar_update_event, calendar_delete_event, calendar_rsvp

Drive (3 tools) drive_search, drive_list_folder, drive_read_file

Setup

1. Google Cloud project

  1. Go to https://console.cloud.google.com/
  2. Create a project (or use an existing one)
  3. Enable the Gmail, Google Calendar, and Google Drive APIs
  4. Under APIs & Services → Credentials, create an OAuth 2.0 Client ID
    • Application type: Desktop app
  5. Download the JSON file and save it to ~/.config/gmcp/credentials.json
  6. Under Google Auth Platform → Audience, click Publish app

Step 6 is not optional for day-to-day use. While the consent screen is in Testing, Google expires every refresh token after 7 days, so each account silently stops working about a week after you authorize it and the server reports invalid_grant. Publishing to In production removes that expiry.

Publishing is not the same as verification. An unverified production app still shows a "Google hasn't verified this app" interstitial (Advanced → Go to …) on each authorization, and is capped at 100 users. Full verification of the restricted gmail.modify scope requires an annual third-party security assessment, which is not worth doing for a personal install — accept the interstitial.

2. Install

gem install gmcp

Or from source:

git clone https://github.com/TwilightCoders/GMCP ~/.gmcp
cd ~/.gmcp && bundle install

3. Wire up Claude Code

MCP servers are configured in .claude.json, not settings.json. Let the CLI write it so it lands in the config directory your sessions actually read ($CLAUDE_CONFIG_DIR/.claude.json when that is set, ~/.claude.json otherwise):

GMCP=~/.gmcp   # wherever you cloned it

claude mcp add gmcp --scope user \
  --env "GMCP_ACCOUNTS=you@example.com,work@example.com" \
  --env "GMCP_CAPABILITIES=gmcp.authorize,gmail.read,calendar.read,drive.read" \
  --env "BUNDLE_GEMFILE=$GMCP/Gemfile" \
  -- "$(which ruby)" "$GMCP/bin/gmcp"

--scope user registers it for every session on the machine; --scope project writes a .mcp.json next to a single repo, which is how you give one project a wider grant than the default (see GMCP_CAPABILITIES below).

If you use a Ruby version manager, point the command at its shim rather than a versioned binary, so a Ruby upgrade does not silently break every session. With rbenv, add --env "RBENV_DIR=$GMCP" and the shim resolves the version from GMCP's own .ruby-version, regardless of which directory the session runs in.

Verify with claude mcp get gmcp. On startup the server prints its accounts and capabilities to stderr — that stderr is the only place an operator can see what a given process was actually granted.

4. Authorize from Claude

Ask Claude to call gmcp_authorize, or run bin/gmcp-auth <account> from a terminal. Either opens your browser and captures the redirect on a temporary local callback server — there is nothing to copy and paste. (Google retired the out-of-band code flow in 2022.)

Tokens are stored owner-only at ~/.config/gmcp/<account>/token.yaml, and access tokens refresh automatically while the server runs. If a refresh token is revoked, the server still starts — the account is left unbound and reports that it needs re-authorizing, rather than taking the whole server down.

Multiple accounts and capability scoping

GMCP is scoped along two independent axes. Both are environment variables read once at process start, so a given bin/gmcp process has a fixed, inspectable reach — it prints both to stderr on startup.

GMCP_ACCOUNTS — which mailboxes

Comma-separated. Each account keeps its own OAuth token at ~/.config/gmcp/<account>/token.yaml, and every Gmail/Calendar/Drive tool takes an optional account: argument selecting among them (defaulting to the first).

GMCP_ACCOUNTS=personal@example.com,work@example.com bin/gmcp

Authorize each one separately with gmcp_authorize(account: "...").

GMCP_CAPABILITIES — which verbs

Comma-separated capability names from config/capabilities.yml. Capabilities gate registration: a tool whose capability was not granted is never defined on the MCP server, so it never appears in tools/list and cannot be called no matter what the client sends.

# a read-only assistant
GMCP_CAPABILITIES=gmail.read,calendar.read bin/gmcp

# one that may also send
GMCP_CAPABILITIES=gmail.read,gmail.send bin/gmcp
Capability Grants
gmcp.authorize Start the OAuth flow (opens a browser)
gmail.read Search (paginated), read, list labels, list attachments, inspect unsubscribe options
gmail.modify_labels Add/remove labels and archive, single or batched (archiving is removing INBOX)
gmail.trash Trash a message, single or batched
gmail.send Send, draft, reply
gmail.download Write attachment bytes to a local directory
gmail.unsubscribe RFC 8058 one-click unsubscribe — leaves Google, tells a third party the address is live
calendar.read List calendars, read events
calendar.write Create, update, RSVP
calendar.delete Delete an event
drive.read Search, list folders, read files
voice.read List/search Voice; report an account's Voice number
voice.modify Mark a conversation read

config/capabilities.yml is the source of truth; a spec asserts it matches the tools actually registered, so it cannot silently drift.

Bulk work

gmail_search returns one line per message (id, date, sender, subject) and paginates. It returns a page_token whenever more results exist; pass it back to walk a whole mailbox. calendar_list_events, drive_search and drive_list_folder page the same way. In Ruby, Message.each_page handles the cursor for you and is bounded by max_pages so a runaway cursor cannot spin.

GMCP::Gmail::Message.each_page('in:inbox', max_results: 500) do |page|
  page[:messages].each { |m| ... }
end

Acting on the result is batched — gmail_batch_archive, gmail_batch_trash, and gmail_batch_modify take up to 1000 ids in one request rather than one round trip per message. Gmail returns nothing per-message, so a partial failure is not distinguishable; re-read if you need per-message confirmation.

Attachments are two steps: gmail_list_attachments (under gmail.read) gives you filenames and part_ids, gmail_download_attachment (under gmail.download) writes one to a directory you name. There is no default destination, the filename is reduced to a basename so it cannot escape that directory, and an existing file is never overwritten.

Contract: unset and empty mean opposite things

This distinction is the whole security boundary. It is not a convenience.

Value Meaning
GMCP_CAPABILITIES not set Every capability. For a human running bin/gmcp by hand with no grant system in front of it.
GMCP_CAPABILITIES="" No capabilities. The explicit empty grant set for an identity that was granted nothing.

An automated launcher must always set the variable explicitly, including when the grant set is empty. Omitting the key because there are no grants fails open and hands the process everything. There is no wildcard value, and unknown entries are dropped rather than trusted, so a grant cannot widen itself by typo.

Account isolation

Models are bound to a resolver, not to a concrete API: him calls use_api at request time if it responds to :call, so GMCP::ApiBinding installs a lambda once and swaps the account per fiber via Thread.current. Server.with_account scopes a binding to a block and restores whatever was bound before, so two concurrent tool calls for different accounts cannot see each other's APIs, and a nested call cannot leak its account to its caller.

A request with nothing bound raises ApiBinding::NotBound rather than falling back to whichever account the previous caller happened to leave behind.

This was not always true. Until 0.3.0 the account was written onto the model class itself, so isolation depended on never running two requests at once — which is not isolation, only an absence of opportunity. Per-process separation was the recommendation because it was the only thing that actually held.

Capability separation is still per-process, because GMCP_CAPABILITIES is read once at startup and decides which tools get registered at all. One process per capability set; that process may now serve several accounts safely.

Connectors and account scoping

GMCP models gmail, google_calendar, drive, and voice as separate connectors, served by one process. Each declares in config/capabilities.yml whether GMCP_ACCOUNTS constrains it and where its credential lives:

Connector account_scoping credential_source GMCP_ACCOUNTS constrains it?
gmcp enforced local_file yes
gmail enforced local_file yes
google_calendar enforced local_file yes
drive enforced local_file yes
voice enforced delegated (Chrome) yes

Voice has no usable OAuth path — the token-to-cookie exchange is reserved for Chromium — so GMCP::Voice::Session uses the Google session of the Chrome profile signed in as the requested account, found by email in Chrome's profile list. Voice tools take account: like the rest and are limited to GMCP_ACCOUNTS; an account with no Chrome profile has no Voice. Chrome keeps the session fresh as you use it, so there is no separate sign-in.

Chrome encrypts its cookies with a key in the macOS Keychain ("Chrome Safe Storage"). The first Voice call asks for access; choose Always Allow. Voice is macOS-only for that reason.

Development

bundle install
bundle exec rake spec

Ruby 3.4.9+ required.

License

MIT. See LICENSE.txt.

About

Google Workspace MCP server (Gmail, Calendar, Drive, Voice) with per-capability scoping

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages