Google Calendar MCP Server
Enables management of Google Calendar events across multiple calendars with natural language support, including searching events, checking availability, creating/updating/deleting events, responding to invitations, and auto-generating Google Meet links.
README
Google Calendar MCP Server
Streamable HTTP MCP server for Google Calendar — manage events, check availability, and schedule meetings.
Author: overment
[!WARNING] You connect this server to your MCP client at your own responsibility. Language models can make mistakes, misinterpret instructions, or perform unintended actions. Review tool outputs, verify changes (e.g., with
search_events), and prefer small, incremental writes.The HTTP/OAuth layer is designed for convenience during development, not production-grade security. If deploying remotely, harden it: proper token validation, secure storage, TLS termination, strict CORS/origin checks, rate limiting, audit logging, and compliance with Google's terms.
Notice
This repo works in two ways:
- As a Node/Hono server for local workflows
- As a Cloudflare Worker for remote interactions
For production Cloudflare deployments, see Remote Model Context Protocol servers (MCP).
Features
- ✅ Events — Search across ALL calendars, create, update, delete, respond to invitations
- ✅ Calendars — Discover available calendars
- ✅ Availability — Check free/busy status before scheduling
- ✅ Natural Language — Create events with text like "Lunch tomorrow at noon"
- ✅ Google Meet — Auto-create Meet links for events
- ✅ OAuth 2.1 — Secure PKCE flow with RS token mapping
- ✅ Dual Runtime — Node.js/Bun or Cloudflare Workers
Design Principles
- LLM-friendly: Tools are simplified and unified, not 1:1 API mirrors
- Smart defaults: Search all calendars, no notification spam, recurring expansion
- Just works:
search_eventssearches all calendars by default — no setup needed - Clear feedback: Every response includes which calendar each event belongs to
- Limited features: Due to the model's hallucinations, tools for managing calendars are not included. Ensure that you use the client that allows you to confirm dangerous actions, such as event deletion or updating.
Installation
Prerequisites: Bun, Node.js 20+, Google Cloud project. For remote: a Cloudflare account.
Ways to Run (Pick One)
- Local + OAuth — Standard setup with Google OAuth
- Cloudflare Worker (wrangler dev) — Local Worker testing
- Cloudflare Worker (deploy) — Remote production
1. Local + OAuth — Quick Start
-
Set up Google Cloud Console:
Create Project & Enable API:
- Go to Google Cloud Console
- Create a new project (or select existing)
- Navigate to APIs & Services > Library
- Search for "Google Calendar API" and click Enable
Configure OAuth Consent Screen (required before credentials):
- Go to APIs & Services > OAuth consent screen
- Select External (or Internal for Workspace)
- Fill in app name, user support email, developer email
- Add scopes:
../auth/calendar.events,../auth/calendar.readonly - Add your email as a Test user (required while app is in "Testing" mode)
- Save
Create Credentials:
- Go to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Application type: Web application
- Name: anything (e.g., "Google Calendar MCP")
- Authorized redirect URIs:
http://127.0.0.1:3001/oauth/callback - Click Create and copy Client ID and Client Secret
-
Configure environment:
git clone <repo>
cd google-calendar-mcp
bun install
cp env.example .env
Edit .env:
PORT=3000
AUTH_ENABLED=true
PROVIDER_CLIENT_ID=your_client_id
PROVIDER_CLIENT_SECRET=your_client_secret
OAUTH_SCOPES=https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly
OAUTH_REDIRECT_URI=http://127.0.0.1:3001/oauth/callback
OAUTH_REDIRECT_ALLOWLIST=alice://oauth/callback,http://127.0.0.1:3001/oauth/callback
- Run:
bun dev
# MCP: http://127.0.0.1:3000/mcp
# OAuth: http://127.0.0.1:3001
Tip: The Authorization Server runs on PORT+1 (3001 by default).
Claude Desktop / Cursor:
{
"mcpServers": {
"google-calendar": {
"command": "bunx",
"args": ["mcp-remote", "http://localhost:3000/mcp", "--transport", "http-only"],
"env": { "NO_PROXY": "127.0.0.1,localhost" }
}
}
}
Token Encryption (Recommended)
Generate an encryption key for secure token storage:
openssl rand -base64 32
Add to .env:
RS_TOKENS_ENC_KEY=your-32-byte-base64-key
2. Cloudflare Worker (Local Dev)
bun x wrangler dev --local | cat
With OAuth:
bun x wrangler secret put PROVIDER_CLIENT_ID
bun x wrangler secret put PROVIDER_CLIENT_SECRET
bun x wrangler dev --local | cat
Endpoint: http://127.0.0.1:8787/mcp
3. Cloudflare Worker (Deploy)
- Create KV namespace for token storage:
bun x wrangler kv:namespace create TOKENS
Output will show:
Add the following to your wrangler.toml:
[[kv_namespaces]]
binding = "TOKENS"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- Update
wrangler.tomlwith your KV namespace ID:
[[kv_namespaces]]
binding = "TOKENS"
id = "your-kv-namespace-id-from-step-1"
- Set secrets:
bun x wrangler secret put PROVIDER_CLIENT_ID
bun x wrangler secret put PROVIDER_CLIENT_SECRET
# Generate encryption key (32-byte base64url):
openssl rand -base64 32 | tr -d '=' | tr '+/' '-_'
bun x wrangler secret put TOKENS_ENC_KEY
Note:
TOKENS_ENC_KEYencrypts OAuth tokens stored in KV (AES-256-GCM). Without it, tokens are stored unencrypted!
- Update redirect URI in
wrangler.toml:
OAUTH_REDIRECT_URI = "https://your-worker.your-subdomain.workers.dev/oauth/callback"
OAUTH_REDIRECT_ALLOWLIST = "alice://oauth/callback,https://your-worker.your-subdomain.workers.dev/oauth/callback"
-
Add Workers URL to your Google Cloud OAuth app's redirect URIs
-
Deploy:
bun x wrangler deploy
Endpoint: https://<worker-name>.<account>.workers.dev/mcp
Client Configuration
Pre-authenticate (Recommended)
Claude Desktop has short timeouts that can kill the OAuth flow mid-process. Pre-authenticate manually first:
# Authenticate (complete Google sign-in when browser opens)
npx mcp-remote https://your-worker.workers.dev/mcp --transport http-only
Once you see "Authentication successful!", tokens are cached and Claude Desktop will use them.
Claude Desktop / Cursor (Local Server)
{
"mcpServers": {
"google-calendar": {
"command": "npx",
"args": ["mcp-remote", "http://127.0.0.1:3000/mcp", "--transport", "http-only"],
"env": { "NO_PROXY": "127.0.0.1,localhost" }
}
}
}
Claude Desktop / Cursor (Cloudflare Worker)
{
"mcpServers": {
"google-calendar": {
"command": "npx",
"args": ["mcp-remote", "https://your-worker.workers.dev/mcp", "--transport", "http-only"]
}
}
}
Node Version Issues (nvm users)
If you get ReadableStream is not defined or similar errors, Claude Desktop may be using an old Node version. Fix by specifying the full path:
{
"mcpServers": {
"google-calendar": {
"command": "/Users/YOUR_USER/.nvm/versions/node/v22.0.0/bin/npx",
"args": ["mcp-remote", "https://your-worker.workers.dev/mcp", "--transport", "http-only"],
"env": {
"PATH": "/Users/YOUR_USER/.nvm/versions/node/v22.0.0/bin:/usr/local/bin:/usr/bin:/bin"
}
}
}
}
Find your node path with: which node
MCP Inspector (Quick Test)
bunx @modelcontextprotocol/inspector
# Connect to: http://localhost:3000/mcp (local) or https://your-worker.workers.dev/mcp (remote)
Tools
list_calendars
Discover available calendars and their IDs. Usually not needed since search_events searches all calendars by default.
// Input
{}
// Output
{
items: Array<{
id, summary, primary?, backgroundColor?,
accessRole, timeZone, description?
}>;
}
search_events
Search events across all calendars by default. Returns merged results sorted by start time.
// Input
{
calendarId?: string | string[]; // Default: "all" (searches ALL calendars)
// Can be: "all", single ID, or array of IDs
timeMin?: string; // ISO 8601
timeMax?: string; // ISO 8601
query?: string; // Text search
maxResults?: number; // Default: 50 (total across all calendars)
eventTypes?: string[]; // default, birthday, focusTime, outOfOffice
orderBy?: "startTime" | "updated";
fields?: string[]; // Control output verbosity
pageToken?: string; // Pagination (single calendar only)
}
// Output
{
items: Array<{
id, summary, start, end, location?,
calendarId, calendarName, // NEW: which calendar this event belongs to
htmlLink, status, attendees?, hangoutLink?
}>;
calendarsSearched: string[]; // List of calendars that were searched
nextPageToken?: string;
}
Note: Each event includes
calendarIdandcalendarNameso you know which calendar it belongs to. Use thiscalendarIdwhen callingupdate_eventordelete_event.
check_availability
Check free/busy status before scheduling.
// Input
{
timeMin: string; // ISO 8601 (required)
timeMax: string; // ISO 8601 (required)
calendarIds?: string[]; // Default: ["primary"]
}
// Output
{
calendars: {
[calendarId]: {
busy: Array<{ start, end }>;
}
}
}
create_event
Create events using natural language OR structured input.
// Natural language mode
{
text: "Lunch with Anna tomorrow at noon for 1 hour";
calendarId?: string;
sendUpdates?: "all" | "externalOnly" | "none";
}
// Structured mode
{
summary: string; // Required
start: string; // ISO 8601 or YYYY-MM-DD
end: string; // ISO 8601 or YYYY-MM-DD
calendarId?: string;
description?: string;
location?: string;
attendees?: string[]; // Email addresses
addGoogleMeet?: boolean; // Auto-create Meet link
recurrence?: string[]; // RRULE array
visibility?: "default" | "public" | "private";
sendUpdates?: "all" | "externalOnly" | "none";
}
update_event
Update or move existing events (PATCH semantics).
{
eventId: string; // Required
calendarId?: string;
targetCalendarId?: string; // Move to different calendar
summary?: string;
start?: string;
end?: string;
description?: string;
location?: string;
attendees?: string[];
addGoogleMeet?: boolean;
sendUpdates?: "all" | "externalOnly" | "none";
}
delete_event
Remove an event from calendar.
{
eventId: string; // Required
calendarId?: string;
sendUpdates?: "all" | "externalOnly" | "none";
}
respond_to_event
Accept, decline, or tentatively accept an event invitation.
// Input
{
eventId: string; // Required
calendarId?: string; // Default: "primary"
response: "accepted" | "declined" | "tentative"; // Required
sendUpdates?: "all" | "externalOnly" | "none"; // Default: "all"
}
// Output
{
ok: true,
response: "accepted", // Your response
event: { ... } // Updated event
}
Note: Only works for events you were invited to. For events you created, you are the organizer, not an attendee.
Examples
1. List today's events (searches all calendars)
{
"name": "search_events",
"arguments": {
"timeMin": "2025-01-15T00:00:00Z",
"timeMax": "2025-01-15T23:59:59Z"
}
}
No need to call
list_calendarsfirst —search_eventssearches all accessible calendars by default and shows which calendar each event belongs to.
2. Create event with Google Meet
{
"name": "create_event",
"arguments": {
"summary": "Team Standup",
"start": "2025-01-16T09:00:00+01:00",
"end": "2025-01-16T09:30:00+01:00",
"addGoogleMeet": true,
"attendees": ["alice@example.com", "bob@example.com"]
}
}
3. Natural language event
{
"name": "create_event",
"arguments": {
"text": "Coffee with Sarah next Monday at 3pm for 30 minutes"
}
}
4. Check availability before scheduling
{
"name": "check_availability",
"arguments": {
"timeMin": "2025-01-16T09:00:00Z",
"timeMax": "2025-01-16T18:00:00Z"
}
}
HTTP Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/mcp |
POST | MCP JSON-RPC 2.0 |
/mcp |
GET | SSE stream (Node.js only) |
/health |
GET | Health check |
/.well-known/oauth-authorization-server |
GET | OAuth AS metadata |
/.well-known/oauth-protected-resource |
GET | OAuth RS metadata |
OAuth (PORT+1):
GET /authorize— Start OAuth flowGET /oauth/callback— Google callbackPOST /token— Token exchangePOST /revoke— Revoke tokens
Development
bun dev # Start with hot reload
bun run typecheck # TypeScript check
bun run lint # Lint code
bun run build # Production build
bun start # Run production
Architecture
src/
├── shared/
│ ├── tools/
│ │ ├── list-calendars.ts
│ │ ├── search-events.ts
│ │ ├── check-availability.ts
│ │ ├── create-event.ts
│ │ ├── update-event.ts
│ │ └── delete-event.ts
│ ├── oauth/ # OAuth flow (PKCE, discovery)
│ └── storage/ # Token storage (file, KV, memory)
├── services/
│ └── google-calendar.ts # Google Calendar API client
├── config/
│ └── metadata.ts # Server & tool descriptions
├── index.ts # Node.js entry
└── worker.ts # Workers entry
Troubleshooting
| Issue | Solution |
|---|---|
| "Authentication required" | Complete OAuth flow. Run rm -rf ~/.mcp-auth and re-authenticate. |
| "redirect_uri_mismatch" | Google treats localhost and 127.0.0.1 as different. Use 127.0.0.1 consistently in both .env and Google Cloud Console. |
| "unknown_txn" error | Stale mcp-remote processes. Run pkill -9 -f mcp-remote && rm -rf ~/.mcp-auth then retry. |
| "ReadableStream is not defined" | Node.js version too old (needs 18+). Use full path to newer node in config. |
| "spawn bunx ENOENT" | Claude Desktop can't find bunx. Use npx instead, or specify full path. |
| "Another instance handling auth" | Kill zombie processes: pkill -9 -f mcp-remote && rm -rf ~/.mcp-auth |
| OAuth timeout in Claude | Claude kills auth flow too quickly. Pre-authenticate manually (see Client Configuration). |
| Token expired | Google tokens expire after 1 hour. Refresh tokens are used automatically if access_type=offline was set. |
| OAuth doesn't start (Worker) | curl -i -X POST https://<worker>/mcp should return 401 with WWW-Authenticate. |
| KV namespace error | Run wrangler kv:namespace create TOKENS and update wrangler.toml with the ID. |
| Tools empty in Claude | Ensure Worker returns JSON Schema for tools/list; use mcp-remote. |
Debugging
Enable detailed logs with --debug:
npx mcp-remote https://your-worker.workers.dev/mcp --transport http-only --debug
Logs are written to ~/.mcp-auth/{hash}_debug.log.
Test auth flow independently:
npx -p mcp-remote@latest mcp-remote-client https://your-worker.workers.dev/mcp --transport http-only --debug
Environment Variables
Node.js (.env)
| Variable | Required | Description |
|---|---|---|
PROVIDER_CLIENT_ID |
✓ | Google OAuth Client ID |
PROVIDER_CLIENT_SECRET |
✓ | Google OAuth Client Secret |
RS_TOKENS_ENC_KEY |
Prod | 32-byte base64 key for token encryption |
PORT |
MCP server port (default: 3000) | |
HOST |
Server host (default: 127.0.0.1) | |
LOG_LEVEL |
debug, info, warning, error | |
OAUTH_REDIRECT_URI |
Callback URL for OAuth | |
OAUTH_REDIRECT_ALLOWLIST |
Comma-separated allowed redirect URIs |
Cloudflare Workers (wrangler.toml + secrets)
wrangler.toml vars:
AUTH_ENABLED = "true"
AUTH_STRATEGY = "oauth"
OAUTH_SCOPES = "https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly"
OAUTH_REDIRECT_URI = "https://your-worker.workers.dev/oauth/callback"
Secrets (set via wrangler secret put):
PROVIDER_CLIENT_ID— Google OAuth Client IDPROVIDER_CLIENT_SECRET— Google OAuth Client SecretTOKENS_ENC_KEY— 32-byte base64url encryption key
KV Namespace:
[[kv_namespaces]]
binding = "TOKENS"
id = "your-kv-namespace-id"
License
MIT
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。