Google Workspace integration (all eight services, native)
Visibility status (updated in v1.49.0): the dashboard’s Integrations → Google tab is now visible by default. Earlier versions hid it pending Google’s OAuth app verification, but verification turns out to gate only the “bring your own OAuth client” path (domain-wide delegation via service account and the Apps Script bridge are unaffected), so there was no reason to keep the tab hidden. Being visible does not mean the tools are active, though: there is still a separate master switch,
config.toml [integrations] google_workspace, defaulting tofalse. Without it, credentials can be configured and the connection test can pass, but the tools never reach your AI employees, and the dashboard shows a clear yellow warning. For a guided walkthrough of the three credential paths, see google-workspace-integration.md.
Design decision (D5, 2026-08-04): DuDuClaw does not ship shared Google OAuth credentials. Users supply their own OAuth client (or use the DWD / Apps Script paths instead), and DuDuClaw only stores and refreshes the resulting token — it never bundles a client id/secret of its own. See google-workspace-integration.md for the decision context (D5).
Connect a Google account so your AI employees can search and read mail, prepare draft replies, list your calendar, create events (with Google Meet links), and read/append rows in your Google Sheets, read Google Forms responses, and manage Google Tasks. DuDuClaw talks to the Google REST APIs natively — there is no third-party MCP server to install. The access token is stored in DuDuClaw’s encrypted OAuth vault and refreshed automatically.
All eight Workspace services are covered natively — Gmail, Calendar, Sheets, Drive, Docs, Slides, Forms, Tasks — on GA REST APIs, so nothing here depends on Google’s Developer Preview program and any customer can use it. (Google’s own remote MCP servers cover six of the eight but are Preview-only, and their terms forbid exposing Pre-GA APIs to users outside your own domain. They remain available as an advanced opt-in: google-mcp.md.)
What you get
Section titled “What you get”Nineteen agent-facing MCP tools, gated by two scopes (google:read /
google:write):
| Tool | Class | What it does |
|---|---|---|
google_status |
read | Connection diagnostics: connected?, granted scopes, token validity. Reads local state only. |
gmail_search |
read | Search the mailbox using Gmail query syntax (from:… is:unread, etc.). Returns sender/subject/date/snippet. |
gmail_read |
read | Read one message in full: headers, plain-text body (truncated if long), attachment manifest (filename + size only — never downloaded). |
gmail_create_draft |
write | Create a Gmail draft. Never sends — sending stays a manual human action. |
calendar_list_events |
read | List primary-calendar events (defaults to the next 7 days). |
calendar_create_event |
write | Create a real, externally-visible event; optional Google Meet link. |
sheets_read |
read | Read a cell range from a spreadsheet (accepts a spreadsheet ID or a full sheet URL). Returns up to 200 rows of formatted values. |
sheets_append |
write | Append one row to a spreadsheet using USER_ENTERED input (numbers/dates/formulas parsed as if typed). |
forms_get |
read | Read a Form’s structure: title, description, and every question with its question_id, type and choice options. |
forms_list_responses |
read | List a Form’s submitted responses (up to 50). Answers are keyed by question_id — pair with forms_get to map ids to titles. |
gtasks_lists |
read | List the account’s Google Tasks lists (id + title). @default targets the default list without a lookup. |
gtasks_list |
read | List tasks in one list (pending only by default; show_completed=true includes finished + hidden). |
gtasks_create |
write | Create a real task in the user’s Google Tasks. |
gtasks_complete |
write | Mark a task completed. |
drive_search |
read | Search Drive by file name and full text (trashed files excluded, newest first). Optional exact MIME filter. |
drive_read |
read | Read a Drive file as text: Docs/Slides export as plain text, Sheets as CSV (first sheet only), text-like blobs verbatim. Binary types return metadata + a note, never binary content. |
docs_read |
read | Read a Google Doc’s text in document order, including table cell text. |
docs_append |
write | Append text to the end of a Doc. Append-only — no tool rewrites or deletes existing content. |
slides_read |
read | Read a presentation’s text slide by slide (shapes, grouped shapes, table cells). |
No Slides write tool ships on purpose: DuDuClaw’s office document suite already produces real
.pptxfiles, which is both safer and better output than driving the SlidesbatchUpdateelement API.
Naming: the Google Tasks tools are
gtasks_*. DuDuClaw’s own task board keepstasks_*(tasks_list/tasks_create/tasks_complete/ …) — two separate systems, deliberately distinct prefixes so an agent never confuses “my work queue” with “the user’s Google Tasks”.
No official MCP server exists for Forms or Tasks (verified 2026-07-30: both
formsmcp/tasksmcp endpoints 404 and neither appears in Google’s MCP docs),
which is why they are served natively here.
Safety design
Section titled “Safety design”-
Drafts never send.
gmail_create_draftonly saves a draft; there is no “send” tool. Delivery is always a human decision. -
Read stays read. The read-class tools cannot modify anything in Gmail or Calendar.
-
Forms, Drive and Slides are read-only. No tool creates or edits a form, writes to Drive, or modifies a presentation. Only Gmail (draft), Calendar, Sheets, Docs (append) and Tasks have write tools.
-
Least privilege. Drive is requested
drive.readonly(neverdriveordrive.file— nothing creates Drive files) and Slidespresentations.readonly. Docs needs fulldocumentsonly becausedocs_appendwrites. -
Optional approval gate. For extra caution, list the write tools under an agent’s
agent.toml [capabilities] approval_required_toolsso each draft, event, or spreadsheet write waits for HITL approval:[capabilities]approval_required_tools = ["gmail_create_draft", "calendar_create_event", "sheets_append", "gtasks_create", "gtasks_complete", "docs_append"]
Choosing a credential path
Section titled “Choosing a credential path”Three ways to authorize the same nineteen tools. They differ in who has to set something up, and in whether Google needs to have verified an app first.
| Personal @gmail.com | Workspace domain | Who sets it up | Tool coverage | |
|---|---|---|---|---|
| OAuth client (below) | ✅ | ✅ | each customer creates their own Google Cloud OAuth client | all 19 |
| Service account + domain-wide delegation | ❌ | ✅ | the domain’s super admin authorizes one client id | all 19 |
| Apps Script bridge | ✅ | ✅ (unless the admin disables Apps Script) | the end user deploys a script in their own account | Gmail / Calendar / Sheets only |
When more than one is configured the order of precedence is service account →
OAuth vault → Apps Script bridge; the bridge sits last because it covers the
fewest tools. google_status names the source actually in effect.
The two credential-free paths are documented in google-no-oauth-client.md. To give each AI employee its own colleague’s account, see Per-employee Google accounts.
Per-employee Google accounts (company delegation, unreleased)
Section titled “Per-employee Google accounts (company delegation, unreleased)”By default every AI employee uses the one Google account set up below (the
company’s shared account). A Google Workspace customer can instead let each
employee act as one named colleague: the sales assistant reads and drafts in
alice@customer.com’s mailbox, the scheduling assistant works in
bob@customer.com’s calendar. One service account with domain-wide
delegation serves all of them; each employee names the colleague it acts as.
What you need
Section titled “What you need”- A Google Workspace domain and a super admin who can authorize one client id (Admin console → Security → Access and data control → API controls → Manage Domain Wide Delegation) for the full scope list the dashboard shows.
- Credential protection in the system keychain (Security page, see Security defense). The service-account key is sealed with that protection key, so it is never a file an AI employee can read. macOS and Windows qualify. On Linux and in Docker there is no supported keychain yet, and acting as a colleague is refused.
Set it up
Section titled “Set it up”- Security → Credential protection → Move to system keychain.
- Integrations → Google → Company delegation → Import key file. Pick
the service-account JSON key. The gateway checks it, seals it and stores
it as
<home>/protected/google-service-account.sealed(owner-only). Delete the original file afterwards. The panel shows the client id and the scope list to hand to the super admin. - Allowed domains. Only colleagues in these domains can be named
(exact domain match, no subdomains). Left empty, only the domain of the
home service account’s
subjectis allowed; with neither, nobody is. - Employee page → Connections → Change → Act as a colleague (domain delegation), enter the colleague’s work email, save.
- Test connection mints a token for that colleague with the full scope list, checks Gmail reports the same address, then mints per-service scopes. Gmail, Calendar, Drive and Tasks read read-only endpoints. Sheets, Docs, Slides and Forms first find an accessible sample and then GET its service resource. With no sample, that service stays unverified; token issuance alone does not count as data access. It sends nothing and does not reuse or fill the token cache.
The account card’s “Configured — test connection” badge is a local setup check, not proof the sealed key opens or Google accepts the grant. Every native delegated tool call asks the gateway to recheck current settings, requester permissions and account status; only the gateway caches minting.
The same choices are available as RPCs (all Admin): google.delegation.get,
google.delegation.import_key, google.delegation.remove_key,
google.delegation.settings_set, agents.google_identity_set,
google.identity.test. agents.google_identity_get needs Viewer access to
the employee.
Settings
Section titled “Settings”# agent.toml (admin-set; employees cannot edit it with Write/Edit)[google]identity = "delegate:alice@customer.com" # home | none | delegate:<email>fallback = "none" # none (default) | homerequesters = "bound_people" # bound_people (default) | owner_only | anyone
# config.toml[integrations.google_identity]default_identity = "home" # what employees without [google] use: home (default) | noneallowed_subject_domains = ["customer.com"]An employee without [google] uses default_identity, so an existing
installation behaves exactly as before. none hides the 19 Google tools
from that employee and refuses calls to them. A value that cannot be read
(a typo, an unknown key) refuses Google for that employee; it never turns
into the shared account. Enterprise employees may use owner or user:<id>
with an active direct Owner and that person’s explicit consent; see
Personal Google connections (unreleased).
Who can use the account through the employee
Section titled “Who can use the account through the employee”The colleague’s mailbox is a person’s data, so the employee only uses it for
some requesters (requesters):
| Setting | Allowed |
|---|---|
bound_people (default) |
People bound to this employee (any access level) and Admins, in the dashboard, WebChat or a verified channel binding; scheduled and system work (cron, heartbeat, Agent Mail); work delegated from one of those people |
owner_only |
The colleague’s own DuDuClaw account and scheduled work |
anyone |
Anyone who can talk to the employee (needs confirm_anyone: true when set over RPC) |
Public website visitors (WebChat widget, Odoo Live Chat) are refused under
every setting. An unbound channel sender is refused unless anyone is set.
When it cannot be used
Section titled “When it cannot be used”The tool call fails with a reason and who can fix it. It does not quietly
use the shared account. The reasons: the caller is not a verified employee;
the requester is not allowed; the email is outside the allowed domains; the
colleague’s DuDuClaw account is suspended or offboarded; credential
protection is not ready (or its key was replaced, in which case the sealed
key no longer opens: reset protection and import the key again); no key was
imported; Google rejected the delegation (unauthorized_client usually means
the scope list in the Admin console is incomplete, invalid_grant that the
account does not exist or is disabled); the gateway is not running.
With “If the colleague’s account cannot be used, use the company’s shared
account” ticked (fallback = "home"), the last group (no key imported,
colleague suspended, Google rejected or unreachable) uses the shared account
instead and the result starts with a note saying so. A refused requester, a
setting error and a protection problem never fall back.
What else changes
Section titled “What else changes”google_statusstarts with who the employee acts as. For a colleague it reports whether a token can be obtained right now instead of the shared account’s details.- Agent Mail reads the mailbox as the identity of the employee that owns arriving mail. When that is a colleague, the Mailbox pages are visible only to Admins and that colleague (behaviour change; a shared-account mailbox keeps the Manager gate). Agent Mail is still one mailbox per installation.
- External Google MCP servers (
bearer_token = "oauth://google") use the employee’s selected account on HTTP mounts. Permission failures skip that Google mount; other configured servers remain available. See below. - Audit: every token the gateway issues or refuses is a
google_tokenrow intool_calls.jsonl(employee, identity, account, requester kind, tool, refusal code); every Google tool call on a colleague’s identity is agoogle_callrow. Neither ever contains a token. Changing an employee’s identity writesagent.google_identity_setto the account audit log andagent_google_identity_settosecurity_audit.jsonl.
Dedicated employee API keys and Google MCP mounts (unreleased)
Section titled “Dedicated employee API keys and Google MCP mounts (unreleased)”An authenticated employee-specific API key with explicit google:read or
google:write may use the corresponding tools in the closed 19-tool Google
surface. admin alone is not a substitute. The key must belong to a real
employee with readable configuration. General external key grant options and
other connectors are unchanged; OAuth authentication does not create this
execution receipt. A legitimate internal custom integration key without an
employee keeps its existing Home-account path.
These calls still pass the employee’s tool allow/deny lists, action rules, PolicyKernel, scoped grants and required approvals. The accepted employee, tool and final arguments stay fixed through an approval wait. Expired keys, removed employees and changed authority are refused, never replaced by a process’s ambient identity or another account. The gateway executes the Google request and returns bounded tool results without returning a Google bearer to the external MCP client.
A key identifies the employee, not a human requester. For a delegated person
account it is an Unknown requester: bound_people and owner_only refuse
it, even with Home fallback enabled. Only an operator’s explicit anyone
setting permits this use; it does not authorize public website visitors.
For an employee’s HTTP MCP mount, bearer_token = "oauth://google" resolves
that employee’s account in the gateway. Each actual call rechecks the live
requester, selector, domain, colleague status and credential protection,
including when the connection is reused. A person-account stdio mount is
skipped because stdio does not consume the HTTP bearer. An actual configured
Home fallback adds a separate notice to that call’s result; a successful
person-account call does not claim to have used Home. Provider JSON and
redaction/CCR processing retain their original structure.
Agent Mail also records the last completed poll time, unavailable-account count and transport-error count. An unavailable or unreadable account is shown separately from an empty mailbox; no completed poll is not reported as successful zero-mail access. The per-installation mailbox and reader gates remain unchanged.
Dedicated gateway execution makes one attempt for non-GET/HEAD REST requests: transport errors, 429 and 5xx do not automatically repeat a write. GET/HEAD retain one retry. This restriction applies to the dedicated execute route; existing Home callers keep their previous helper behavior.
Limits
Section titled “Limits”gmail.composelets whoever holds the access token send mail. The tools only create drafts, but an employee with an unrestricted shell can call Google directly with a token it was issued for its own colleague.- An employee with a shell runs as the same OS user as the gateway; see the keychain limits in Security defense.
- Not yet verified against a real Workspace tenant: the exact Admin console flow, Google’s real error strings, a Gmail-disabled organizational unit, and whether Google’s own remote MCP endpoints accept delegated tokens.
Prerequisites: create a Google OAuth client
Section titled “Prerequisites: create a Google OAuth client”You supply your own Google OAuth client (DuDuClaw never ships shared credentials). One-time setup:
-
Open the Google Cloud Console → Credentials page (create/select a project first).
-
Enable these eight APIs for the project (APIs & Services → Library), or in one command:
終端機視窗 gcloud services enable gmail.googleapis.com calendar-json.googleapis.com \sheets.googleapis.com drive.googleapis.com docs.googleapis.com \slides.googleapis.com forms.googleapis.com tasks.googleapis.com \--project=PROJECT_ID -
Configure the OAuth consent screen (External or Internal). Add your own Google account as a test user if the app stays in “Testing”.
-
Create an OAuth client ID of type Web application.
-
Under Authorized redirect URIs, add exactly:
http://localhost:18889/api/mcp/oauth/callback18889 is the gateway’s default port. If you run it elsewhere (
DUDUCLAW_PORT), register that port instead — the dashboard’s setup step shows the exact URI, derived from the port the gateway is actually listening on. A mismatch here is silent: Google redirects the browser to a port with nothing on it, so the token never arrives and the page stays on “not connected”. -
Copy the generated Client ID and Client secret.
The requested scopes are:
https://www.googleapis.com/auth/gmail.readonlyhttps://www.googleapis.com/auth/gmail.composehttps://www.googleapis.com/auth/calendar.eventshttps://www.googleapis.com/auth/spreadsheetshttps://www.googleapis.com/auth/drive.readonlyhttps://www.googleapis.com/auth/documentshttps://www.googleapis.com/auth/presentations.readonlyhttps://www.googleapis.com/auth/forms.body.readonlyhttps://www.googleapis.com/auth/forms.responses.readonlyhttps://www.googleapis.com/auth/taskshttps://www.googleapis.com/auth/userinfo.emailScope change (v1.45): the
spreadsheetsscope was added for the Sheets tools. A Google account connected before v1.45 will get a403from the Sheets APIs because its token predates this scope —google_statusflags the missing scope, and reconnecting from the dashboard re-consents with the full set.Scope change (v1.47): Drive (
drive.readonly), Docs (documents), Slides (presentations.readonly), Forms (forms.body.readonly,forms.responses.readonly) and Tasks (tasks) were added for the new native tools. Same rule as above — an older token yields403with re-auth guidance; reconnect from Integrations → Google to re-consent.
Connect from the dashboard
Section titled “Connect from the dashboard”- Go to Integrations → Google (
/manage/integrations?tab=google). - Paste the Client ID and Client secret, then click Connect Google.
- A Google consent window opens. Approve access. The window confirms success and the dashboard flips to Google is connected.
The client credentials are persisted (secret encrypted at rest) so the access token can be refreshed automatically, and so re-authorizing later does not require re-entering the secret.
To disconnect, click Disconnect on the connected view. Your stored client credentials are kept so you can reconnect in one click; the access token is removed.
How refresh works
Section titled “How refresh works”Google issues a refresh token only when the authorization requests offline
access, so the connect flow adds access_type=offline&prompt=consent
automatically for Google. When the access token expires, get_valid_google_token
runs a refresh grant with your stored client credentials, saves the new token,
and continues. If refresh is not possible (no refresh token, or missing stored
credentials), the tools return a clear message directing you back to the
Integrations → Google page to reconnect.
Re-authorizing after a scope change
Section titled “Re-authorizing after a scope change”A token authorized before this integration shipped (older scope set) will get a
403 from the new write APIs. The tools detect this and return guidance listing
the scopes to grant. Reconnect from Integrations → Google to re-consent with the
current scopes.
Troubleshooting
Section titled “Troubleshooting”- “Google is not connected.” — No token stored. Connect from the dashboard.
401 Unauthorized— The authorization was revoked or is invalid. Reconnect.403with a scope list — The token is missing required scopes. Reconnect to re-consent.- Redirect URI mismatch during consent — The redirect URI in your Google
OAuth client must be exactly
http://localhost:18889/api/mcp/oauth/callback.
Run google_status at any time for a live diagnosis.