grr — Google Workspace CLI + MCP server

Google Workspace from the terminal: 401 methods across 14 Google APIs in one generated command tree, plus a typed MCP server (grr mcp). Zero-config release binaries, JSON on stdout, HTTP/3 with honest fallback. Rust, MIT.

Documentation

grr — Google Rust Rewrite

grr favicon

grr

CI License: MIT crates.io

Google tools from the terminal, at maximum performance. grr-cli is one published Rust package built around the grr command-line binary. Fourteen Google APIs share one OAuth login and one command tree — generated straight from Google's own Discovery Service — while stdout stays clean and machine-readable.

Project site: grr-cli.pages.dev · Privacy

grr is an independent project and is not affiliated with or endorsed by Google.

grr auth login
grr gmail users messages list --user-id me --q "in:inbox" --max-results 5
grr drive files list --page-size 10
grr api describe calendar.events.list
grr schema

Install

Prebuilt binaries — macOS arm64, Linux x86_64, Linux aarch64, Windows x86_64, and Windows on ARM (aarch64, Snapdragon X / Oryon): GitHub Releases

Release binaries are zero-config — an OAuth client is compiled in, so grr auth login works immediately. Archives are .tar.zst (zstd level 22) on unix and .zip on Windows; binaries are UPX-packed with --best --lzma where the compressor supports it — the workflow verifies the packed copy still executes and falls back to the unpacked binary with a warning if not.

From source (the default CLI build requires Rust nightly — see Development):

cargo install --git https://github.com/debanjanbasu/grr-cli

or from a local clone at the repository root:

git clone https://github.com/debanjanbasu/grr-cli
cd grr-cli
cargo install --path .

A source build has no OAuth client compiled in — see Authentication and configuration.

Package managers:

winget install debanjanbasu.grr       # Windows (manifest not yet published — see Packaging & status)
cargo install grr-cli                 # crates.io

Homebrew (macOS + Linux), from the debanjanbasu/tap tap:

brew tap debanjanbasu/tap
brew trust debanjanbasu/tap
brew install grr

brew trust is required, not decorative: since Homebrew 4.4 third-party taps are untrusted by default, and brew install refuses to install a formula from an untrusted tap until you trust it once. Skipping that line is the most common first-run failure.

The crates.io CLI build needs nightly Rust and RUSTFLAGS="--cfg reqwest_unstable" for HTTP/3; the prebuilt releases avoid that source-build step. cargo install also produces a binary with no embedded OAuth client, so budget five minutes for grr auth setup (or a .env) on that path.

60-second quickstart

brew tap debanjanbasu/tap
brew trust debanjanbasu/tap
brew install grr

grr auth login
grr gmail users getProfile --user-id me

There is nothing to configure first: a release binary already carries an OAuth client, so grr auth login opens the browser, you consent, and every service works. Headless machine? grr auth login --device prints a URL + code instead of opening a browser.

Building from source instead of installing a release? Read Authentication and configuration before your first grr invocation.

Authentication and configuration

Release binaries are zero-config. build.rs reads GRR_CLIENT_ID / GRR_CLIENT_SECRET from the build environment — GitHub Actions repo secrets for the official builds — and compiles them in, so GitHub Releases and Homebrew installs need no config file and no console visit.

Source builds bring their own client. cargo install grr-cli compiles the published sources with nothing in the build environment, so that binary has no embedded client. Any one of these fixes it:

  1. A .env next to Cargo.toml — copy .env.example to .env, fill in both values, rebuild. build.rs reads the environment first and .env second, so every build after that is zero-config too. .env is gitignored; never commit it.
  2. Environment variables for a single build — GRR_CLIENT_ID=… GRR_CLIENT_SECRET=… cargo build --release.
  3. grr auth setup — no rebuild required, because it writes ~/.grr/config.toml (Windows: %USERPROFILE%\.grr\config.toml) instead.

grr auth setup

Create and store an OAuth client. Release builds ship with one, so this is only needed when using
your own client

Usage: grr auth setup [OPTIONS]

Options:
      --client-id <CLIENT_ID>          Google OAuth client id (…apps.googleusercontent.com).
                                       Prompted for when omitted
      --client-secret <CLIENT_SECRET>  Google OAuth client secret. Prompted for when omitted
      --print-only                     Print the instructions and the resolved path, but write
                                       nothing. Useful for a first look, and for agents that only
                                       want the recipe
      --force                          Overwrite an existing ~/.grr/config.toml instead of refusing
      --enable-apis                    Try `gcloud services enable` for the APIs when gcloud is on
                                       PATH
  -f, --format <FORMAT>                Output format [default: json] [possible values: json, jsonl,
                                       table, pretty]
      --readonly                       Refuse every write method (POST/PATCH/PUT/DELETE); only reads
                                       run
      --deny-service <SERVICE>         Refuse every method of this service (repeatable)
      --deny-verb <VERB>               Refuse this HTTP verb (repeatable)
  -h, --help                           Print help

It checks the client-id shape before writing anything (must end in .apps.googleusercontent.com, and a truncated paste is rejected), rejects a truncated secret, writes the file mode 0600 on unix, and refuses to clobber an existing config.toml without --force. It prints the exact Cloud Console URLs and the gcloud services enable line covering every API behind a command — gmail, calendar-json, drive, people, chat, forms, tasks, docs, sheets, slides, script, analyticsadmin, analyticsdata, and searchconsole …googleapis.com — and --print-only hands you that recipe without touching disk, which is the right first command when you have no client at all:

grr auth setup --print-only

Because it runs before any client is resolved, grr auth setup works even when nothing is configured yet: it is the escape hatch, not a last resort. Under a non-TTY stdin (CI, agents, pipes) it errors instead of hanging, so pass --client-id and --client-secret explicitly rather than relying on the prompts.

Resolution order at runtime: GRR_OAUTH__CLIENT_ID / GRR_OAUTH__CLIENT_SECRET → ~/.grr/config.toml → the client compiled into the binary (env wins over the file; the compiled-in client is the fallback). GRR_CONFIG_PATH moves the file; RUST_LOG sets the log level.

Embedding a client secret is acceptable here because Google treats installed-app client secrets as non-confidential — the flow is protected by PKCE, which is always on. The secret lives in GitHub Actions repo secrets and is compiled in at release time; it must never enter the repository or a CI log.

To create a client from scratch, follow docs/gcp-setup.md — you only need that if you are building from source or deliberately want your own client.

The project/fork is Google Rust Rewrite; the Google consent-screen application is named Rust Rewrite. The consent screen is where that shorter name appears.

grr ask and System One (JEV)

grr ask "<request>" routes plain words to a typed method + parameters and prints the plan; --run executes it through the same call path as grr api. Routing is three System One judgments — service, then method, then parameters — each a small HTTP request. The model only ever selects among candidates the CLI supplies: the 14 services, a service's methods, a parameter's enum values or spans found in your request. It cannot invent a method id or a value — the CLI decides what runs.

Set TYPESAFE_API_KEY and it works against TypeSafe's hosted Jev:

export TYPESAFE_API_KEY=...
grr ask "show my unread messages"

The env route is preferred: a key in a config file can leak with the file. The [systemone] block in ~/.grr/config.toml is the only other surface:

KeyDefaultMeaning
endpointhttps://api.typesafe.ai/v1/systemoneany provider speaking the System One contract (POST state + typed questions, back structured answers)
modeljev-latestmodel name sent to that endpoint
api-key—sent as Authorization: Bearer; empty behaves as absent
confidence-threshold0.6below this method-choice confidence the plan is flagged, never blocked

Any key also accepts the generic GRR_SYSTEMONE__ENDPOINT / GRR_SYSTEMONE__MODEL / GRR_SYSTEMONE__API_KEY env form (the same GRR_ route as GRR_OAUTH__*), and TYPESAFE_API_KEY overrides api-key. The key never appears in the printed plan or in a request body — it rides only in the Authorization header.

Pointing endpoint/model at your own provider changes only which model ranks the CLI-supplied candidates; the candidate sets, the typed parameter fill, and what actually executes stay grr's. grr ask honors the global safety flags (--readonly, --deny-service <name>, --deny-verb <VERB>) whether or not you pass --run, and the gate runs as soon as the method is resolved — before the parameter-fill judgment and before any Google request is built or sent.

Usage highlights

grr gmail users messages list --user-id me --q "from:github.com" --max-results 10
grr gmail users messages get --user-id me --id 191f8ab2 --param-format metadata
grr gmail users threads list --user-id me --max-results 10
grr calendar events list --calendar-id primary --time-min 2026-10-01T00:00:00Z --max-results 10
grr drive files list --q "name contains 'report'" --page-size 10
grr drive files export --file-id <id> --mime-type application/pdf
grr chat spaces messages create --parent spaces/AAAA --body-file ./message.json
grr sheets spreadsheets values get --spreadsheet-id 1AbC… --range Sheet1!A1:B10
grr tasks tasklists list
grr transport
grr schema

Every command takes -f/--format json|jsonl|table|pretty (default json). For gmail users messages get, the Gmail MIME parameter named format arrives as --param-format full|metadata|minimal|raw (see the collision rule below), while -f/--format still selects the printed output format. Logs go to stderr, so stdout is always parseable:

grr gmail users messages list --user-id me --q "in:inbox" --max-results 1 | jq -r '.[0].id'

grr --version prints the semver on its first line (scripts depend on that), then the crab mascot: in full 24-bit-colour pixel art on a terminal that advertises COLORTERM/direct colour, and as plain ASCII everywhere else — a pipe, TERM=dumb, or NO_COLOR. Set CLICOLOR_FORCE=1 to force the colour logo through a pipe (grr --version | less -R); NO_COLOR still wins over it.

Command reference

The command tree is generated from the committed Discovery index by scripts/generate-commands.ts (CI-gated with --check), so this table is deliberately coarse: grr --help and grr schema are the source of truth, and every one of the 401 methods is a real, typed command.

GroupWhat it coversMethods
grr authlogin [--device] [--account name], status [--account name], setup [--client-id] [--client-secret] [--print-only] [--force] [--enable-apis]—
grr ask"<request>" [--run] [--service] [--method] — natural language to a typed method + params via a System One model; prints the plan by default-
grr mcpan MCP (JSON-RPC over stdio) server exposing every method as a typed tool; --readonly for read-only-
grr apilist [--service X] [--filter substr] [--grouped], describe <id>, call <id> …, refresh [--service X] — every method by id, the flat escape hatch401
grr schemathe whole command tree as JSON—
grr transportnegotiated HTTP version + runtime features—
grr skillsinstall [--dir PATH] [--claude] [--force], list — the 16 packaged skill files (the core skill, one per service, and their index) into the user-level skills directory (~/.agents/skills, offline; --claude mirrors to ~/.claude/skills)—
grr gmailmessages, threads, drafts, labels, history, attachments, filters, forwarding, POP/IMAP, send-as, CSE, delegates, watches79
grr calendarcalendars, events, instances, ACL, free/busy, colors, settings38
grr drivefiles, permissions, comments, replies, revisions, changes, drives, apps, approvals64
grr peoplecontacts, connections, contact groups, other contacts, directory people24
grr chatspaces, members, messages, reactions, media, custom emoji, read state54
grr formsform bodies, responses, watches, publish settings10
grr taskstasklists and the tasks inside them14
grr docsdocuments get / create / batchUpdate3
grr sheetsspreadsheets, values, batch operations, developer metadata17
grr slidespresentations, pages, thumbnails5
grr scriptscript projects, deployments, versions, processes, running functions16
grr analyticsadminaccounts, properties, data streams, custom dimensions and metrics, conversion events55
grr analyticsdatastandard, realtime, pivot and batch reports, audience exports11
grr searchconsolesearch analytics queries, sitemaps, sites, URL inspection11

Safety profiles. --readonly, --deny-service <name> and --deny-verb <VERB> are global flags honored by the generated tree, grr api call, grr ask and grr mcp alike — they parse before or after the subcommand, and turn destructive methods into actionable errors:

grr --readonly gmail users messages delete --user-id me --id abc   # refused: DELETE
grr --deny-service chat chat spaces list                            # refused: the chat service

The generated tree

Every service command above is generated from the committed index by scripts/generate-commands.ts — there are no hand-written per-service commands and no hand-written per-service clients. scripts/generate-commands.ts reads the committed Discovery index (src/discovery/*.json, ~430 KiB across the 14 services) and emits the whole tree into src/commands/generated.rs — 401 leaves, 994 typed flags, using clap's builder API. A daily workflow refetches Google's Discovery Service, regenerates the index along with the command tree, the per-service agent skills, and the site's coverage table, and opens a PR, so new API surface reaches you without waiting for a grr release. The tree and the index are generated artifacts: never hand-edit them.

The rules, so you can predict any command without memorizing it:

  • The naming rule. A leaf mirrors its Discovery method id: gmail.users.messages.list → grr gmail users messages list. Resources nest as subcommands; each leaf also carries its bare method name as a visible alias (list, get — camelCase methods keep their casing, e.g. getProfile).
  • Typed flags per method. Parameter names come from the same ids: userId → --user-id, maxResults → --max-results. Integers are parsed as i64, booleans are presence flags, repeated parameters repeat (--label-ids a --label-ids b), enum parameters validate their values, and required parameters are enforced by clap.
  • The collision rule. A parameter named format, query, params, body-file, dry-run, or help would collide with the shared escape hatches, so it is exposed as --param-format / --param-query / … (see RESERVED_FLAG_IDS in scripts/script-utils.ts).
  • Untyped bodies. Discovery's request schemas are not part of the index, so POST/PATCH/PUT bodies pass through --params <JSON> (merged; typed flags win) or --body-file <PATH|-> verbatim.
  • Every leaf also carries the escape hatches: --params <JSON>, --body-file, repeatable --query KEY=VALUE, --dry-run, and -f json|jsonl|table|pretty.

Dispatch resolves the leaf's id against the embedded index and funnels into one shared call path — the same engine grr api call uses. The two routes are interchangeable:

grr api call gmail.users.messages.list --param userId=me --dry-run
grr gmail users messages list --user-id me --dry-run
# byte-identical output

grr api stays as the flat escape hatch: list [--service] [--filter] [--grouped] to browse, describe <id> for parameters and scopes, call <id> to invoke, refresh [--service] to pull the index forward between releases. Because the index is embedded rather than fetched, both grr api list and grr schema work with no network at all.

Authorisation is checked per method: grr auth login consents to a fixed set of scopes, and each call compares the method's least-privilege scope against that set instead of silently escalating — so a method needing something you never granted prints a note naming the scope and may fail with a spelled-out 403 rather than an opaque error. Docs, Sheets, and Slides work through the drive scope grr already holds; Tasks needs the tasks scope, which this build does not request, so a Tasks call prints a note naming the scope and can come back 403. Their APIs (tasks.googleapis.com, docs.googleapis.com, sheets.googleapis.com, slides.googleapis.com) must be enabled on your Cloud project too — see docs/gcp-setup.md.

Design philosophy

  • Zero-config. Release binaries carry an OAuth client compiled in by build.rs, so a fresh install runs grr auth login with nothing to configure. ~/.grr/config.toml is the override, not the prerequisite, and holds only the OAuth client (ID, optionally a secret) plus — if you point grr ask at your own model — its [systemone] block (see grr ask and System One). Scopes, redirect URI, pool sizes, timeouts, and retry policy are compile-time constants tuned for Google's frontends (src/core/http.rs). GRR_CONFIG_PATH overrides the file location, RUST_LOG the log level (GRR_OAUTH__* env vars exist for headless overrides) — nothing else is configurable, on purpose.
  • Generated, namespaced services. The command tree is compiled from the Discovery index — the same machine-readable description Google publishes — so method additions land as a daily PR instead of a hand-written backlog. Mail is grr gmail …; Calendar, Drive, People, Chat, Forms, Tasks, Docs, Sheets, and Slides live alongside it, and account-level concerns stay top-level (grr auth, grr api, grr ask, grr mcp, grr transport, grr schema, grr skills). One login covers every service.
  • Self-migrating installs. A newer binary brings the agent skills it installed up to date on its own: files you never touched are replaced, files you edited get a 3-way merge against the bytes grr last wrote, and a conflict leaves your file untouched with the incoming version beside it (.grr-incoming). Files grr did not install are never touched. The check is lazy — a version compare; nothing is hashed until something actually needs merging.
  • stdout purity. Logs go to stderr, results go to stdout, so | jq always works. -f jsonl streams arrays one object per line.
  • Keyring-first token storage. Tokens live in the OS keyring (Windows Credential Manager, macOS Keychain, Linux Secret Service via D-Bus), with automatic fallback to <cache dir>/grr/token.json on headless systems. A token found in the fallback file auto-imports into the keyring on first sight.
  • Agent-first. grr schema dumps the complete command tree as JSON with zero configuration — the machine-readable contract for AI agents, discoverable without touching a config file or scraping --help. One fast CLI replaces per-service MCP servers: no MCP setup, just grr schema. There is also a packaged agent skill at skills/grr/SKILL.md — see Agent skills.

Agent skills

grr ships a packaged agent skill: skills/grr/SKILL.md — the discovery-first discipline (schema → api list → api describe → --dry-run), the method-id naming rule, and the output contract in one file, written for any AI agent. The core skill plus the 14 generated per-service skills are compiled into the binary, so grr skills install writes them into ~/.agents/skills/ with no network (Codex, opencode, Cursor, Gemini CLI, Copilot CLI and omp all read that location; --claude mirrors into ~/.claude/skills/ for Claude Code, which reads nothing else):

grr skills install            # all 16 into ~/.agents/skills/ (idempotent; identical files are a no-op)
grr skills install --claude   # also mirror into ~/.claude/skills/
grr skills install --force    # replace files that differ from the packaged copy
grr skills list               # the packaged set, and what each target directory holds

or install straight from the repository with the skills CLI:

npx skills add https://github.com/debanjanbasu/grr-cli

The site's agents guide carries the same contract for humans and harness authors.

Performance

  • HTTP/3 (QUIC) by default — prior-knowledge h3 with one authenticated probe at startup and silent HTTP/2 fallback; grr transport shows what was actually negotiated.
  • Tokio multi-threaded runtime, auto-sized to cores — no thread pool to tune.
  • In-flight request valve — a semaphore (not a thread pool) caps concurrent HTTP requests at 64, staying under Gmail's per-user rate limits so bursts don't self-DOS into 429s. 429s are retried honoring Retry-After (waits capped at 30s); whole-request timeout is 30s.
  • Compression always on — gzip, deflate, zstd, and brotli response decompression.
  • io_uring file I/O is a Linux-only target-specific dependency, auto-detected at runtime; it is not a Cargo feature.

Measured startup, binary size, and request-latency numbers against the other Google Workspace CLIs live at grr-cli.pages.dev/compare/, checked nightly by an automated workflow and re-measured whenever a new version ships.

Architecture

grr-cli is one published crate at the repository root. src/lib.rs builds the library target grr_cli; src/main.rs is a thin wrapper over src/cli.rs, and the binary is named grr.

.
├── src/
│   ├── core/                 # auth/device/oauth/server/store, http.rs,
│   │                         # config.rs, config_loader.rs, error.rs,
│   │                         # runtime.rs (the io_uring probe)
│   ├── commands/             # auth.rs, api.rs, setup.rs, transport.rs,
│   │                         # skills.rs, mcp.rs, ask.rs, safety.rs (the static
│   │                         # + generated.rs (GENERATED — the
│   │                         # whole service tree, ~470 KiB) and gen_dispatch.rs
│   │                         # (resolves leaf ids, funnels into the shared path)
│   ├── discovery.rs          # loader over the embedded index
│   ├── discovery/             # generated *.json index (~430 KiB, committed,
│   │                         # refreshed daily by workflow PR)
│   ├── schema.rs
│   ├── output.rs
│   ├── cli.rs
│   ├── lib.rs
│   └── main.rs
└── tests/                    # 5 integration test files

One shared core, no per-service client modules: the CLI speaks Discovery through a single call path (src/commands/api.rs), and the typed request/response models of the 0.3.x library era are gone. The repository also contains the assets/, site/ (the Astro site at grr-cli.pages.dev, deployed from main by Cloudflare Pages), packaging/, and scripts/ material used for the project site and distribution.

Development

grr requires Rust nightly — the build script fails with a clear message on any other toolchain. rust-toolchain.toml floats on the latest nightly and supplies the components needed by the build, while .cargo/config.toml sets the reqwest_unstable cfg for HTTP/3.

cargo build
cargo test
cargo clippy --all-targets -- -D warnings
cargo fmt --all --check
cargo run -- api list

node scripts/fetch-discovery.ts          # refresh the Discovery index
node scripts/generate-commands.ts        # regenerate the service command tree
node scripts/generate-skills.ts          # regenerate skills/<service>/SKILL.md
node scripts/generate-coverage.ts        # regenerate the site coverage table
node scripts/generate-changelog.ts       # regenerate CHANGELOG.md + site data

Each generator takes --check and exits 1 when its output is stale — that is the CI gate. Change the index or the generator, never src/commands/generated.rs by hand.

  • Tests never touch real credentials — token paths are injected, and wiremock serves the API endpoints.
  • RUST_LOG=debug traces requests; the default level mutes quinn_udp's harmless IPv6 warnings (info,quinn_udp=error).
  • A local build needs an OAuth client before a live call can work: copy .env.example to .env and fill it in (compiled in by build.rs), or run grr auth setup to write ~/.grr/config.toml. See CONTRIBUTING.md.
  • cargo run -- api list and cargo run -- schema need no client at all — the index is embedded.

Packaging & status

ChannelInstallStatus
GitHub Releases5-platform binaries (macOS arm64, Linux x86_64, Linux aarch64, Windows x86_64, Windows on ARM) built on v* tags, UPX-packed, .tar.zst on unix and .zip on Windowslive — every push to main releases; releases always carries the newest
crates.iocargo install grr-cli (binary installs as grr; needs nightly + RUSTFLAGS="--cfg reqwest_unstable" for the default CLI HTTP/3 build, and brings no embedded OAuth client)live — one crate, published on every release. Trusted publishing uses OIDC (no stored API tokens)
wingetwinget install debanjanbasu.grrnot yet published — the first submission stalled because the archives named the binary after its target (windows-x86_64.exe) while the manifest promised grr.exe; winget validation could never find the nested installer. Both sides are fixed (v0.8.1 archives ship grr.exe; node scripts/generate-winget.ts <tag> derives the manifest from the released archive and rides every release as grr-<tag>-winget-manifests.tar.gz)
Homebrewbrew tap debanjanbasu/tap && brew trust debanjanbasu/tap && brew install grr (tap: debanjanbasu/homebrew-tap, formula Formula/grr.rb, updated automatically on every release)live (arm64 macOS + x86_64/arm64 Linux)

The project publishes one package, grr-cli, whose binary is grr. v* tags trigger the release workflow, and crates.io publishing is handled through trusted publishing.

License

MIT — see LICENSE.