Add Gmail Tool (OneCLI-native)
This skill wires the @gongrzhe/server-gmail-autoauth-mcp stdio MCP server into selected agent groups. The MCP server reads stub credentials containing the onecli-managed placeholder; the OneCLI gateway intercepts outbound calls to gmail.googleapis.com and injects the real OAuth bearer from its vault.
Tools exposed (from gmail-mcp@1.1.11, surfaced to the agent as mcp__gmail__<name>): search_emails, read_email, send_email, draft_email, delete_email, modify_email, batch_modify_emails, batch_delete_emails, download_attachment, list_email_labels, create_label, update_label, delete_label, get_or_create_label, list_filters, get_filter, create_filter, create_filter_from_template, delete_filter.
Why this pattern: v2's invariant is that containers never receive raw API keys — OneCLI is the sole credential path (see CHANGELOG v2.0.0). The stub-file pattern satisfies this: the container sees "onecli-managed" placeholders, the gateway swaps them in flight.
Phase 1: Pre-flight
Verify OneCLI has Gmail connected
onecli apps get --provider gmail
Expected: "connection": { "status": "connected" } with scopes including gmail.readonly, gmail.modify, gmail.send.
If not connected, tell the user:
Open the OneCLI web UI at http://127.0.0.1:10254, go to Apps → Gmail, and click Connect. Sign in with the Google account you want the agent to act as.
Verify stub credentials exist
ls -la ~/.gmail-mcp/gcp-oauth.keys.json ~/.gmail-mcp/credentials.json 2>&1
If both exist and contain "onecli-managed":
grep -l onecli-managed ~/.gmail-mcp/gcp-oauth.keys.json ~/.gmail-mcp/credentials.json
...skip to Phase 2.
If either file exists but does not contain onecli-managed, STOP and tell the user — these are real OAuth credentials from a previous non-OneCLI install. Back them up, then delete before proceeding. The OneCLI migration normally handles this; if it didn't, something is wrong.
If both files are absent, write them now:
mkdir -p ~/.gmail-mcp
cat > ~/.gmail-mcp/gcp-oauth.keys.json <<'EOF'
{
"installed": {
"client_id": "onecli-managed.apps.googleusercontent.com",
"client_secret": "onecli-managed",
"redirect_uris": ["http://localhost:3000/oauth2callback"]
}
}
EOF
cat > ~/.gmail-mcp/credentials.json <<'EOF'
{
"access_token": "onecli-managed",
"refresh_token": "onecli-managed",
"token_type": "Bearer",
"expiry_date": 99999999999999,
"scope": "https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/gmail.send"
}
EOF
chmod 600 ~/.gmail-mcp/gcp-oauth.keys.json ~/.gmail-mcp/credentials.json
Verify mount allowlist covers the path
cat ~/.config/nanoclaw/mount-allowlist.json
~/.gmail-mcp must sit under an allowedRoots entry (e.g. /home/<user>). If it doesn't, tell the user to run /manage-mounts first or add their home directory.
Check agent secret-mode
For each target agent group, confirm OneCLI will inject Gmail secrets into its container. Find the OneCLI agent ID that matches the group's agentGroupId:
onecli agents list
If that agent's secretMode is all, you're done — Gmail secrets (identified by OneCLI's Gmail hostPattern) will auto-inject. If it's selective, explicitly assign the Gmail secrets using the safe merge pattern (set-secrets replaces the entire list — always read first):
GMAIL_IDS=$(onecli secrets list | jq -r '[.data[] | select(.name | test("(?i)gmail")) | .id] | join(",")')
CURRENT=$(onecli agents secrets --id <agent-id> | jq -r '[.data[]] | join(",")')
MERGED=$(printf '%s' "$CURRENT,$GMAIL_IDS" | tr ',' '\n' | sort -u | paste -sd ',' -)
onecli agents set-secrets --id <agent-id> --secret-ids "$MERGED"
onecli agents secrets --id <agent-id>
Phase 2: Apply Code Changes
Check if already applied
grep -q 'GMAIL_MCP_VERSION' container/Dockerfile && \
echo "ALREADY APPLIED — skip to Phase 3"
Add MCP server to Dockerfile
Edit container/Dockerfile. Find the pinned-version ARG block:
ARG CLAUDE_CODE_VERSION=2.1.116
ARG AGENT_BROWSER_VERSION=latest
ARG VERCEL_VERSION=latest
ARG BUN_VERSION=1.3.12
Add a new line:
ARG GMAIL_MCP_VERSION=1.1.11
Then find the last pnpm global-install RUN block (the one that installs @anthropic-ai/claude-code) and add a new block after it, before # ---- Entrypoint:
RUN --mount=type=cache,target=/root/.cache/pnpm \
pnpm install -g \
"@gongrzhe/server-gmail-autoauth-mcp@${GMAIL_MCP_VERSION}" \
"zod-to-json-schema@3.22.5"
Pinned version matters — minimumReleaseAge in pnpm-workspace.yaml gates trunk installs, and CLAUDE.md requires a fixed ARG version for all Node CLIs installed into the image.
Why the zod-to-json-schema pin: @gongrzhe/server-gmail-autoauth-mcp@1.1.11 has loose deps (zod-to-json-schema: ^3.22.1, zod: ^3.22.4). pnpm resolves zod-to-json-schema to the latest 3.25.x, which imports zod/v3 — a subpath that only exists in zod>=3.25. But zod resolves to 3.24.x (highest satisfying ^3.22.4 without breaking peer ranges). Result: ERR_PACKAGE_PATH_NOT_EXPORTED at import time. Pinning zod-to-json-schema to a pre-v3-subpath version avoids it. Re-check if you bump GMAIL_MCP_VERSION.
No TOOL_ALLOWLIST edit needed. container/agent-runner/src/providers/claude.ts derives the allow-pattern dynamically from each group's mcpServers map (Object.keys(this.mcpServers).map(mcpAllowPattern)), so registering gmail in Phase 3 automatically allows mcp__gmail__*. Earlier versions of this skill instructed a static TOOL_ALLOWLIST edit — that's now redundant.
Rebuild the container image
./container/build.sh
Must complete cleanly. The new pnpm install -g layer is ~60s first time (cached on rebuild).
Phase 3: Wire Per-Agent-Group
For each agent group that should have Gmail (ask the user — typically their personal DM and CLI agents, sometimes shared household agents), persist two changes to the central DB (data/v2.db): the mcpServers.gmail entry and an additionalMounts entry for .gmail-mcp. Both flow through materializeContainerJson on every spawn, so editing groups/<folder>/container.json by hand does not stick — that file is regenerated from the DB.
List groups, pick which ones get Gmail
ncl groups list
Register the MCP server
For each chosen <group-id>:
ncl groups config add-mcp-server \
--id <group-id> \
--name gmail \
--command gmail-mcp \
--args '[]' \
--env '{"GMAIL_OAUTH_PATH":"/workspace/extra/.gmail-mcp/gcp-oauth.keys.json","GMAIL_CREDENTIALS_PATH":"/workspace/extra/.gmail-mcp/credentials.json"}'
Approval behaviour depends on where you run it: from inside an agent's container ncl write verbs are approval-gated (admin approves before it lands); from a host operator shell with full scope, it executes immediately. Either way, the response tells you which path it took.
Add the .gmail-mcp mount
There is no ncl groups config add-mount verb yet (tracked in #2395). Until that ships, edit the DB directly via the in-tree wrapper (scripts/q.ts — setup/verify.ts:5 codifies that NanoClaw avoids depending on the sqlite3 CLI binary, so don't shell out to it):
GROUP_ID='<group-id>'
HOST_PATH="$HOME/.gmail-mcp"
MOUNT=$(jq -cn --arg h "$HOST_PATH" '{hostPath:$h, containerPath:".gmail-mcp", readonly:false}')
pnpm exec tsx scripts/q.ts data/v2.db "UPDATE container_configs \
SET additional_mounts = json_insert(additional_mounts, '\$[#]', json('$MOUNT')), \
updated_at = datetime('now') \
WHERE agent_group_id = '$GROUP_ID';"
Run from your N