# Directory Structure ``` ├── .gitignore ├── docker-compose.yml ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build 8 | build/ 9 | dist/ 10 | *.tsbuildinfo 11 | 12 | # Environment 13 | .env 14 | .env.local 15 | .env.*.local 16 | 17 | # IDE 18 | .idea/ 19 | .vscode/ 20 | *.swp 21 | *.swo 22 | 23 | # OS 24 | .DS_Store 25 | Thumbs.db 26 | 27 | # Project specific 28 | gcp-oauth.keys.json 29 | .calendar-mcp/ 30 | credentials.json 31 | .calendar-server-credentials.json ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Calendar AutoAuth MCP Server 2 | 3 | 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. 4 | 5 |  6 | [](https://smithery.ai/server/@gongrzhe/server-calendar-autoauth-mcp) 7 | [](https://www.npmjs.com/package/@gongrzhe/server-calendar-autoauth-mcp) 8 | [](https://opensource.org/licenses/ISC) 9 | 10 | ## Features 11 | 12 | - Create calendar events with title, time, description, and location 13 | - Retrieve event details by event ID 14 | - Update existing events (title, time, description, location) 15 | - Delete events 16 | - List events within a specified time range 17 | - Full integration with Google Calendar API 18 | - Simple OAuth2 authentication flow with auto browser launch 19 | - Support for both Desktop and Web application credentials 20 | - Global credential storage for convenience 21 | 22 | ## Installation & Authentication 23 | 24 | ### Installing via Smithery 25 | 26 | To install Calendar AutoAuth Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@gongrzhe/server-calendar-autoauth-mcp): 27 | 28 | ```bash 29 | npx -y @smithery/cli install @gongrzhe/server-calendar-autoauth-mcp --client claude 30 | ``` 31 | 32 | 1. Create a Google Cloud Project and obtain credentials: 33 | 34 | a. Create a Google Cloud Project: 35 | - Go to [Google Cloud Console](https://console.cloud.google.com/) 36 | - Create a new project or select an existing one 37 | - Enable the Google Calendar API for your project 38 | 39 | b. Create OAuth 2.0 Credentials: 40 | - Go to "APIs & Services" > "Credentials" 41 | - Click "Create Credentials" > "OAuth client ID" 42 | - Choose either "Desktop app" or "Web application" as application type 43 | - Give it a name and click "Create" 44 | - For Web application, add `http://localhost:3000/oauth2callback` to the authorized redirect URIs 45 | - Download the JSON file of your client's OAuth keys 46 | - Rename the key file to `gcp-oauth.keys.json` 47 | 48 | 2. Run Authentication: 49 | 50 | You can authenticate in two ways: 51 | 52 | a. Global Authentication (Recommended): 53 | ```bash 54 | # First time: Place gcp-oauth.keys.json in your home directory's .calendar-mcp folder 55 | mkdir -p ~/.calendar-mcp 56 | mv gcp-oauth.keys.json ~/.calendar-mcp/ 57 | 58 | # Run authentication from anywhere 59 | npx @gongrzhe/server-calendar-autoauth-mcp auth 60 | ``` 61 | 62 | b. Local Authentication: 63 | ```bash 64 | # Place gcp-oauth.keys.json in your current directory 65 | # The file will be automatically copied to global config 66 | npx @gongrzhe/server-calendar-autoauth-mcp auth 67 | ``` 68 | 69 | The authentication process will: 70 | - Look for `gcp-oauth.keys.json` in the current directory or `~/.calendar-mcp/` 71 | - If found in current directory, copy it to `~/.calendar-mcp/` 72 | - Open your default browser for Google authentication 73 | - Save credentials as `~/.calendar-mcp/credentials.json` 74 | 75 | > **Note**: 76 | > - After successful authentication, credentials are stored globally in `~/.calendar-mcp/` and can be used from any directory 77 | > - Both Desktop app and Web application credentials are supported 78 | > - For Web application credentials, make sure to add `http://localhost:3000/oauth2callback` to your authorized redirect URIs 79 | 80 | 3. Configure in Claude Desktop: 81 | 82 | ```json 83 | { 84 | "mcpServers": { 85 | "calendar": { 86 | "command": "npx", 87 | "args": [ 88 | "@gongrzhe/server-calendar-autoauth-mcp" 89 | ] 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | ### Docker Support 96 | 97 | If you prefer using Docker: 98 | 99 | 1. Authentication: 100 | ```bash 101 | docker run -i --rm \ 102 | --mount type=bind,source=/path/to/gcp-oauth.keys.json,target=/gcp-oauth.keys.json \ 103 | -v mcp-calendar:/calendar-server \ 104 | -e CALENDAR_OAUTH_PATH=/gcp-oauth.keys.json \ 105 | -e "CALENDAR_CREDENTIALS_PATH=/calendar-server/credentials.json" \ 106 | -p 3000:3000 \ 107 | mcp/calendar auth 108 | ``` 109 | 110 | 2. Usage: 111 | ```json 112 | { 113 | "mcpServers": { 114 | "calendar": { 115 | "command": "docker", 116 | "args": [ 117 | "run", 118 | "-i", 119 | "--rm", 120 | "-v", 121 | "mcp-calendar:/calendar-server", 122 | "-e", 123 | "CALENDAR_CREDENTIALS_PATH=/calendar-server/credentials.json", 124 | "mcp/calendar" 125 | ] 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | ## Usage Examples 132 | 133 | The server provides several tools that can be used through the Claude Desktop: 134 | 135 | ### Create Event 136 | ```json 137 | { 138 | "summary": "Team Meeting", 139 | "start": { 140 | "dateTime": "2024-01-20T10:00:00Z" 141 | }, 142 | "end": { 143 | "dateTime": "2024-01-20T11:00:00Z" 144 | }, 145 | "description": "Weekly team sync", 146 | "location": "Conference Room A" 147 | } 148 | ``` 149 | 150 | ### List Events 151 | ```json 152 | { 153 | "timeMin": "2024-01-01T00:00:00Z", 154 | "timeMax": "2024-12-31T23:59:59Z", 155 | "maxResults": 10, 156 | "orderBy": "startTime" 157 | } 158 | ``` 159 | 160 | ### Update Event 161 | ```json 162 | { 163 | "eventId": "event123", 164 | "summary": "Updated Meeting Title", 165 | "start": { 166 | "dateTime": "2024-01-20T11:00:00Z" 167 | }, 168 | "end": { 169 | "dateTime": "2024-01-20T12:00:00Z" 170 | } 171 | } 172 | ``` 173 | 174 | ### Delete Event 175 | ```json 176 | { 177 | "eventId": "event123" 178 | } 179 | ``` 180 | 181 | ## Security Notes 182 | 183 | - OAuth credentials are stored securely in your local environment (`~/.calendar-mcp/`) 184 | - The server uses offline access to maintain persistent authentication 185 | - Never share or commit your credentials to version control 186 | - Regularly review and revoke unused access in your Google Account settings 187 | - Credentials are stored globally but are only accessible by the current user 188 | 189 | ## Troubleshooting 190 | 191 | 1. **OAuth Keys Not Found** 192 | - Make sure `gcp-oauth.keys.json` is in either your current directory or `~/.calendar-mcp/` 193 | - Check file permissions 194 | 195 | 2. **Invalid Credentials Format** 196 | - Ensure your OAuth keys file contains either `web` or `installed` credentials 197 | - For web applications, verify the redirect URI is correctly configured 198 | 199 | 3. **Port Already in Use** 200 | - If port 3000 is already in use, please free it up before running authentication 201 | - You can find and stop the process using that port 202 | 203 | ## Contributing 204 | 205 | Contributions are welcome! Please feel free to submit a Pull Request. 206 | 207 | ## License 208 | 209 | This project is licensed under the ISC License. 210 | 211 | ## Author 212 | 213 | gongrzhe 214 | 215 | ## Support 216 | 217 | If you encounter any issues or have questions, please file an issue on the GitHub repository. 218 | ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml 1 | version: '3.8' 2 | 3 | services: 4 | redis: 5 | image: redis:latest 6 | container_name: my-redis 7 | ports: 8 | - "6379:6379" 9 | volumes: 10 | - redis_data:/data 11 | command: redis-server --appendonly yes 12 | restart: always 13 | 14 | volumes: 15 | redis_data: ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "build"] 15 | } 16 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | # Copy package files 6 | COPY package*.json ./ 7 | 8 | # Install dependencies 9 | RUN npm install 10 | 11 | # Copy source code 12 | COPY . . 13 | 14 | # Build the application 15 | RUN npm run build 16 | 17 | # Create data directory 18 | RUN mkdir -p /app/calendar-data 19 | 20 | # Set permissions for the data directory 21 | RUN chown -R node:node /app/calendar-data 22 | 23 | # Switch to non-root user 24 | USER node 25 | 26 | # Start the server 27 | CMD ["node", "build/index.js"] ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - calendarOauthPath 10 | - calendarCredentialsPath 11 | properties: 12 | calendarOauthPath: 13 | type: string 14 | default: ~/.calendar-mcp/gcp-oauth.keys.json 15 | description: Path to the Google OAuth credentials file. 16 | calendarCredentialsPath: 17 | type: string 18 | default: ~/.calendar-mcp/credentials.json 19 | description: Path where the OAuth tokens will be stored. 20 | commandFunction: 21 | # A function that produces the CLI command to start the MCP on stdio. 22 | |- 23 | (config) => ({ command: 'node', args: ['build/index.js'], env: { CALENDAR_OAUTH_PATH: config.calendarOauthPath, CALENDAR_CREDENTIALS_PATH: config.calendarCredentialsPath } }) ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@gongrzhe/server-calendar-autoauth-mcp", 3 | "version": "1.0.2", 4 | "description": "A Model Context Protocol server for Google Calendar integration with auto authentication", 5 | "main": "build/index.js", 6 | "type": "module", 7 | "bin": { 8 | "server-calendar-autoauth-mcp": "./build/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "prepublishOnly": "npm run build", 13 | "auth": "node ./build/index.js auth" 14 | }, 15 | "files": [ 16 | "build", 17 | "README.md" 18 | ], 19 | "keywords": [ 20 | "calendar", 21 | "events", 22 | "scheduling", 23 | "mcp", 24 | "model-context-protocol", 25 | "google-calendar", 26 | "claude", 27 | "cursor", 28 | "auto-auth" 29 | ], 30 | "author": "gongrzhe", 31 | "license": "ISC", 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/gongrzhe/server-calendar-autoauth-mcp.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/gongrzhe/server-calendar-autoauth-mcp/issues" 38 | }, 39 | "homepage": "https://github.com/gongrzhe/server-calendar-autoauth-mcp#readme", 40 | "publishConfig": { 41 | "access": "public" 42 | }, 43 | "devDependencies": { 44 | "@types/node": "^22.10.2", 45 | "@types/open": "^6.2.1", 46 | "typescript": "^5.7.2" 47 | }, 48 | "dependencies": { 49 | "@modelcontextprotocol/sdk": "^0.4.0", 50 | "googleapis": "^133.0.0", 51 | "open": "^8.4.2", 52 | "zod": "^3.24.1", 53 | "zod-to-json-schema": "^3.22.4" 54 | }, 55 | "engines": { 56 | "node": ">=14.0.0" 57 | } 58 | } 59 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | } from "@modelcontextprotocol/sdk/types.js"; 9 | import { google } from 'googleapis'; 10 | import { z } from "zod"; 11 | import { zodToJsonSchema } from "zod-to-json-schema"; 12 | import { OAuth2Client } from 'google-auth-library'; 13 | import fs from 'fs'; 14 | import path from 'path'; 15 | import { fileURLToPath } from 'url'; 16 | import http from 'http'; 17 | import open from 'open'; 18 | import os from 'os'; 19 | 20 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 21 | 22 | // Configuration paths 23 | const CONFIG_DIR = path.join(os.homedir(), '.calendar-mcp'); 24 | const OAUTH_PATH = process.env.CALENDAR_OAUTH_PATH || path.join(CONFIG_DIR, 'gcp-oauth.keys.json'); 25 | const CREDENTIALS_PATH = process.env.CALENDAR_CREDENTIALS_PATH || path.join(CONFIG_DIR, 'credentials.json'); 26 | 27 | // OAuth2 configuration 28 | let oauth2Client: OAuth2Client; 29 | 30 | async function loadCredentials() { 31 | try { 32 | // Create config directory if it doesn't exist 33 | if (!fs.existsSync(CONFIG_DIR)) { 34 | fs.mkdirSync(CONFIG_DIR, { recursive: true }); 35 | } 36 | 37 | // Check for OAuth keys in current directory first, then in config directory 38 | const localOAuthPath = path.join(process.cwd(), 'gcp-oauth.keys.json'); 39 | let oauthPath = OAUTH_PATH; 40 | 41 | if (fs.existsSync(localOAuthPath)) { 42 | // If found in current directory, copy to config directory 43 | fs.copyFileSync(localOAuthPath, OAUTH_PATH); 44 | console.log('OAuth keys found in current directory, copied to global config.'); 45 | } 46 | 47 | if (!fs.existsSync(OAUTH_PATH)) { 48 | console.error('Error: OAuth keys file not found. Please place gcp-oauth.keys.json in current directory or', CONFIG_DIR); 49 | process.exit(1); 50 | } 51 | 52 | const keysContent = JSON.parse(fs.readFileSync(OAUTH_PATH, 'utf8')); 53 | const keys = keysContent.installed || keysContent.web; 54 | 55 | if (!keys) { 56 | console.error('Error: Invalid OAuth keys file format. File should contain either "installed" or "web" credentials.'); 57 | process.exit(1); 58 | } 59 | 60 | oauth2Client = new OAuth2Client( 61 | keys.client_id, 62 | keys.client_secret, 63 | 'http://localhost:3000/oauth2callback' 64 | ); 65 | 66 | if (fs.existsSync(CREDENTIALS_PATH)) { 67 | const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf8')); 68 | oauth2Client.setCredentials(credentials); 69 | } 70 | } catch (error) { 71 | console.error('Error loading credentials:', error); 72 | process.exit(1); 73 | } 74 | } 75 | 76 | async function authenticate() { 77 | const server = http.createServer(); 78 | server.listen(3000); 79 | 80 | return new Promise<void>((resolve, reject) => { 81 | const authUrl = oauth2Client.generateAuthUrl({ 82 | access_type: 'offline', 83 | scope: ['https://www.googleapis.com/auth/calendar'], 84 | }); 85 | 86 | console.log('Please visit this URL to authenticate:', authUrl); 87 | open(authUrl); 88 | 89 | server.on('request', async (req, res) => { 90 | if (!req.url?.startsWith('/oauth2callback')) return; 91 | 92 | const url = new URL(req.url, 'http://localhost:3000'); 93 | const code = url.searchParams.get('code'); 94 | 95 | if (!code) { 96 | res.writeHead(400); 97 | res.end('No code provided'); 98 | reject(new Error('No code provided')); 99 | return; 100 | } 101 | 102 | try { 103 | const { tokens } = await oauth2Client.getToken(code); 104 | oauth2Client.setCredentials(tokens); 105 | fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(tokens)); 106 | 107 | res.writeHead(200); 108 | res.end('Authentication successful! You can close this window.'); 109 | server.close(); 110 | resolve(); 111 | } catch (error) { 112 | res.writeHead(500); 113 | res.end('Authentication failed'); 114 | reject(error); 115 | } 116 | }); 117 | }); 118 | } 119 | 120 | // Schema definitions 121 | const CreateEventSchema = z.object({ 122 | summary: z.string().describe("Event title"), 123 | start: z.object({ 124 | dateTime: z.string().describe("Start time (ISO format)"), 125 | timeZone: z.string().optional().describe("Time zone"), 126 | }), 127 | end: z.object({ 128 | dateTime: z.string().describe("End time (ISO format)"), 129 | timeZone: z.string().optional().describe("Time zone"), 130 | }), 131 | description: z.string().optional().describe("Event description"), 132 | location: z.string().optional().describe("Event location"), 133 | }); 134 | 135 | const GetEventSchema = z.object({ 136 | eventId: z.string().describe("ID of the event to retrieve"), 137 | }); 138 | 139 | const UpdateEventSchema = z.object({ 140 | eventId: z.string().describe("ID of the event to update"), 141 | summary: z.string().optional().describe("New event title"), 142 | start: z.object({ 143 | dateTime: z.string().describe("New start time (ISO format)"), 144 | timeZone: z.string().optional().describe("Time zone"), 145 | }).optional(), 146 | end: z.object({ 147 | dateTime: z.string().describe("New end time (ISO format)"), 148 | timeZone: z.string().optional().describe("Time zone"), 149 | }).optional(), 150 | description: z.string().optional().describe("New event description"), 151 | location: z.string().optional().describe("New event location"), 152 | }); 153 | 154 | const DeleteEventSchema = z.object({ 155 | eventId: z.string().describe("ID of the event to delete"), 156 | }); 157 | 158 | const ListEventsSchema = z.object({ 159 | timeMin: z.string().describe("Start of time range (ISO format)"), 160 | timeMax: z.string().describe("End of time range (ISO format)"), 161 | maxResults: z.number().optional().describe("Maximum number of events to return"), 162 | orderBy: z.enum(['startTime', 'updated']).optional().describe("Sort order"), 163 | }); 164 | 165 | // Main function 166 | async function main() { 167 | await loadCredentials(); 168 | 169 | if (process.argv[2] === 'auth') { 170 | await authenticate(); 171 | console.log('Authentication completed successfully'); 172 | process.exit(0); 173 | } 174 | 175 | // Initialize Google Calendar API 176 | const calendar = google.calendar({ version: 'v3', auth: oauth2Client }); 177 | const calendarId = 'primary'; 178 | 179 | // Server implementation 180 | const server = new Server({ 181 | name: "google-calendar", 182 | version: "1.0.0", 183 | capabilities: { 184 | tools: {}, 185 | }, 186 | }); 187 | 188 | // Tool handlers 189 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 190 | tools: [ 191 | { 192 | name: "create_event", 193 | description: "Creates a new event in Google Calendar", 194 | inputSchema: zodToJsonSchema(CreateEventSchema), 195 | }, 196 | { 197 | name: "get_event", 198 | description: "Retrieves details of a specific event", 199 | inputSchema: zodToJsonSchema(GetEventSchema), 200 | }, 201 | { 202 | name: "update_event", 203 | description: "Updates an existing event", 204 | inputSchema: zodToJsonSchema(UpdateEventSchema), 205 | }, 206 | { 207 | name: "delete_event", 208 | description: "Deletes an event from the calendar", 209 | inputSchema: zodToJsonSchema(DeleteEventSchema), 210 | }, 211 | { 212 | name: "list_events", 213 | description: "Lists events within a specified time range", 214 | inputSchema: zodToJsonSchema(ListEventsSchema), 215 | }, 216 | ], 217 | })); 218 | 219 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 220 | const { name, arguments: args } = request.params; 221 | 222 | try { 223 | switch (name) { 224 | case "create_event": { 225 | const validatedArgs = CreateEventSchema.parse(args); 226 | const response = await calendar.events.insert({ 227 | calendarId, 228 | requestBody: validatedArgs, 229 | }); 230 | return { 231 | content: [ 232 | { 233 | type: "text", 234 | text: `Event created with ID: ${response.data.id}\n` + 235 | `Title: ${validatedArgs.summary}\n` + 236 | `Start: ${validatedArgs.start.dateTime}\n` + 237 | `End: ${validatedArgs.end.dateTime}`, 238 | }, 239 | ], 240 | }; 241 | } 242 | 243 | case "get_event": { 244 | const validatedArgs = GetEventSchema.parse(args); 245 | const response = await calendar.events.get({ 246 | calendarId, 247 | eventId: validatedArgs.eventId, 248 | }); 249 | return { 250 | content: [ 251 | { 252 | type: "text", 253 | text: JSON.stringify(response.data, null, 2), 254 | }, 255 | ], 256 | }; 257 | } 258 | 259 | case "update_event": { 260 | const validatedArgs = UpdateEventSchema.parse(args); 261 | const { eventId, ...updates } = validatedArgs; 262 | const response = await calendar.events.patch({ 263 | calendarId, 264 | eventId, 265 | requestBody: updates, 266 | }); 267 | return { 268 | content: [ 269 | { 270 | type: "text", 271 | text: `Event updated: ${eventId}\n` + 272 | `New title: ${updates.summary || '(unchanged)'}\n` + 273 | `New start: ${updates.start?.dateTime || '(unchanged)'}\n` + 274 | `New end: ${updates.end?.dateTime || '(unchanged)'}`, 275 | }, 276 | ], 277 | }; 278 | } 279 | 280 | case "delete_event": { 281 | const validatedArgs = DeleteEventSchema.parse(args); 282 | await calendar.events.delete({ 283 | calendarId, 284 | eventId: validatedArgs.eventId, 285 | }); 286 | return { 287 | content: [ 288 | { 289 | type: "text", 290 | text: `Event deleted: ${validatedArgs.eventId}`, 291 | }, 292 | ], 293 | }; 294 | } 295 | 296 | case "list_events": { 297 | const validatedArgs = ListEventsSchema.parse(args); 298 | const response = await calendar.events.list({ 299 | calendarId, 300 | timeMin: validatedArgs.timeMin, 301 | timeMax: validatedArgs.timeMax, 302 | maxResults: validatedArgs.maxResults || 10, 303 | orderBy: validatedArgs.orderBy || 'startTime', 304 | singleEvents: true, 305 | }); 306 | return { 307 | content: [ 308 | { 309 | type: "text", 310 | text: `Found ${response.data.items?.length || 0} events:\n` + 311 | JSON.stringify(response.data.items, null, 2), 312 | }, 313 | ], 314 | }; 315 | } 316 | 317 | default: 318 | throw new Error(`Unknown tool: ${name}`); 319 | } 320 | } catch (error) { 321 | return { 322 | content: [ 323 | { 324 | type: "text", 325 | text: `Error: ${error instanceof Error ? error.message : String(error)}`, 326 | }, 327 | ], 328 | isError: true, 329 | }; 330 | } 331 | }); 332 | 333 | // Start the server 334 | const transport = new StdioServerTransport(); 335 | server.connect(transport).catch((error) => { 336 | console.error("Fatal error running server:", error); 337 | process.exit(1); 338 | }); 339 | console.error('Google Calendar MCP Server running on stdio'); 340 | } 341 | 342 | main().catch(console.error); ```