mcpbeat

Google Workspace

google/google-workspace

How to read and write Google Drive, Docs, Sheets, Gmail, Calendar, Chat, Tasks, Slides, Keep, Forms, Apps Script, and Meet via the `gws` CLI (a community Google Workspace CLI, not an official Google product). `gws` wraps the Workspace REST APIs (pagination, retries, JSON parsing) and the agent shells out via `terminal`. Note that `gws` does NOT ship an OAuth client — an OAuth client or service account must be configured before interactive login will work (unless a pre-injected token is present). Use whenever the user asks for anything involving a Google Workspace surface.

5k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
10025
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/google/adk-samples --skill google-workspace

The instruction itself

14 sections, as written by the author

Google Workspace via gws

gws (install: npm install -g @googleworkspace/cli; this skill targets

0.22.x) speaks every Workspace API the user is likely to ask about —

Drive, Docs, Sheets, Gmail, Calendar, Chat, Tasks, Slides, Keep, Forms, Apps

Script, Meet. It is a community CLI ("not an

officially supported Google product"). It wraps the REST APIs so you don't

hand-roll curl, but it does not bundle credentials: you must configure

an OAuth client or service account first, then authenticate.

When to use this skill

Any user request that names a Google Workspace surface:

  • "read this Drive file", "list my Drive folder", "share this doc"
  • "create a Google Doc with…", "update this sheet", "append a row"
  • "send an email", "draft a Gmail reply", "check my inbox for…"
  • "what's on my calendar", "schedule a meeting"
  • "post to this Chat space"

Do NOT shell out to curl against the raw Workspace REST APIs — gws

already wraps them and handles pagination, retries, and JSON parsing.

Auth

First, always: try the pre-injected token

Before any OAuth client, service account, or login dance, check for

a pre-injected access token. When the user has used "Connect Workspace"

in the web UI, the GOOGLE_WORKSPACE_CLI_TOKEN secret is auto-injected into

every terminal command and is gws's highest-priority auth source — no

OAuth client, no login, no loopback bridge. **Don't ask, don't inspect the

environment, just probe with a cheap read against the surface you need:**

terminal(command="export GOOGLE_WORKSPACE_CLI_CONFIG_DIR=/workspace/lha/config/gws GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file && gws gmail messages list --params '{\"maxResults\": 1}'")
  • If it returns data, you're done — the token works. Proceed with the task.
  • If it 403s / scope error, the token is valid but the user didn't grant

this surface (or only read-only). The "Connect Workspace" flow is per-surface

(drive / gmail / calendar / sheets / docs / chat / tasks / slides / keep /

script / meet / forms) × read-only|read-write,

read-only by default — ask the user to reconnect with the surface (or

read-write) you need; don't start a login dance.

  • If it 401s / "not logged in", the token is truly missing or lapsed (it's

a ~1h token, no refresh) — *then* fall back to the OAuth-client login below, or

ask the user to re-click "Connect Workspace".

Do NOT trust gws auth status here. With the env token it reports

auth_method: none / credential_source: token_env_var even though reads

succeed — a real read is the only reliable check. Skipping the probe and

jumping to gws auth login is the mistake to avoid: it drags the user through

a browser-redirect bridge that the already-present token makes unnecessary.

Otherwise: configure an OAuth client

gws has no embedded OAuth client. gws auth login fails until one of

these is in place:

  • client_secret.json — drop an OAuth client secret file into the gws

config dir. Preferred in the sandbox — see "Creating the OAuth client"

below. Keep that dir on /workspace (next section) so it survives.

  • Env vars — set GOOGLE_WORKSPACE_CLI_CLIENT_ID and

GOOGLE_WORKSPACE_CLI_CLIENT_SECRET.

  • Service account — set GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json.

No gws auth login step is needed in this mode; calls authenticate

directly with the key (use for fully unattended automation, no user present).

  • gws auth setup — provisions a GCP project + OAuth client for you, but

renders a full-screen interactive TUI that can't be driven from

terminal (no keyboard input). Don't use it headless — create the

client_secret.json by hand (option 1) instead.

Creating the OAuth client (one-time)

When building the OAuth client yourself (via gcloud or the Cloud console):

  • Choose the "Desktop app" client type, not "Web application". `gws

auth login` listens on a random loopback port; a Web-app client rejects that

with Error 400: redirect_uri_mismatch. Desktop-app clients accept loopback.

  • For a corporate Workspace org, set the consent-screen audience to

Internal. An internal-audience client clears the Context-Aware Access /

"Account restricted" block that the default gcloud OAuth client trips on.

  • The client_secret.json's project_id must be a project the authenticating

account may use — otherwise calls fail with the quota-project 403 (see

Failure handling).

Config dir + keyring (sandbox gotchas)

Set both on every gws command — each terminal call is a fresh non-login

shell, so exports don't carry over between calls:

export GOOGLE_WORKSPACE_CLI_CONFIG_DIR=/workspace/lha/config/gws
export GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file
  • GOOGLE_WORKSPACE_CLI_CONFIG_DIR — relocates the config dir (which holds

client_secret.json, the token, and the encryption key) onto /workspace.

The default ~/.config/gws is not migrated on runtime upgrades, so without

this you'd redo the browser login after the next upgrade.

  • GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file — there's no OS keyring headless;

the file backend writes the key into the config dir (hence also on

/workspace).

Authenticating

With an OAuth client configured, authenticate with:

terminal(command="gws auth login --readonly")  # read-only scopes (start here)
terminal(command="gws auth login")             # all default scopes
terminal(command="gws auth login -s drive,gmail,sheets")  # limit the picker
terminal(command="gws auth login --scopes <comma,separated,scopes>")

Start with read-only scopes (--readonly) — least privilege. Add write

scopes only when the task actually needs to write, by re-running `gws auth

login --scopes <…>` for the specific surface; Google's incremental consent

merges the new scope with what's already granted, so you never over-grant

up front.

gws auth login is loopback-browser only — it opens a browser and waits on

a local listener for the OAuth redirect. There is NO device-code /

paste-a-code flag.

Completing the loopback flow headless. It *does* work in the sandbox — you

bridge the redirect by hand. The browser opens on the *user's* machine, but the

listener is inside the container, so you relay the redirect URL across. The URL

appears in one turn and the user pastes back in a later turn, so use the

cross-turn process pipeline (terminal(background=True) + process):

terminal(command="export GOOGLE_WORKSPACE_CLI_CONFIG_DIR=/workspace/lha/config/gws GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file && gws auth login", background=True)
# read the printed auth URL, surface it VERBATIM to the user, then wait
process(action='read', session_id=<id>)
  • The user opens the auth URL, consents, and their browser lands on a

http://localhost:<port>/?code=...&scope=... page that won't load (the

listener is in the sandbox, not on their machine). Ask them to copy that

full redirect URL from the address bar and paste it back.

  • curl that exact URL from inside the sandbox to hand the code to the waiting

listener, which completes the exchange and writes the token:

   terminal(command="curl -s 'http://localhost:<port>/?code=...&scope=...'")

The backgrounded gws auth login then finishes.

(localhost is on Layer A's allowlist, so this curl isn't gated.) For

fully unattended jobs where no user is present to paste the redirect, use a

service account instead — the loopback bridge needs a live user.

Check state any time with terminal(command="gws auth status").

Command form

gws <service> <resource> [sub-resource] <method> [flags]

Query parameters are passed as JSON via --params; request bodies via

--json. Wrap both in single quotes so the shell preserves inner double

quotes:

gws drive files list --params '{"pageSize": 5, "q": "trashed=false"}'
gws drive files create --json '{"name": "notes.txt"}' --upload notes.txt

Several common operations also have ergonomic helper commands (prefixed

+) that take plain flags instead of JSON — e.g. gws sheets +read,

gws sheets +append, gws gmail +send, gws drive +upload.

Useful global flags: --format json|table|yaml|csv (json default),

--dry-run (validate without hitting the API), --page-all (auto-paginate

to NDJSON), -o/--output <path> (save binary responses),

--sanitize <template> (screen responses through Model Armor).

Discovery — don't guess params

terminal(command="gws drive --help")                 # resources + methods
terminal(command="gws schema drive.files.list")      # params, types, defaults

gws schema <service>.<resource>.<method> prints the exact param shape for a

method. Run it (or gws <service> --help) whenever you're unsure of a flag —

it's authoritative for the installed version.

Prefer the official per-surface skills for depth

The tables below are a quick reference, not the full surface. gws ships

maintained per-surface skills upstreamgws-drive, gws-gmail,

gws-sheets, gws-calendar, gws-chat, gws-people, gws-slides,

gws-tasks, plus curated recipes — that cover the less-common methods this file

doesn't. For anything beyond the common operations, install the relevant skill

into the workspace and read it (note the path the command prints):

terminal(command="npx -y skills add https://github.com/googleworkspace/cli/tree/main/skills/gws-drive")

Swap gws-drive for the surface you need. These are version-pinned docs, so when

a skill and the installed binary disagree, gws schema <service>.<resource>.<method>

wins — it reflects the gws actually on PATH.

Common operations

| You want to… | Command |

|---|---|

| List Drive files | gws drive files list --params '{"q": "trashed=false", "pageSize": 20}' |

| Get Drive file metadata | gws drive files get --params '{"fileId": "ID", "fields": "id,name,mimeType"}' |

| Download file contents | gws drive files get --params '{"fileId": "ID", "alt": "media"}' -o out.bin |

| Export a Doc/Sheet | gws drive files export --params '{"fileId": "ID", "mimeType": "text/plain"}' -o out.txt |

| Read a Sheet range | gws sheets +read --spreadsheet ID --range "Sheet1!A1:D10" |

| Append a Sheet row | gws sheets +append --spreadsheet ID --range Sheet1 --json '{"values": [["a","b"]]}' |

| List Gmail messages | gws gmail messages list --params '{"q": "from:[email protected] newer_than:7d", "maxResults": 10}' |

| Get a Gmail message | gws gmail messages get --params '{"id": "MSG_ID", "format": "full"}' |

| Send mail | gws gmail +send --to [email protected] --subject 'Hi' --body 'Hello!' |

| List Calendar events | gws calendar events list --params '{"calendarId": "primary", "maxResults": 10}' |

| List Tasks task lists | gws tasks tasklists list |

| List tasks in a list | gws tasks tasks list --params '{"tasklist": "TASKLIST_ID"}' |

| Add a task | gws tasks tasks insert --params '{"tasklist": "TASKLIST_ID"}' --json '{"title": "Buy milk"}' |

| Read a presentation | gws slides presentations get --params '{"presentationId": "ID"}' |

| List Keep notes | gws keep notes list |

| Get a Form + its responses | gws forms forms get --params '{"formId": "ID"}' · gws forms forms responses list --params '{"formId": "ID"}' |

Confirm the exact params with gws schema … before write/delete calls — the

shapes above are the common cases, not an exhaustive contract.

Keep caveat: the Google Keep API is restricted to Workspace Enterprise

domains via service-account domain-wide delegation — an ordinary

Connect-Workspace access token will likely 403 on gws keep … regardless of

the granted scope. If it does, tell the user it's an API restriction, not a

missing grant.

**Under a routine, the Google token is present only if you declared its secret

name (GOOGLE_WORKSPACE_CLI_TOKEN) in the routine's secrets:.**

Google Chat — unread state & sender names

Two Chat limitations are scope-bound, so check before promising the user a

result:

  • **"Unread messages" needs an extra scope, but gws has a native command for

it.** The default read scopes (chat.spaces.readonly +

chat.messages.readonly) let you list spaces and read messages but do not

expose per-space unread state — read-position needs

chat.users.readstate.readonly. With that scope, don't hand-roll REST:

gws wraps the read-state endpoint as

gws chat users spaces getSpaceReadState. Compute unread per space yourself —

there's no single "list unread" call:

  • List the spaces you care about (DMs/group chats sort by lastActiveTime):
     gws chat spaces list --params '{"pageSize": 1000}'
  • For each space, read your last-read mark and its latest message, then

compare:

     gws chat users spaces getSpaceReadState --params '{"name": "users/me/spaces/<space>/spaceReadState"}'
     gws chat spaces messages list --params '{"parent": "spaces/<space>", "pageSize": 1, "orderBy": "create_time DESC"}'

A space is unread when the latest message's createTime is strictly

newer than the read state's lastReadTime and that message's

sender.name (a users/<id>) isn't you. (orderBy is create_time DESC

snake_case field + ASC/DESC, not createTime desc.)

  • Resolving your own users/<id> to skip self-authored messages is the one

snag: people/me 403s under the Connect-Workspace token, so pass your id in

explicitly (read it off any message you sent, or gws people people get

once) rather than relying on me.

  • Resolve the remaining sender ids to names with gws people people getBatchGet

(see the People note above).

  • Without the read-state scope you can't get true unread — be upfront, then

either re-auth to add it

(gws auth login --scopes https://www.googleapis.com/auth/chat.users.readstate.readonly;

incremental consent merges it) or fall back to recency as a proxy (list

spaces by lastActiveTime, pull each space's latest messages). Say which.

  • Senders come back as user IDs, not names. Chat returns each sender as

users/<numeric-id>; the Chat read scopes don't resolve those to display

names. The People API does (gws wraps it as gws people …), and "Connect

Workspace" now offers the grant as a separate Directory surface (scope

directory.readonly — read-only regardless of the read-write toggle). To turn

IDs into names:

  • If a gws people read 403s, the user hasn't ticked Directory — ask them

to reconnect with it.

  • **Resolve in batch — a Chat thread has many distinct senders, so collect all

the <numeric-id>s from users/<numeric-id> and resolve them in one

getBatchGet call, not one get per ID** (the People resource id is the

<numeric-id>):

    gws people people getBatchGet --params '{"resourceNames": ["people/<id1>", "people/<id2>"], "personFields": "names,emailAddresses"}'

Read each name off responses[].person.names[0].displayName. Use single-ID

gws people people get --params '{"resourceName": "people/<numeric-id>", "personFields": "names"}'

only for a one-off lookup. If a sender can't be seen as a domain member, fall

back to gws people people searchDirectoryPeople --params '{"query": "<name-or-email>", "readMask": "names,emailAddresses"}'.

  • Confirm exact param shapes with gws schema people.people.get before relying

on them.

Failure handling

  • No OAuth client configured → set up credentials first (see Auth: drop a

client_secret.json, set the GOOGLE_WORKSPACE_CLI_CLIENT_* env vars, or use

a service account).

  • gws auth login hangs on the listener → expected headless; bridge the

loopback by relaying the redirect URL and curl-ing it back in (see Auth →

"Completing the loopback flow headless"). Only fall back to a service account

when no user is present to paste the redirect.

  • Login token doesn't persist / "no keyring" error, or asked to log in again

after a runtime upgrade → set both GOOGLE_WORKSPACE_CLI_CONFIG_DIR=/workspace/lha/config/gws

and GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file on every gws call (config +

key then live on /workspace, which persists; ~/.config/gws does not).

  • Error 400: redirect_uri_mismatch on login → the OAuth client is a

"Web application" type; recreate it as Desktop app (loopback ports).

  • access_denied / "Account restricted" on consent → Context-Aware Access on

the OAuth client. Use an Internal-audience client (see Auth → "Creating

the OAuth client").

  • 401 / not logged in → if GOOGLE_WORKSPACE_CLI_TOKEN is the auth source,

the ~1h token has lapsed — ask the user to re-click "Connect Workspace" (it

has no refresh). Otherwise the OAuth login expired; re-run gws auth login.

  • gws auth status shows auth_method: none but reads work → expected when

the env token (GOOGLE_WORKSPACE_CLI_TOKEN) is the source; auth status

doesn't reflect it. Confirm with a real read, not auth status.

  • 403, scope too narrow → the granted scopes don't cover the call. If the

auth source is the GOOGLE_WORKSPACE_CLI_TOKEN env token, the "Connect

Workspace" grant was per-surface and read-only by default — ask the user to

reconnect including the surface (or read-write) you need. On the OAuth-login

path instead, re-auth with the needed scope: gws auth login --scopes <…>

(Google's incremental consent merges it with what was already granted).

  • 403, serviceUsageConsumer / quota project → distinct from a scope 403:

the client_secret.json's project_id is a project the account can't bill

to. Point the client at a project the account may use (the account needs

roles/serviceusage.serviceUsageConsumer on it).

  • gws: command not found → not installed. npm install -g @googleworkspace/cli.
  • Unsure of a flag or param → gws <service> --help or

gws schema <service>.<resource>.<method>. Don't guess.

<!-- find-skills:tested:false -->

How to use it

Copy the folder

Take google/google-workspace from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.

Install what it needs

The instructions reference npm, npx. Without those the skill loads but fails at the first command.