mcpField
G
google

google

github.com/vercel-labs测试与质量2.0k365 次安装1 天前更新
代码仓库

关于 google

google 是用于本地模拟 Google OAuth 2.0、OpenID Connect、Gmail、Calendar 和 Drive API 的工具。开发者可使用预置数据测试认证与 Google API 工作流,而无需依赖真实 Google 服务。

主要功能

  1. 01模拟 OAuth 2.0 和 OIDC 流程
  2. 02支持 PKCE、令牌刷新与撤销
  3. 03提供 Gmail、Calendar 和 Drive API 接口
  4. 04预置用户和服务数据
  5. 05可配置范围校验

适用场景

  1. 01测试 OAuth 登录和令牌处理
  2. 02开发 Gmail、Calendar 或 Drive API 客户端
  3. 03检查基于范围的访问控制和授权失败

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vercel-labs/emulate --skill google

安装后在 Claude Code 中输入/google确认已加载。

详细介绍

Google OAuth 2.0 / OIDC + Gmail、Calendar 和 Drive 模拟器

模拟 OAuth 2.0 和 OpenID Connect,支持授权码流程、PKCE、ID 令牌、OIDC 发现、刷新令牌,以及 Gmail、Google Calendar 和 Google Drive REST API 接口。

启动

默认情况下,本地监听器绑定到 127.0.0.1。若要从容器或其他机器访问,请在 CLI 中传入 --host 0.0.0.0,或在 createEmulator 中传入 hostname: '0.0.0.0'。配置 --base-url 或 baseUrl,以设置这些客户端可访问的对外 URL。

# Google only
npx emulate --service google

# Default port
# http://localhost:4002

或者通过编程方式:

import { createEmulator } from 'emulate'

const google = await createEmulator({ service: 'google', port: 4002 })
// google.url === 'http://localhost:4002'

将应用指向模拟器

环境变量
GOOGLE_EMULATOR_URL=http://localhost:4002
OAuth URL 映射
真实 Google URL模拟器 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/...
google-auth-library(Node.js)
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=...`
Auth.js / NextAuth.js
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`,
  },
})
Passport.js
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"

未配置 OAuth 客户端时,模拟器接受任意 client_id。配置了客户端后,将严格验证 client_id、client_secret 和 redirect_uri。

对于本地 bearer token 工作流,Google 身份验证默认较为宽松。在种子配置中设置 google.strict_scopes: true,即可要求 Gmail、Calendar、Drive 和 userinfo 路由使用由模拟器颁发且未过期的访问令牌。凭据无效时返回 401 UNAUTHENTICATED;缺少方法所需的 scope 时返回 403 和 insufficientPermissions。严格模式会区分读取、写入、发送、设置和元数据 scope,userinfo 仅包含已授权的身份声明。撤销访问令牌或刷新令牌会使关联的访问凭据和刷新凭据失效。发现文档、签名密钥和登录页面仍保持公开。此模式会检查方法所需的 scope;不会模拟 drive.file 和 Gmail 插件 scope 等特定资源的授权。

Hosted domain(hd)声明

Google Workspace 账号会在 ID 令牌和 userinfo 响应中包含 hd 声明,用于标识用户的托管域名。模拟器会根据用户的电子邮件域名自动推导该值。与真实 Google 行为一致,消费者域名(gmail.com、googlemail.com)不会包含此声明。

若要覆盖推导出的值,请在种子用户上设置 hd。若要完全省略该声明,请将 hd 设置为空字符串。

OAuth / OIDC 端点

OIDC 发现
curl http://localhost:4002/.well-known/openid-configuration
JWKS
curl http://localhost:4002/oauth2/v3/certs

返回用于验证模拟器所颁发 ID 令牌的 RSA 公钥。Google ID 令牌使用 RS256,发现文档也会公布相同的算法。

授权
# 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"

支持用于 PKCE 的 code_challenge 和 code_challenge_method。

令牌交换
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"

也接受 application/json 请求体。返回:

{
  "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"

返回新的 access_token(刷新时不会返回新的 refresh_token 或 id_token)。

用户信息
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_..."

Gmail API

所有 Gmail 端点都位于 /gmail/v1/users/:userId/... 下,其中 :userId 为 me 或已通过身份验证的用户电子邮件地址。

邮件
# 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/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"

Google Calendar API

发现

Calendar 发现文档是公开的,描述了模拟器支持的 Calendar v3 方法:

curl http://localhost:4002/discovery/v1/apis/calendar/v3/rest
日历列表
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"}]}'

Google Drive API

文件
# 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>"
基于 OIDC 发现的设置
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'],
})
发送 Gmail 邮件并检查会话
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"

与内置模拟器一起使用自定义模拟器

使用 npx emulate init --custom inventory 搭建第三方 API 模拟器并进行测试。使用 emulate 中的 defineConfig 在 emulate.config.ts 中注册它,并与 { emulator: "google" } 等内置条目并列。运行 npx emulate start --watch 以重新加载导入项,并在输出的 /_emulate URL 中检查自定义状态。成功重新加载后,运行会重置为种子状态。现有的扁平种子配置仍可使用;--config 用于选择指定文件。有关编写和测试第三方 API 模拟器,请参阅 https://emulate.dev/docs/custom-emulators。

评论

支持 Markdown