Emulated Google OAuth 2.0, OpenID Connect, Gmail, Calendar, and Drive for local development and testing. Use when the user needs to test Google sign-in locally, emulate OIDC discovery, handle Google token exchange, configure Google OAuth clients, work with Gmail messages/drafts/threads/labels, manage Calendar events, upload or list Drive files, or work with Google userinfo without hitting real Google APIs. Triggers include "Google OAuth", "emulate Google", "mock Google login", "test Google sign-in", "OIDC emulator", "Google OIDC", "Gmail API", "Google Calendar", "Google Drive", "local Google auth", or any task requiring a local Google API.
npx skills add https://github.com/vercel-labs/emulate --skill google
OAuth 2.0 and OpenID Connect emulation with authorization code flow, PKCE support, ID tokens, OIDC discovery, refresh tokens, plus Gmail, Google Calendar, and Google Drive REST API surfaces.
# Google only
npx emulate --service google
# Default port
# http://localhost:4002
Or programmatically:
import { createEmulator } from 'emulate'
const google = await createEmulator({ service: 'google', port: 4002 })
// google.url === 'http://localhost:4002'
GOOGLE_EMULATOR_URL=http://localhost:4002
| Real Google URL | Emulator URL |
|-----------------|-------------|
| https://accounts.google.com/o/oauth2/v2/auth | $GOOGLE_EMULATOR_URL/o/oauth2/v2/auth |
| https://oauth2.googleapis.com/token | $GOOGLE_EMULATOR_URL/oauth2/token |
| https://www.googleapis.com/oauth2/v2/userinfo | $GOOGLE_EMULATOR_URL/oauth2/v2/userinfo |
| https://accounts.google.com/.well-known/openid-configuration | $GOOGLE_EMULATOR_URL/.well-known/openid-configuration |
| https://www.googleapis.com/oauth2/v3/certs | $GOOGLE_EMULATOR_URL/oauth2/v3/certs |
| https://gmail.googleapis.com/gmail/v1/... | $GOOGLE_EMULATOR_URL/gmail/v1/... |
| https://www.googleapis.com/calendar/v3/... | $GOOGLE_EMULATOR_URL/calendar/v3/... |
| https://www.googleapis.com/drive/v3/... | $GOOGLE_EMULATOR_URL/drive/v3/... |
import { OAuth2Client } from 'google-auth-library'
const GOOGLE_URL = process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'
const client = new OAuth2Client({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
redirectUri: 'http://localhost:3000/api/auth/callback/google',
})
const emulatorAuthorizeUrl = `${GOOGLE_URL}/o/oauth2/v2/auth?client_id=${process.env.GOOGLE_CLIENT_ID}&redirect_uri=...&scope=openid+email+profile&response_type=code&state=...`
import Google from '@auth/core/providers/google'
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
url: `${process.env.GOOGLE_EMULATOR_URL}/o/oauth2/v2/auth`,
params: { scope: 'openid email profile' },
},
token: {
url: `${process.env.GOOGLE_EMULATOR_URL}/oauth2/token`,
},
userinfo: {
url: `${process.env.GOOGLE_EMULATOR_URL}/oauth2/v2/userinfo`,
},
})
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'
const GOOGLE_URL = process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'
new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/api/auth/callback/google',
authorizationURL: `${GOOGLE_URL}/o/oauth2/v2/auth`,
tokenURL: `${GOOGLE_URL}/oauth2/token`,
userProfileURL: `${GOOGLE_URL}/oauth2/v2/userinfo`,
}, verifyCallback)
google:
users:
- email: [email protected]
name: Test User
given_name: Test
family_name: User
picture: https://lh3.googleusercontent.com/a/default-user
email_verified: true
locale: en
- email: [email protected]
name: Developer
- email: [email protected]
name: Admin
hd: acme.com
oauth_clients:
- client_id: my-client-id.apps.googleusercontent.com
client_secret: GOCSPX-secret
name: My App
redirect_uris:
- http://localhost:3000/api/auth/callback/google
labels:
- id: Label_ops
user_email: [email protected]
name: Ops/Review
color_background: "#DDEEFF"
color_text: "#111111"
messages:
- id: msg_welcome
user_email: [email protected]
thread_id: thr_welcome
from: "[email protected]"
to: [email protected]
subject: Welcome to the Gmail emulator
body_text: You can now test Gmail flows locally.
label_ids: [INBOX, UNREAD, CATEGORY_UPDATES]
date: "2025-01-04T10:00:00.000Z"
calendars:
- id: primary
user_email: [email protected]
summary: [email protected]
primary: true
selected: true
time_zone: UTC
calendar_events:
- id: evt_kickoff
user_email: [email protected]
calendar_id: primary
summary: Project Kickoff
start_date_time: "2025-01-10T09:00:00.000Z"
end_date_time: "2025-01-10T09:30:00.000Z"
attendees:
- email: [email protected]
display_name: Test User
conference_entry_points:
- entry_point_type: video
uri: https://meet.google.com/example
label: Google Meet
hangout_link: https://meet.google.com/example
drive_items:
- id: drv_docs
user_email: [email protected]
name: Docs
mime_type: application/vnd.google-apps.folder
parent_ids: [root]
- id: drv_readme
user_email: [email protected]
name: README.md
mime_type: text/markdown
parent_ids: [drv_docs]
data: "# Hello World"
When no OAuth clients are configured, the emulator accepts any client_id. With clients configured, strict validation is enforced for client_id, client_secret, and redirect_uri.
Google Workspace accounts include an hd claim in ID tokens and userinfo responses identifying the user's hosted domain. The emulator derives this automatically from the user's email domain. Consumer domains (gmail.com, googlemail.com) omit the claim, matching real Google behavior.
To override the derived value, set hd on a seeded user. To suppress the claim entirely, set hd to an empty string.
curl http://localhost:4002/.well-known/openid-configuration
curl http://localhost:4002/oauth2/v3/certs
Returns { "keys": [] }. ID tokens are signed with HS256 using an internal secret.
# Browser flow: redirects to a user picker page
curl -v "http://localhost:4002/o/oauth2/v2/auth?\
client_id=my-client-id.apps.googleusercontent.com&\
redirect_uri=http://localhost:3000/api/auth/callback/google&\
scope=openid+email+profile&\
response_type=code&\
state=random-state&\
nonce=random-nonce"
Supports code_challenge and code_challenge_method for PKCE.
curl -X POST http://localhost:4002/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "code=<authorization_code>&\
client_id=my-client-id.apps.googleusercontent.com&\
client_secret=GOCSPX-secret&\
redirect_uri=http://localhost:3000/api/auth/callback/google&\
grant_type=authorization_code"
Also accepts application/json body. Returns:
{
"access_token": "google_...",
"refresh_token": "google_refresh_...",
"id_token": "<jwt>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email profile"
}
curl -X POST http://localhost:4002/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "refresh_token=google_refresh_...&\
client_id=my-client-id.apps.googleusercontent.com&\
client_secret=GOCSPX-secret&\
grant_type=refresh_token"
Returns a new access_token (no new refresh_token or id_token on refresh).
curl http://localhost:4002/oauth2/v2/userinfo \
-H "Authorization: Bearer google_..."
curl -X POST http://localhost:4002/oauth2/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=google_..."
All Gmail endpoints are under /gmail/v1/users/:userId/... where :userId is me or the authenticated user's email.
# List messages (filter by labels, search query)
curl "http://localhost:4002/gmail/v1/users/me/messages?labelIds=INBOX&q=from:welcome&maxResults=10" \
-H "Authorization: Bearer $TOKEN"
# Get message (format: full, metadata, minimal, raw)
curl "http://localhost:4002/gmail/v1/users/me/messages/msg_welcome?format=full" \
-H "Authorization: Bearer $TOKEN"
# Send message
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to": "[email protected]", "subject": "Hello", "body_text": "Hi there"}'
# Insert message (bypass send)
curl -X POST http://localhost:4002/gmail/v1/users/me/messages \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to": "[email protected]", "from": "[email protected]", "subject": "Test", "body_text": "Body", "labelIds": ["INBOX"]}'
# Import message
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/import \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to": "[email protected]", "from": "[email protected]", "subject": "Imported", "body_text": "Content"}'
# Modify labels on a message
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/modify \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"addLabelIds": ["STARRED"], "removeLabelIds": ["UNREAD"]}'
# Trash / untrash
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/trash \
-H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/untrash \
-H "Authorization: Bearer $TOKEN"
# Delete permanently
curl -X DELETE http://localhost:4002/gmail/v1/users/me/messages/msg_welcome \
-H "Authorization: Bearer $TOKEN"
# Batch modify
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/batchModify \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"ids": ["msg_welcome", "msg_build"], "addLabelIds": ["STARRED"]}'
# Batch delete
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/batchDelete \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"ids": ["msg_welcome"]}'
# Get attachment
curl http://localhost:4002/gmail/v1/users/me/messages/msg_id/attachments/att_id \
-H "Authorization: Bearer $TOKEN"
Upload variants also available at /upload/gmail/v1/users/:userId/messages, .../messages/send, .../messages/import.
# List drafts
curl http://localhost:4002/gmail/v1/users/me/drafts \
-H "Authorization: Bearer $TOKEN"
# Create draft
curl -X POST http://localhost:4002/gmail/v1/users/me/drafts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"message": {"to": "[email protected]", "subject": "Draft subject", "body_text": "Draft body"}}'
# Get draft (format: full, metadata, minimal, raw)
curl "http://localhost:4002/gmail/v1/users/me/drafts/draft_id?format=full" \
-H "Authorization: Bearer $TOKEN"
# Update draft
curl -X PUT http://localhost:4002/gmail/v1/users/me/drafts/draft_id \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"message": {"subject": "Updated subject", "body_text": "Updated body"}}'
# Send draft
curl -X POST http://localhost:4002/gmail/v1/users/me/drafts/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": "draft_id"}'
# Delete draft
curl -X DELETE http://localhost:4002/gmail/v1/users/me/drafts/draft_id \
-H "Authorization: Bearer $TOKEN"
# List threads (filter by labels, search query)
curl "http://localhost:4002/gmail/v1/users/me/threads?labelIds=INBOX&maxResults=20" \
-H "Authorization: Bearer $TOKEN"
# Get thread (all messages in thread)
curl "http://localhost:4002/gmail/v1/users/me/threads/thr_welcome?format=full" \
-H "Authorization: Bearer $TOKEN"
# Modify labels on all messages in thread
curl -X POST http://localhost:4002/gmail/v1/users/me/threads/thr_welcome/modify \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"addLabelIds": ["STARRED"], "removeLabelIds": ["UNREAD"]}'
# Trash / untrash / delete thread
curl -X POST http://localhost:4002/gmail/v1/users/me/threads/thr_welcome/trash \
-H "Authorization: Bearer $TOKEN"
curl -X DELETE http://localhost:4002/gmail/v1/users/me/threads/thr_welcome \
-H "Authorization: Bearer $TOKEN"
# List labels
curl http://localhost:4002/gmail/v1/users/me/labels \
-H "Authorization: Bearer $TOKEN"
# Get label
curl http://localhost:4002/gmail/v1/users/me/labels/INBOX \
-H "Authorization: Bearer $TOKEN"
# Create label
curl -X POST http://localhost:4002/gmail/v1/users/me/labels \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "My Label", "color": {"backgroundColor": "#DDEEFF", "textColor": "#111111"}}'
# Update label (PUT replaces, PATCH merges)
curl -X PATCH http://localhost:4002/gmail/v1/users/me/labels/Label_ops \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Ops/Reviewed"}'
# Delete label (user labels only)
curl -X DELETE http://localhost:4002/gmail/v1/users/me/labels/Label_ops \
-H "Authorization: Bearer $TOKEN"
# List history changes since a given historyId
curl "http://localhost:4002/gmail/v1/users/me/history?startHistoryId=1&historyTypes=messageAdded&maxResults=100" \
-H "Authorization: Bearer $TOKEN"
# Set up push notification watch (stub)
curl -X POST http://localhost:4002/gmail/v1/users/me/watch \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"topicName": "projects/my-project/topics/gmail", "labelIds": ["INBOX"]}'
# Stop watch
curl -X POST http://localhost:4002/gmail/v1/users/me/stop \
-H "Authorization: Bearer $TOKEN"
# List filters
curl http://localhost:4002/gmail/v1/users/me/settings/filters \
-H "Authorization: Bearer $TOKEN"
# Create filter (auto-label incoming messages matching criteria)
curl -X POST http://localhost:4002/gmail/v1/users/me/settings/filters \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"criteria": {"from": "[email protected]"}, "action": {"addLabelIds": ["Label_ops"]}}'
# Delete filter
curl -X DELETE http://localhost:4002/gmail/v1/users/me/settings/filters/filter_id \
-H "Authorization: Bearer $TOKEN"
# List forwarding addresses
curl http://localhost:4002/gmail/v1/users/me/settings/forwardingAddresses \
-H "Authorization: Bearer $TOKEN"
# List send-as aliases
curl http://localhost:4002/gmail/v1/users/me/settings/sendAs \
-H "Authorization: Bearer $TOKEN"
curl http://localhost:4002/calendar/v3/users/me/calendarList \
-H "Authorization: Bearer $TOKEN"
# List events (filter by time range, search, order)
curl "http://localhost:4002/calendar/v3/calendars/primary/events?\
timeMin=2025-01-01T00:00:00Z&timeMax=2025-12-31T23:59:59Z&maxResults=50&orderBy=startTime" \
-H "Authorization: Bearer $TOKEN"
# Create event
curl -X POST http://localhost:4002/calendar/v3/calendars/primary/events \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"summary": "Team Meeting", "start": {"dateTime": "2025-01-10T14:00:00Z"}, "end": {"dateTime": "2025-01-10T15:00:00Z"}, "attendees": [{"email": "[email protected]"}]}'
# Delete event
curl -X DELETE http://localhost:4002/calendar/v3/calendars/primary/events/evt_kickoff \
-H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4002/calendar/v3/freeBusy \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"timeMin": "2025-01-10T00:00:00Z", "timeMax": "2025-01-10T23:59:59Z", "items": [{"id": "primary"}]}'
# List files (with query filter, pagination, ordering)
curl "http://localhost:4002/drive/v3/files?q='root'+in+parents&pageSize=20" \
-H "Authorization: Bearer $TOKEN"
# Create file (JSON metadata)
curl -X POST http://localhost:4002/drive/v3/files \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "notes.txt", "mimeType": "text/plain", "parents": ["root"]}'
# Create file with content (multipart/related upload)
curl -X POST http://localhost:4002/upload/drive/v3/files \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: multipart/related; boundary=boundary" \
--data-binary $'--boundary\r\nContent-Type: application/json\r\n\r\n{"name":"data.csv","mimeType":"text/csv"}\r\n--boundary\r\nContent-Type: text/csv\r\n\r\na,b,c\n1,2,3\r\n--boundary--'
# Get file metadata
curl http://localhost:4002/drive/v3/files/drv_readme \
-H "Authorization: Bearer $TOKEN"
# Download file content
curl "http://localhost:4002/drive/v3/files/drv_readme?alt=media" \
-H "Authorization: Bearer $TOKEN"
# Update file (PATCH or PUT; move parents with query params)
curl -X PATCH "http://localhost:4002/drive/v3/files/drv_readme?addParents=folder_id&removeParents=root" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "README-updated.md"}'
GOOGLE_URL="http://localhost:4002"
CLIENT_ID="my-client-id.apps.googleusercontent.com"
CLIENT_SECRET="GOCSPX-secret"
REDIRECT_URI="http://localhost:3000/api/auth/callback/google"
# 1. Open in browser (user picks a seeded account)
# $GOOGLE_URL/o/oauth2/v2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=openid+email+profile&response_type=code&state=abc
# 2. After user selection, emulator redirects to:
# $REDIRECT_URI?code=<code>&state=abc
# 3. Exchange code for tokens
curl -X POST $GOOGLE_URL/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "code=<code>&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&redirect_uri=$REDIRECT_URI&grant_type=authorization_code"
# 4. Fetch user info with the access_token
curl $GOOGLE_URL/oauth2/v2/userinfo \
-H "Authorization: Bearer <access_token>"
import { Issuer } from 'openid-client'
const googleIssuer = await Issuer.discover(
process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'
)
const client = new googleIssuer.Client({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uris: ['http://localhost:3000/api/auth/callback/google'],
})
TOKEN="test_token_admin"
BASE="http://localhost:4002"
# Send a message
curl -X POST $BASE/gmail/v1/users/me/messages/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to": "[email protected]", "subject": "Test", "body_text": "Hello"}'
# List threads in INBOX
curl "$BASE/gmail/v1/users/me/threads?labelIds=INBOX" \
-H "Authorization: Bearer $TOKEN"
Interactive daily standup/meeting update generator. Use when user says 'daily', 'standup', 'scrum update', 'status update', 'what did I do yesterday', 'prepare for meeting', 'morning update', or 'team sync'. Pulls activity from GitHub, Jira, and Claude Code session history. Conducts 4-question interview (yesterday, today, blockers, discussion topics) and generates formatted Markdown update.
Start your day with a prioritized sales briefing. Works standalone when you tell me your meetings and priorities, supercharged when you connect your calendar, CRM, and email. Trigger with "morning briefing", "daily brief", "what's on my plate today", "prep my day", or "start my day".
Full ICP-to-leads pipeline. Describe your ideal customer in plain English and get a ranked table of enriched decision-maker leads with emails and phone numbers.
Analyze meeting notes to find action items and create Jira tasks for assigned work. When an agent needs to: (1) Create Jira tasks or tickets from meeting notes, (2) Extract or find action items from notes or Confluence pages, (3) Parse meeting notes for assigned tasks, or (4) Analyze notes and generate tasks for team members. Identifies assignees, looks up account IDs, and creates tasks with proper context.
When the user wants to optimize post-signup onboarding, user activation, first-run experience, or time-to-value. Also use when the user mentions "onboarding flow," "activation rate," "user activation," "first-run experience," "empty states," "onboarding checklist," "aha moment," or "new user experience." For signup/registration optimization, see signup-flow-cro. For ongoing email sequences, see email-sequence.
Prepare for tomorrow''s meetings and tasks. Pulls calendar from Outlook via WorkIQ, cross-references open tasks and workspace context, classifies meetings, detects conflicts and day-fit issues, finds learning and deep-work slots, and generates a structured HTML prep file with productivity recommendations.
Design meeting rhythms, metric reporting, quarterly planning, and decision-making velocity for scaling companies. Use when decisions are slow, planning is broken, the company is growing but alignment is worse, or leadership meetings consume all time without producing decisions.
Add Google Calendar as an MCP tool (list calendars, list/search/create events, free/busy queries) using OneCLI-managed OAuth. Multi-calendar and multi-account supported. Mirrors /add-gmail-tool's stub pattern — no raw credentials ever reach the container; OneCLI injects real tokens at request time.
Take vercel-labs/google from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
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.
The instructions reference npx.
Without those the skill loads but fails at the first command.