Loading...
Loading...
Build integrations with Google Workspace APIs (Gmail, Calendar, Drive, Sheets, Docs, Chat, Meet, Forms, Tasks, Admin SDK). Covers OAuth 2.0, service accounts, rate limits, batch operations, and Cloudflare Workers patterns. Use when building MCP servers, automation tools, or integrations with any Google Workspace API, or troubleshooting OAuth errors, rate limit 429 errors, scope issues, or API-specific gotchas.
npx skill4agent add jezweb/claude-skills google-workspace| API | Common Use Cases | Reference |
|---|---|---|
| Gmail | Email automation, inbox management | gmail-api.md |
| Calendar | Event management, scheduling | calendar-api.md |
| Drive | File storage, sharing | drive-api.md |
| Sheets | Spreadsheet data, reporting | sheets-api.md |
| Docs | Document generation | docs-api.md |
| Chat | Bots, webhooks, spaces | chat-api.md |
| Meet | Video conferencing | meet-api.md |
| Forms | Form responses, creation | forms-api.md |
| Tasks | Task management | tasks-api.md |
| Admin SDK | User/group management | admin-sdk.md |
| People | Contacts management | people-api.md |
// Authorization URL
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth')
authUrl.searchParams.set('client_id', env.GOOGLE_CLIENT_ID)
authUrl.searchParams.set('redirect_uri', `${env.BASE_URL}/callback`)
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('scope', SCOPES.join(' '))
authUrl.searchParams.set('access_type', 'offline') // For refresh tokens
authUrl.searchParams.set('prompt', 'consent') // Force consent for refresh token
// Token exchange
async function exchangeCode(code: string): Promise<TokenResponse> {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: env.GOOGLE_CLIENT_ID,
client_secret: env.GOOGLE_CLIENT_SECRET,
redirect_uri: `${env.BASE_URL}/callback`,
grant_type: 'authorization_code',
}),
})
return response.json()
}
// Refresh token
async function refreshToken(refresh_token: string): Promise<TokenResponse> {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
refresh_token,
client_id: env.GOOGLE_CLIENT_ID,
client_secret: env.GOOGLE_CLIENT_SECRET,
grant_type: 'refresh_token',
}),
})
return response.json()
}access_type=offlineprompt=consentimport { SignJWT } from 'jose'
async function getServiceAccountToken(
serviceAccount: ServiceAccountKey,
scopes: string[]
): Promise<string> {
const now = Math.floor(Date.now() / 1000)
// Create JWT
const jwt = await new SignJWT({
iss: serviceAccount.client_email,
scope: scopes.join(' '),
aud: 'https://oauth2.googleapis.com/token',
iat: now,
exp: now + 3600,
})
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
.sign(await importPKCS8(serviceAccount.private_key, 'RS256'))
// Exchange JWT for access token
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: jwt,
}),
})
const data = await response.json()
return data.access_token
}const jwt = await new SignJWT({
iss: serviceAccount.client_email,
sub: 'user@domain.com', // User to impersonate
scope: scopes.join(' '),
aud: 'https://oauth2.googleapis.com/token',
iat: now,
exp: now + 3600,
})| API | Reads | Writes | Notes |
|---|---|---|---|
| Gmail | 250/user/sec | 250/user/sec | Aggregate across all methods |
| Calendar | 500/user/100sec | 500/user/100sec | Per calendar |
| Drive | 1000/user/100sec | 1000/user/100sec | |
| Sheets | 100/user/100sec | 100/user/100sec | Lower than others |
| API | Daily Quota | Per-Minute | Notes |
|---|---|---|---|
| Gmail | 1B units | Varies | Unit-based (send = 100 units) |
| Calendar | 1M queries | 500/sec | |
| Drive | 1B queries | 1000/sec | |
| Sheets | Unlimited | 500/user/100sec |
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 5
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error: any) {
const status = error.status || error.code
if (status === 429 || status === 503) {
// Rate limited or service unavailable
const retryAfter = error.headers?.get('Retry-After') || Math.pow(2, i)
await new Promise(r => setTimeout(r, retryAfter * 1000))
continue
}
if (status === 403 && error.message?.includes('rateLimitExceeded')) {
// Quota exceeded - exponential backoff
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000))
continue
}
throw error
}
}
throw new Error('Max retries exceeded')
}async function batchRequest(
accessToken: string,
requests: BatchRequestItem[]
): Promise<BatchResponse[]> {
const boundary = 'batch_boundary'
let body = ''
requests.forEach((req, i) => {
body += `--${boundary}\r\n`
body += 'Content-Type: application/http\r\n'
body += `Content-ID: <item${i}>\r\n\r\n`
body += `${req.method} ${req.path} HTTP/1.1\r\n`
body += 'Content-Type: application/json\r\n\r\n'
if (req.body) body += JSON.stringify(req.body)
body += '\r\n'
})
body += `--${boundary}--`
const response = await fetch('https://www.googleapis.com/batch/v1', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': `multipart/mixed; boundary=${boundary}`,
},
body,
})
// Parse multipart response...
return parseBatchResponse(await response.text())
}// wrangler.jsonc
{
"name": "google-workspace-mcp",
"main": "src/index.ts",
"compatibility_date": "2026-01-03",
"compatibility_flags": ["nodejs_compat"],
// Store OAuth tokens
"kv_namespaces": [
{ "binding": "TOKENS", "id": "xxx" }
],
// Or use D1 for structured storage
"d1_databases": [
{ "binding": "DB", "database_name": "workspace-mcp", "database_id": "xxx" }
]
}echo "your-client-id" | npx wrangler secret put GOOGLE_CLIENT_ID
echo "your-client-secret" | npx wrangler secret put GOOGLE_CLIENT_SECRET
# For service accounts:
cat service-account.json | npx wrangler secret put GOOGLE_SERVICE_ACCOUNTreferences/{
"devDependencies": {
"@cloudflare/workers-types": "^4.20260109.0",
"wrangler": "^4.58.0",
"jose": "^6.1.3"
}
}