# Directory Structure ``` ├── .gitignore ├── docker-compose.yml ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* # Build build/ dist/ *.tsbuildinfo # Environment .env .env.local .env.*.local # IDE .idea/ .vscode/ *.swp *.swo # OS .DS_Store Thumbs.db # Project specific gcp-oauth.keys.json .calendar-mcp/ credentials.json .calendar-server-credentials.json ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Calendar AutoAuth MCP Server A Model Context Protocol (MCP) server for Google Calendar integration in Cluade Desktop with auto authentication support. This server enables AI assistants to manage Google Calendar events through natural language interactions.  [](https://smithery.ai/server/@gongrzhe/server-calendar-autoauth-mcp) [](https://www.npmjs.com/package/@gongrzhe/server-calendar-autoauth-mcp) [](https://opensource.org/licenses/ISC) ## Features - Create calendar events with title, time, description, and location - Retrieve event details by event ID - Update existing events (title, time, description, location) - Delete events - List events within a specified time range - Full integration with Google Calendar API - Simple OAuth2 authentication flow with auto browser launch - Support for both Desktop and Web application credentials - Global credential storage for convenience ## Installation & Authentication ### Installing via Smithery To install Calendar AutoAuth Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@gongrzhe/server-calendar-autoauth-mcp): ```bash npx -y @smithery/cli install @gongrzhe/server-calendar-autoauth-mcp --client claude ``` 1. Create a Google Cloud Project and obtain credentials: a. Create a Google Cloud Project: - Go to [Google Cloud Console](https://console.cloud.google.com/) - Create a new project or select an existing one - Enable the Google Calendar API for your project b. Create OAuth 2.0 Credentials: - Go to "APIs & Services" > "Credentials" - Click "Create Credentials" > "OAuth client ID" - Choose either "Desktop app" or "Web application" as application type - Give it a name and click "Create" - For Web application, add `http://localhost:3000/oauth2callback` to the authorized redirect URIs - Download the JSON file of your client's OAuth keys - Rename the key file to `gcp-oauth.keys.json` 2. Run Authentication: You can authenticate in two ways: a. Global Authentication (Recommended): ```bash # First time: Place gcp-oauth.keys.json in your home directory's .calendar-mcp folder mkdir -p ~/.calendar-mcp mv gcp-oauth.keys.json ~/.calendar-mcp/ # Run authentication from anywhere npx @gongrzhe/server-calendar-autoauth-mcp auth ``` b. Local Authentication: ```bash # Place gcp-oauth.keys.json in your current directory # The file will be automatically copied to global config npx @gongrzhe/server-calendar-autoauth-mcp auth ``` The authentication process will: - Look for `gcp-oauth.keys.json` in the current directory or `~/.calendar-mcp/` - If found in current directory, copy it to `~/.calendar-mcp/` - Open your default browser for Google authentication - Save credentials as `~/.calendar-mcp/credentials.json` > **Note**: > - After successful authentication, credentials are stored globally in `~/.calendar-mcp/` and can be used from any directory > - Both Desktop app and Web application credentials are supported > - For Web application credentials, make sure to add `http://localhost:3000/oauth2callback` to your authorized redirect URIs 3. Configure in Claude Desktop: ```json { "mcpServers": { "calendar": { "command": "npx", "args": [ "@gongrzhe/server-calendar-autoauth-mcp" ] } } } ``` ### Docker Support If you prefer using Docker: 1. Authentication: ```bash docker run -i --rm \ --mount type=bind,source=/path/to/gcp-oauth.keys.json,target=/gcp-oauth.keys.json \ -v mcp-calendar:/calendar-server \ -e CALENDAR_OAUTH_PATH=/gcp-oauth.keys.json \ -e "CALENDAR_CREDENTIALS_PATH=/calendar-server/credentials.json" \ -p 3000:3000 \ mcp/calendar auth ``` 2. Usage: ```json { "mcpServers": { "calendar": { "command": "docker", "args": [ "run", "-i", "--rm", "-v", "mcp-calendar:/calendar-server", "-e", "CALENDAR_CREDENTIALS_PATH=/calendar-server/credentials.json", "mcp/calendar" ] } } } ``` ## Usage Examples The server provides several tools that can be used through the Claude Desktop: ### Create Event ```json { "summary": "Team Meeting", "start": { "dateTime": "2024-01-20T10:00:00Z" }, "end": { "dateTime": "2024-01-20T11:00:00Z" }, "description": "Weekly team sync", "location": "Conference Room A" } ``` ### List Events ```json { "timeMin": "2024-01-01T00:00:00Z", "timeMax": "2024-12-31T23:59:59Z", "maxResults": 10, "orderBy": "startTime" } ``` ### Update Event ```json { "eventId": "event123", "summary": "Updated Meeting Title", "start": { "dateTime": "2024-01-20T11:00:00Z" }, "end": { "dateTime": "2024-01-20T12:00:00Z" } } ``` ### Delete Event ```json { "eventId": "event123" } ``` ## Security Notes - OAuth credentials are stored securely in your local environment (`~/.calendar-mcp/`) - The server uses offline access to maintain persistent authentication - Never share or commit your credentials to version control - Regularly review and revoke unused access in your Google Account settings - Credentials are stored globally but are only accessible by the current user ## Troubleshooting 1. **OAuth Keys Not Found** - Make sure `gcp-oauth.keys.json` is in either your current directory or `~/.calendar-mcp/` - Check file permissions 2. **Invalid Credentials Format** - Ensure your OAuth keys file contains either `web` or `installed` credentials - For web applications, verify the redirect URI is correctly configured 3. **Port Already in Use** - If port 3000 is already in use, please free it up before running authentication - You can find and stop the process using that port ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License This project is licensed under the ISC License. ## Author gongrzhe ## Support If you encounter any issues or have questions, please file an issue on the GitHub repository. ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml version: '3.8' services: redis: image: redis:latest container_name: my-redis ports: - "6379:6379" volumes: - redis_data:/data command: redis-server --appendonly yes restart: always volumes: redis_data: ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "ES2020", "moduleResolution": "node", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "build"] } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile FROM node:20-alpine WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm install # Copy source code COPY . . # Build the application RUN npm run build # Create data directory RUN mkdir -p /app/calendar-data # Set permissions for the data directory RUN chown -R node:node /app/calendar-data # Switch to non-root user USER node # Start the server CMD ["node", "build/index.js"] ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: - calendarOauthPath - calendarCredentialsPath properties: calendarOauthPath: type: string default: ~/.calendar-mcp/gcp-oauth.keys.json description: Path to the Google OAuth credentials file. calendarCredentialsPath: type: string default: ~/.calendar-mcp/credentials.json description: Path where the OAuth tokens will be stored. commandFunction: # A function that produces the CLI command to start the MCP on stdio. |- (config) => ({ command: 'node', args: ['build/index.js'], env: { CALENDAR_OAUTH_PATH: config.calendarOauthPath, CALENDAR_CREDENTIALS_PATH: config.calendarCredentialsPath } }) ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@gongrzhe/server-calendar-autoauth-mcp", "version": "1.0.2", "description": "A Model Context Protocol server for Google Calendar integration with auto authentication", "main": "build/index.js", "type": "module", "bin": { "server-calendar-autoauth-mcp": "./build/index.js" }, "scripts": { "build": "tsc", "prepublishOnly": "npm run build", "auth": "node ./build/index.js auth" }, "files": [ "build", "README.md" ], "keywords": [ "calendar", "events", "scheduling", "mcp", "model-context-protocol", "google-calendar", "claude", "cursor", "auto-auth" ], "author": "gongrzhe", "license": "ISC", "repository": { "type": "git", "url": "git+https://github.com/gongrzhe/server-calendar-autoauth-mcp.git" }, "bugs": { "url": "https://github.com/gongrzhe/server-calendar-autoauth-mcp/issues" }, "homepage": "https://github.com/gongrzhe/server-calendar-autoauth-mcp#readme", "publishConfig": { "access": "public" }, "devDependencies": { "@types/node": "^22.10.2", "@types/open": "^6.2.1", "typescript": "^5.7.2" }, "dependencies": { "@modelcontextprotocol/sdk": "^0.4.0", "googleapis": "^133.0.0", "open": "^8.4.2", "zod": "^3.24.1", "zod-to-json-schema": "^3.22.4" }, "engines": { "node": ">=14.0.0" } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { google } from 'googleapis'; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import { OAuth2Client } from 'google-auth-library'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import http from 'http'; import open from 'open'; import os from 'os'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Configuration paths const CONFIG_DIR = path.join(os.homedir(), '.calendar-mcp'); const OAUTH_PATH = process.env.CALENDAR_OAUTH_PATH || path.join(CONFIG_DIR, 'gcp-oauth.keys.json'); const CREDENTIALS_PATH = process.env.CALENDAR_CREDENTIALS_PATH || path.join(CONFIG_DIR, 'credentials.json'); // OAuth2 configuration let oauth2Client: OAuth2Client; async function loadCredentials() { try { // Create config directory if it doesn't exist if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } // Check for OAuth keys in current directory first, then in config directory const localOAuthPath = path.join(process.cwd(), 'gcp-oauth.keys.json'); let oauthPath = OAUTH_PATH; if (fs.existsSync(localOAuthPath)) { // If found in current directory, copy to config directory fs.copyFileSync(localOAuthPath, OAUTH_PATH); console.log('OAuth keys found in current directory, copied to global config.'); } if (!fs.existsSync(OAUTH_PATH)) { console.error('Error: OAuth keys file not found. Please place gcp-oauth.keys.json in current directory or', CONFIG_DIR); process.exit(1); } const keysContent = JSON.parse(fs.readFileSync(OAUTH_PATH, 'utf8')); const keys = keysContent.installed || keysContent.web; if (!keys) { console.error('Error: Invalid OAuth keys file format. File should contain either "installed" or "web" credentials.'); process.exit(1); } oauth2Client = new OAuth2Client( keys.client_id, keys.client_secret, 'http://localhost:3000/oauth2callback' ); if (fs.existsSync(CREDENTIALS_PATH)) { const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8')); oauth2Client.setCredentials(credentials); } } catch (error) { console.error('Error loading credentials:', error); process.exit(1); } } async function authenticate() { const server = http.createServer(); server.listen(3000); return new Promise<void>((resolve, reject) => { const authUrl = oauth2Client.generateAuthUrl({ access_type: 'offline', scope: ['https://www.googleapis.com/auth/calendar'], }); console.log('Please visit this URL to authenticate:', authUrl); open(authUrl); server.on('request', async (req, res) => { if (!req.url?.startsWith('/oauth2callback')) return; const url = new URL(req.url, 'http://localhost:3000'); const code = url.searchParams.get('code'); if (!code) { res.writeHead(400); res.end('No code provided'); reject(new Error('No code provided')); return; } try { const { tokens } = await oauth2Client.getToken(code); oauth2Client.setCredentials(tokens); fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(tokens)); res.writeHead(200); res.end('Authentication successful! You can close this window.'); server.close(); resolve(); } catch (error) { res.writeHead(500); res.end('Authentication failed'); reject(error); } }); }); } // Schema definitions const CreateEventSchema = z.object({ summary: z.string().describe("Event title"), start: z.object({ dateTime: z.string().describe("Start time (ISO format)"), timeZone: z.string().optional().describe("Time zone"), }), end: z.object({ dateTime: z.string().describe("End time (ISO format)"), timeZone: z.string().optional().describe("Time zone"), }), description: z.string().optional().describe("Event description"), location: z.string().optional().describe("Event location"), }); const GetEventSchema = z.object({ eventId: z.string().describe("ID of the event to retrieve"), }); const UpdateEventSchema = z.object({ eventId: z.string().describe("ID of the event to update"), summary: z.string().optional().describe("New event title"), start: z.object({ dateTime: z.string().describe("New start time (ISO format)"), timeZone: z.string().optional().describe("Time zone"), }).optional(), end: z.object({ dateTime: z.string().describe("New end time (ISO format)"), timeZone: z.string().optional().describe("Time zone"), }).optional(), description: z.string().optional().describe("New event description"), location: z.string().optional().describe("New event location"), }); const DeleteEventSchema = z.object({ eventId: z.string().describe("ID of the event to delete"), }); const ListEventsSchema = z.object({ timeMin: z.string().describe("Start of time range (ISO format)"), timeMax: z.string().describe("End of time range (ISO format)"), maxResults: z.number().optional().describe("Maximum number of events to return"), orderBy: z.enum(['startTime', 'updated']).optional().describe("Sort order"), }); // Main function async function main() { await loadCredentials(); if (process.argv[2] === 'auth') { await authenticate(); console.log('Authentication completed successfully'); process.exit(0); } // Initialize Google Calendar API const calendar = google.calendar({ version: 'v3', auth: oauth2Client }); const calendarId = 'primary'; // Server implementation const server = new Server({ name: "google-calendar", version: "1.0.0", capabilities: { tools: {}, }, }); // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "create_event", description: "Creates a new event in Google Calendar", inputSchema: zodToJsonSchema(CreateEventSchema), }, { name: "get_event", description: "Retrieves details of a specific event", inputSchema: zodToJsonSchema(GetEventSchema), }, { name: "update_event", description: "Updates an existing event", inputSchema: zodToJsonSchema(UpdateEventSchema), }, { name: "delete_event", description: "Deletes an event from the calendar", inputSchema: zodToJsonSchema(DeleteEventSchema), }, { name: "list_events", description: "Lists events within a specified time range", inputSchema: zodToJsonSchema(ListEventsSchema), }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "create_event": { const validatedArgs = CreateEventSchema.parse(args); const response = await calendar.events.insert({ calendarId, requestBody: validatedArgs, }); return { content: [ { type: "text", text: `Event created with ID: ${response.data.id}\n` + `Title: ${validatedArgs.summary}\n` + `Start: ${validatedArgs.start.dateTime}\n` + `End: ${validatedArgs.end.dateTime}`, }, ], }; } case "get_event": { const validatedArgs = GetEventSchema.parse(args); const response = await calendar.events.get({ calendarId, eventId: validatedArgs.eventId, }); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2), }, ], }; } case "update_event": { const validatedArgs = UpdateEventSchema.parse(args); const { eventId, ...updates } = validatedArgs; const response = await calendar.events.patch({ calendarId, eventId, requestBody: updates, }); return { content: [ { type: "text", text: `Event updated: ${eventId}\n` + `New title: ${updates.summary || '(unchanged)'}\n` + `New start: ${updates.start?.dateTime || '(unchanged)'}\n` + `New end: ${updates.end?.dateTime || '(unchanged)'}`, }, ], }; } case "delete_event": { const validatedArgs = DeleteEventSchema.parse(args); await calendar.events.delete({ calendarId, eventId: validatedArgs.eventId, }); return { content: [ { type: "text", text: `Event deleted: ${validatedArgs.eventId}`, }, ], }; } case "list_events": { const validatedArgs = ListEventsSchema.parse(args); const response = await calendar.events.list({ calendarId, timeMin: validatedArgs.timeMin, timeMax: validatedArgs.timeMax, maxResults: validatedArgs.maxResults || 10, orderBy: validatedArgs.orderBy || 'startTime', singleEvents: true, }); return { content: [ { type: "text", text: `Found ${response.data.items?.length || 0} events:\n` + JSON.stringify(response.data.items, null, 2), }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // Start the server const transport = new StdioServerTransport(); server.connect(transport).catch((error) => { console.error("Fatal error running server:", error); process.exit(1); }); console.error('Google Calendar MCP Server running on stdio'); } main().catch(console.error); ```